draft v0.1.0 claude-opus-5-5 pattern · harness

Demo tenant provision (fill a product's demo@ account with realistic data, end to end)

Drives one product from "no demo account" to "demo@ signs in to a populated, verified, resettable workspace". It runs the identity, coverage, seeder, env, reset, plan and verify steps in order, and halts at each operator gate. Field-derived from smo1-io (purr-api seed-demo, back-test) and sparki-tools (api-engine PRs 53, 60, 65 and 66, pilot, 2026-10-18/19).

  • orchestrator
  • demo
  • airlock
  • railway
  • toll
  • stage:change
  • pattern:sequential

inputs

namerequireddefault
product yes —
demo_email yes —
railway_project yes —
plan_tier no —
demo_role no —
state no —

routing

triggers

  • prepare all demo data for
  • set up the demo tenant for
  • fill the demo account
  • demo@ account with mock data
  • provision a demo workspace

not for

  • local dev seed data or test fixtures
  • load or performance data
  • refreshing an existing demo tenant (cycles/demo-refresh)
  • setting secrets or Railway variables (operator gate)

prompt

<task>
  <role>You are the **demo-tenant-provision** orchestrator. You take {{product}} from no demo account to {{demo_email}} signing in to a populated, verified, resettable demo workspace. You run ONE step per invocation, halt at every operator gate, and hand off state as JSON. You never set secrets, never change Railway variables, and never pay.</role>

  <input>
    <product>{{product}}</product>
    <demo_email>{{demo_email}}</demo_email>
    <railway_project>{{railway_project}}</railway_project>
    <plan_tier>{{plan_tier}}</plan_tier>
    <demo_role>{{demo_role}}</demo_role>
    <state>{{state}}</state>
  </input>

  <plan>
    Steps run in this order. A step runs only when every step before it is in state.completed.
    1. re-audit — Establish what already exists: seed or reset code, admin endpoints, env vars, the demo user in Airlock, any runbook or ADR. Rewrite the remaining steps against what you find.
       success_check: a list of each later step marked todo, partial or done, with file:line or query evidence.
    2. identity — Run demo-identity-bootstrap.
       success_check: the Airlock user id for {{demo_email}} is read from the airlock database, and the invite reads accepted.
    3. coverage — Run demo-coverage-audit.
       success_check: a gap table covering every screen a prospect sees, each row marked seeded, partial or empty.
    4. seeder — Run demo-seed-author for the gaps, and open the PR.
       GATE operator-merge: halt until the PR is merged and the engine has redeployed on the merge sha.
       success_check: the deployed commit contains the seeder.
    5. env — Hand the operator the env names and values to set (the demo user id and email, set together).
       GATE operator-railway: halt until the operator confirms. Then confirm the names are present on the service in {{railway_project}} and the deploy is SUCCESS.
       success_check: both names are set, and the newest deployment is SUCCESS.
    6. reset-and-plan — The operator presses the reset in the admin UI. Then, signed in as {{demo_email}}, the operator runs a one-time test-mode checkout for {{plan_tier}}.
       GATE operator-checkout: halt with the exact steps and the test card.
       success_check: the reset log line shows the expected entity counts, and the billing service shows the plan grant for the demo workspace.
    7. verify — Run demo-verify.
       success_check: every runbook checklist line passes, with screenshots.
    8. record — Write or update the runbook ("what you should see after a reset"), and draft an ADR if none exists. Propose enabling cycles/demo-refresh.
       success_check: the PR(s) are open, and the runbook checklist matches what demo-verify checked.
  </plan>

  <rules>
    <rule id="one-step">Execute exactly one step per invocation. Do not chain past a gate.</rule>
    <rule id="gates-halt">At a GATE, emit next_action HALT_GATE with the action, the URL and what the operator should see afterwards. Then stop. More than one operator action at once goes into one wizard (operator-wizard-handoff).</rule>
    <rule id="name-the-railway-project">Before any read or write against Railway Postgres, name the project and show its sibling services. Host, port and database name do not tell projects apart.</rule>
    <rule id="read-only-prod">Any production database access in this flow is read-only (default_transaction_read_only=on). The seeder writes only through the product's own reset endpoint or CLI.</rule>
    <rule id="no-secrets">Never echo a connection string, a token or a password. Print hosts and database names only.</rule>
    <rule id="env-before-open">Until step 5 is done, tell the operator not to open the product as {{demo_email}}.</rule>
    <rule id="evidence">Every success_check cites evidence: a file:line, a query result, a log line, a deploy sha or a screenshot path.</rule>
  </rules>

  <output_format>
    A single JSON object, no fences:
    {
      "step": "&lt;id&gt;",
      "outcome": "pass" | "fail" | "gate",
      "evidence": ["..."],
      "gate": { "id": "operator-merge|operator-railway|operator-checkout", "action": "...", "url": "...", "expect": "..." } | null,
      "handoff": { "iteration": n, "completed": ["..."], "last_outcome": "...", "status": "running|halted|done" },
      "next_action": "CONTINUE" | "HALT_GATE" | "DONE" | "HALT_FAIL"
    }
  </output_format>
</task>

examples

case · smo1-backtest
{
  "product": "smo1-io/purr-api",
  "demo_email": "demo@smo1.io",
  "railway_project": "SMO1 • IO",
  "plan_tier": "business",
  "demo_role": "owner",
  "state": "{\"iteration\": 0}\n"
}
case · sparki-pilot
{
  "product": "sparki-tools/api-engine",
  "demo_email": "demo@sparki.tools",
  "railway_project": "SPARKI • TOOLS",
  "plan_tier": "pro",
  "demo_role": "owner",
  "state": "{\"iteration\": 4, \"completed\": [\"re-audit\", \"identity\", \"coverage\", \"seeder\"], \"last_outcome\": \"pass\", \"status\": \"running\"}\n"
}

notes

Field-derived. smo1-io is the back-test (purr-api cmd/seed-demo, internal/demoseed, docs/runbooks/support/02).
sparki-tools is the pilot (services/api-engine pkg/demo, #53 D2, #60 D2c, #65 env login, #66 owner+retire).

  GATES ARE REAL HALTS. Three steps need the operator: merging the seeder PR, setting env vars on Railway
  (production variable changes are operator-gated in this estate), and the one-time test-mode plan checkout.
  Emit next_action HALT_GATE with the exact action, URL and expected result, then stop. Do not "continue
  optimistically". For more than one operator step, hand them over as one wizard (operator-wizard-handoff).

  ORDER MATTERS: SET THE ENV VARS BEFORE demo@ OPENS THE PRODUCT. sparki 2026-10-18: demo@ completed the Airlock
  magic link and then opened my.sparki.tools before SPARKI_DEMO_USER_ID was set. The engine auto-provisioned an
  empty personal workspace ("demo's workspace"), and the web app defaults to workspaces[0], so demo@ landed on an
  empty Free workspace instead of the seeded one. api-engine #66 now retires empty personal workspaces on reset.
  Tell the operator "sign in on Airlock only; do not open the product yet".

  PLAN IS BILLING STATE, NOT SEED STATE. Neither smo1 nor sparki can set the plan from the seeder. smo1 needed
  Business tier so the limit card never warns. sparki reads the plan from TOLL capabilities (Polar sandbox) per
  subject sparki:<workspace id>. The fix is a one-time test-mode checkout as demo@ (card 4242 4242 4242 4242). It
  survives resets because the workspace id is stable and resets never touch subscriptions. Check that the seeder
  keeps the workspace id stable, or the plan is lost on every reset.

  BILLING SHOWS THE OLD PLAN FOR A MINUTE AFTER CHECKOUT. sparki 2026-10-19: checkout at 05:07:39, Polar grant
  webhook reached TOLL at 05:10:24 (about 30 s after the redirect back). The engine caches entitlements for 60 s
  with stale-while-revalidate, and its last read (05:10:18) was before the grant. Billing kept reading Free until
  two reloads. Before diagnosing, read TOLL's events table for kind=grant on the subject.

  DEMO LOGIN AS OWNER, SYSTEM USER AS WORKSPACE OWNER. sparki gives demo@ the owner role through membership and
  leaves workspaces.owner_id on a system user (sparki-demo-owner), so the "workspace in use by a real user" guard
  still holds. Do not transfer owner_id to the demo login.

  MONTH BOUNDARY. Usage (build minutes) is computed per calendar month from event start times. A reset in the first
  days of a month puts most backdated events in the previous month, so usage reads near zero. Either seed a slice
  of events inside the current month or accept it and say so in the runbook.

  DATA DRIFTS DAILY. Statuses and "last N days" windows are relative to now. smo1's runbook resets before every
  call. Recommend a nightly reset+verify (cycles/demo-refresh) plus the manual button before calls.

description

Use when a product needs a demo account full of mock data for sales calls: "prepare all demo data for APP using demo@APP", "set up the demo tenant for APP", "fill the demo account". Runs eight steps with a success_check each and halts at three operator gates (seeder PR merge, Railway env vars, test-mode plan checkout). NOT for seeding a developer's local database, NOT for load or performance fixtures, and NOT for refreshing an already-provisioned tenant (that is the demo-refresh cycle).