Skip to Content

← All archived runs

Run: demo-canned-agents

run.md

Run: demo-canned-agents

  • branch: claude/demo-canned-agents-pipeline-bw9nhw
  • pr: #793

03_define/output/spec.md

Spec: Canned agent mode — deterministic onboarding + BRD replay for live walkthroughs

  • slug: demo-canned-agents
  • personas: Vendor, Customer, Admin
  • touches: packages/services/src/ai, apps/web/app/api/onboarding/chat, apps/web/app/api/brd/chat, apps/web/components/onboarding, apps/web/components/projects/brd-chat, apps/web/app/(app)/projects/brd/[id], turbo.json
  • complexity: complex
  • demo: none

Problem

Onboarding and BRD/project discovery are the two streaming-LLM wow-moments in a client walkthrough, and the most fragile things in a scripted pitch: a latency spike, a wording change between rehearsal and stage, or a provider incident lands mid-sentence in front of a prospect. Today both flows go straight to the AI Gateway on every request — /api/onboarding/chat builds the concierge agent per tenant, /api/brd/chat streams the singleton BRDAgent — with no deterministic path anywhere. The old apps/demo solved this with canned recordings of these same agents, but that app is the pipeline's prototyping surface and the demo now runs on the real platform, so the safety net has to exist in the product without forking the features (report E). This advances Refine the bridge / Q2-2026 Objective 1 — Establish Product-Market Fit with Vendor Partners: the demo tenant is only client-credible if its best two minutes are repeatable.

Proposed change

A canned, deterministic replay mode for the onboarding and BRD agents, reachable only on a tenant with isDemo: true, and layered at the model boundary so the agents, tools, routes and chat UI stay as they are.

  1. Recorded transcripts as in-repo fixtures. One transcript per flow (onboarding concierge, BRD qualification), stored as versioned JSON alongside the AI code, describing the assistant's turns as an ordered script of text segments and tool calls. Fictional content only, self-consistent with the demo storyline world. Being in-repo means Paul signs the storyline off as a PR diff, and a demo-reset-ops purge can never take the transcripts with it. A load-time schema check rejects a malformed transcript loudly rather than degrading mid-pitch.

  2. Replay at the provider boundary. A replay model implementing the AI SDK's language-model interface consumes a transcript and emits the same stream parts a live model emits — text deltas with a paced cadence, then tool calls — so ToolLoopAgent, createAgentUIStreamResponse, useChat and every approval affordance behave exactly as they do live. No external call is made. The BRD agent gains a builder taking a model (the existing BRDAgent singleton export stays, built on the default gateway model); the onboarding builder gains the same parameter.

  3. Tool behaviour: real local writes, replayed external calls. Tools whose execute only touches the caller's own tenant data run for real, so the walkthrough produces a genuine BRD and a genuinely populated blueprint that later screens can show. The one tool that reaches outside — analyse_source, which fetches a website and runs a second deep-extraction model call — is served from the transcript in canned mode, which is what keeps the no-external-call guarantee true end to end.

  4. Presenter's session toggle, canned by default. On the demo tenant the two chat surfaces show a demo-only control that flips the session between canned and live; the preference is session-scoped and canned is the default, so the safety net is on unless the presenter chooses to show the real thing. Setting it is guarded server-side by the same demo-tenant check that gates the mode itself.

  5. Inert for real tenants. Mode selection is resolved server-side per request from the authenticated tenant's isDemo flag (via the existing tenantService.isDemoTenant), never from client input alone. A request from an isDemo: false tenant takes the live path whatever cookie, header or body it carries, and the demo-only control does not render for it.

  6. The presenter's toggle becomes the only mode logic on the BRD surface. The FAST_BRD environment flag and everything hanging off it — the fast-path component, its generateBrdFast server action and the shell's branch — are removed outright, along with the flag's turbo.json entry. Today that flag silently pre-empts the whole feature: where it is set, the BRD page renders a generate-and-approve panel instead of the chat, so the presenter toggle never mounts and the BRD is written by a direct gateway call that canned mode cannot reach. After this change every environment renders the same BRD chat, and canned-vs-live is decided solely by the demo-tenant toggle. This is a deliberate behaviour change for any environment that had FAST_BRD=true — it gains the chat it was skipping.

  7. Skip-to-accepted, in canned mode only. With canned mode active on the demo tenant, the BRD chat offers a skip control that approves the transcript's BRD straight onto the lead — the same approveBrd write path a live approval takes, using the overview and requirements the transcript's generate_brd step already carries — and lands the presenter on the workspace. It replaces the value the removed fast path provided (skipping the conversation) without reintroducing a second BRD-generation path or any model call. It does not render in live mode or off the demo tenant.

Acceptance criteria

  • With canned mode active on the demo tenant, the onboarding and BRD flows stream their recorded transcripts to completion with no request to the AI Gateway and no dependence on provider latency.
  • The canned experience is interactively equivalent to live: tokens stream at a human cadence, tool calls raise the same approval steps, approving proceeds and denying is handled as it is today, and the resulting artifacts render through the existing sanitized-HTML path.
  • In canned mode the tools that write only the caller's own tenant data execute for real — the BRD is generated and the onboarding blueprint is populated — while analyse_source returns its transcript result without a network fetch or a second model call.
  • A tenant with isDemo: false cannot reach canned mode by any input: cookie, header, request body or crafted message all still take the live path, and the demo-only toggle is not rendered for it. Unit coverage proves the resolver refuses on the flag, not on the client's say-so.
  • On the demo tenant the presenter can switch the session to the live LLM path and back from both chat surfaces, and the next message in that session follows the chosen path.
  • A transcript fixture that fails its schema check fails loudly at load with a message naming the file, rather than streaming a partial or empty conversation.
  • Zero behaviour change for isDemo: false tenants: with the flag off, both flows behave exactly as they do on main today — save for the deliberate removal of the FAST_BRD fast path below, which applies to every tenant in an environment that had it set.
  • FAST_BRD is gone from the codebase and from turbo.json's globalEnv: no component, action, page branch or env read survives, and the BRD chat is what renders in every environment. No BRD path anywhere calls a model outside the agent.
  • On the demo tenant in canned mode, a skip control approves the transcript's BRD onto the lead and lands on the workspace — the lead carries the same description/requirements a live approval would write, and no request reaches the AI Gateway.
  • The skip control does not render in live mode, and does not render for an isDemo: false tenant by any input.

Out of scope

  • Canning any other AI feature — matching, success and recruiter agents are untouched; this run covers the onboarding concierge and the BRD qualification agent only.
  • Any behaviour change to the live agents, their prompts, their tools or their routes for real tenants — with the single, deliberate exception of removing the FAST_BRD fast path, which returns those environments to the standard BRD chat. The prompt-injection defenses and the client-side DOMPurify allowlist are unchanged.
  • Any replacement for the fast path outside canned mode. Removing FAST_BRD removes a dev shortcut; reinstating an equivalent for real tenants is separate work, not this run.
  • Transcript capture tooling — a dev harness that records a live conversation into a fixture. Transcripts are authored by hand against the storyline this run; a recorder is a later candidate if editing them by hand proves painful.
  • Final storyline naming. Transcript prose is fictional and self-consistent now; reconciling names and numbers with demo-seed-storyline once it lands is a content-only edit to the fixtures, not a code change.
  • The presenter persona quick-switch and the demo-flavoured view-as chrome — demo-presenter-switching.
  • Purge/reseed and the nightly reset — demo-reset-ops. Fixtures are deliberately outside the data that reset touches.
  • Any auth bypass or public no-login launcher; no new bypass in the proxy or the route policies.
  • Persisting the canned/live preference beyond the presenter's session, or exposing it to non-admin users on the demo tenant.

Open questions

  • none

04_build/output/notes.md

Build notes: demo-canned-agents

  • commits: feat: demo-canned-agents — canned replay mode for the onboarding and BRD agents, fix: … derive the V3 spec type, fix: … return the required warnings, verify: … transcript and replay fixes, feat: demo-canned-agents — remove FAST_BRD, add the canned skip to approved BRD
  • demo: none (the demo-tenant scope skipped Design; this run is built from the spec)

What changed

First build (canned replay mode)

  • packages/services/src/ai/canned/ (new) — the whole replay subsystem, client-safe (JSON fixtures plus a pure model; no DB, no network):
    • contract.ts — the transcript Zod schema. A transcript is an ordered list of steps, each carrying prose and/or tool calls, plus externalToolResults for the tools that must not run.
    • transcripts/{onboarding,brd}.json — the two recorded walkthroughs. Fictional throughout (Halden Systems, Verhoeven Distributie; haldensystems.example is the reserved documentation TLD and resolves nowhere).
    • replay-model.ts — a LanguageModelV3 that streams a transcript instead of calling a provider. This is the whole seam: everything above it (ToolLoopAgent, createAgentUIStreamResponse, useChat, the approval affordances) is untouched.
    • registry.ts — schema-validates a transcript on first canned use and caches it.
    • mode.ts — the pure resolveAgentMode resolver plus the cookie name.
    • agent-model.ts — the one call a route makes: flow in, model + replayed tool results out.
  • ai/brd/index.tsBRDAgent was a singleton pinned to the gateway model; extracted buildBrdAgent({ model }) and kept BRDAgent as the default-model instance, so the live import site is unchanged.
  • ai/core/agent.ts, ai/onboarding/agent.tsmodel widened from string to the SDK's LanguageModel, so a model instance can be passed.
  • ai/onboarding/blueprint-tools.tsanalyse_source became a factory taking an optional canned result. It is the only concierge tool that leaves the platform; short-circuiting it is what makes the no-external-call guarantee true. Every other tool executes for real.
  • apps/web/lib/agent-mode.ts (new) — resolves the mode per request from the tenant record, and separately answers "should the presenter controls render at all".
  • apps/web/app/api/{onboarding,brd}/chat/route.ts — resolve the mode, build the agent on the replay model when canned. Auth, role, tenant and ownership checks are unchanged.
  • apps/web/lib/actions/demo-agent-mode.ts (new) + components/demo/demo-agent-mode-toggle.tsx (new) — the presenter's canned/live switch and the action behind it.

Verify-stage fixes

  • The onboarding transcript now drafts the products the crawl returns (it previously drafted only the profile, so the gap interview read an empty section and the walkthrough left slug-labelled products). Two regression tests in registry.test.ts.
  • buildCannedModelOnly replaces the BRD route's silent drop of cannedToolResults — it throws naming the flow, so a future transcript entry can't let the real external call run.
  • chunkText no longer drops leading whitespace, so the stream matches doGenerate.

This build (FAST_BRD removal + skip control)

  • Removed FAST_BRD outright: components/projects/brd-chat/brd-fast-path.tsx deleted, the generateBrdFast action and its schema removed from projects/brd/[id]/actions.ts, the fastBrd branch and prop removed from the page and brd-qualification-shell.tsx, and the FAST_BRD entry removed from turbo.json's globalEnv. The BRD chat is now what renders in every environment, and no BRD path calls a model outside the agent.
  • packages/services/src/ai/canned/artifacts.ts (new) — getCannedBrdDocument() reads the overview/requirements off the transcript's own generate_brd step and validates them, so the skip approves exactly the document the played-through conversation would have produced. Pure and client-safe (zod + the JSON fixture only), so the /ai entrypoint stays clean.
  • skipCannedBrd in projects/brd/[id]/actions.ts (new) — re-resolves the mode server-side via resolveRequestAgentMode and refuses unless it is canned, re-checks lead ownership the same way the removed action did, then writes through the existing leadService.approveBrd.
  • components/demo/skip-canned-brd-button.tsx (new), rendered from the BRD chat header only when demoAgentMode === "canned" — approves and navigates to the workspace.
  • .claude/skills/ai-feature/SKILL.md — its one-shot-generation example pointed at generateBrdFast; replaced with the lesson (don't gate a whole surface on an env flag).

Acceptance criteria status

  • Canned mode streams the transcripts with no external call — the replay model is a LanguageModelV3; nothing in canned/ opens a socket or reads a gateway credential.
  • Interactively equivalent to live — implemented, but a looks-right judgement left for Verify's smoke pass. Note the known gap: denying the BRD approval still advances the derived step index, so the replay serves the success turn. Unchanged by this build.
  • Local writes execute, external calls are replayed — only analyse_source is replayed.
  • isDemo: false cannot reach canned moderesolveAgentMode returns live on the flag before it reads the preference; mode.test.ts asserts the refusal across nine values.
  • Presenter can switch to live and back — session cookie, canned by default.
  • A bad fixture fails loudly, naming the file.
  • Zero behaviour change for isDemo: false — save for the deliberate FAST_BRD removal.
  • FAST_BRD is gone — no component, action, page branch, env read or globalEnv entry survives; a repo-wide grep is clean apart from the skill's rewritten note.
  • Canned skip approves the transcript's BRD onto the leadskipCannedBrd writes description/requirements through leadService.approveBrd, identical to a live approval, then lands on /workspace/<leadId>. No gateway call anywhere on that path.
  • The skip does not render in live mode or off the demo tenant — the control is behind demoAgentMode === "canned", and the action re-checks the mode server-side, so calling it directly on a real tenant is refused rather than silently ignored.

Notes for Verify

  • The FAST_BRD removal is the risky half. Any environment that had the flag set now renders the BRD chat instead of the generate-and-approve panel. That is the intent, but it is a user-visible change for those environments and the first thing to look at on the preview.
  • The deny-path gap is still open (was finding 4 in the previous verify record). This build did not touch it; it remains a judgement call for the gate.
  • The skip's guard is defence in depth, not decoration — worth confirming the action refuses when the toggle is flipped to live, since that is the one path a presenter can reach.
  • getCannedBrdDocument is lazy like the rest of the registry: a malformed generate_brd payload throws on first skip rather than at module load, so a broken fixture cannot take the live BRD chat down.
  • The three review passes in the previous Verify ran against the pre-FAST_BRD tree — they need re-running over this diff.
  • Tests written, not run — pnpm test is the factory's (QualityRun tests).

05_verify/output/verify.md

Verify: demo-canned-agents

Second pass — re-run in full over the post-FAST_BRD tree (798e94a). The first pass reviewed a tree that no longer exists: the scope changed mid-run (see finding 10), so all three passes were re-run rather than topped up. The first pass's three code fixes are still in the branch and are listed under "Carried from the first pass".

  • production-readiness: re-run — no blocker. The FAST_BRD removal is clean (no dangling reference anywhere, globalEnv entry gone, NODE_ENV the only remaining env read and already declared); the new write path is gated on demo-tenant and lead ownership, with leadService.approveBrd re-filtering on {tenantId, _id, customer} itself. Five non-blocking items below, one of which is a config action outside the repo.
  • code-review: high effort (spec complexity: complex), run inline — ENABLE_CLAUDE_REVIEW is still unset, so the CI review job reports skipped. Two findings: one real bug in this round's own code (fixed on branch), one in code this run does not own (referred out).
  • security-review: re-run — no HIGH or MEDIUM findings. Detail below.
  • playwright: TODO — manual DoD smoke required; still not performed.

Security review

The only new attack surface is skipCannedBrd, a server action that writes lead data. It holds:

  • Authorization rests on the flag, not the request. resolveRequestAgentMode reads isDemoTenant from the tenant record and returns live before consulting the cookie; the action throws unless the mode is canned, and that check sits inside runActionBody, so refusal precedes any write. A real tenant cannot reach it by forging a cookie, header or body.
  • Tenant scoping is server-derived. tenantId comes from resolveActionContextgetTenant() (Clerk org); the Zod schema accepts leadId and nothing else.
  • No IDOR. Ownership is checked twice — lead.customer._id === currentUser._id in the action, then approveBrd's own {tenantId, _id, customer} filter — and allowedRoles: ["customer"] keeps staff who can load the page out of the write.
  • No injection. leadId reaches Mongo as an equality filter through the service layer.
  • Stored content is repo-controlled. The approved document is fixture data, Zod-validated by getCannedBrdDocument, written through the same path a live approval uses and rendered through the unchanged client-side DOMPurify allowlist. No user input reaches it.
  • The removal is surface-reducing — it deletes an authenticated action that made a live gateway call, and nothing weaker replaces it.

Not a security control: the client-side currentAgentMode gating added this round only hides the button in live mode. skipCannedBrd's server-side check is the boundary and stands alone if the client is bypassed.

DoD smoke (on the preview — each line says who verified it)

Still not started — this remains the outstanding work, and it now matters more than it did: the riskiest change on the branch (every environment that had FAST_BRD=true now rendering the chat) has no automated coverage at all. Preview: https://web-git-claude-demo-canned-agents-pipeline-bw9nhw-sustentus.vercel.app

  • Canned onboarding + BRD stream to completion with no AI Gateway request (operator)
  • Interactively equivalent to live: cadence, approval steps, deny handling, sanitized render — see finding 4, expected to fail on the deny path (operator)
  • Local tool writes execute for real; analyse_source served from transcript; blueprint genuinely populated — re-test after first-pass fix 1 (operator)
  • isDemo: false tenant cannot reach canned mode by any input (agent: traced and unit-covered; operator: confirm no toggle on a real tenant)
  • Presenter can switch canned↔live from both chat surfaces (operator)
  • A malformed fixture fails loudly naming the file (agent: unit-covered)
  • Zero behaviour change for isDemo: false tenants, FAST_BRD removal aside (operator)
  • FAST_BRD is gone and the BRD chat renders everywhere — agent: grep-verified and typecheck/build green; operator: confirm a former-FAST_BRD environment now renders a working chat, since that is the user-visible half nothing else covers
  • The canned skip approves onto the lead and lands on the workspace (operator — one click, the cheapest criterion on this list to exercise)
  • The skip does not render in live mode — re-test after this round's fix 1: flip the toggle to Live without reloading and confirm the button disappears (operator)
  • auth: Vendor + Customer sign-in and reach their dashboards (operator)
  • payments: not touched by this diff
  • notifications: none expected directly — but see finding 3: the skip fires the expert matcher, so matcher-driven activity on the demo tenant is expected, not a fault

Findings & cleanup — second pass

Fixed on branch:

  1. The skip button survived a flip to live mode. It rendered off the server-supplied demoAgentMode, while DemoAgentModeToggle mutates the mode client-side with no route refresh. Toggling to "Live model" left the button visible, and clicking it failed the server guard with a raw error; live→canned left it hidden when it should work. This broke the acceptance criterion "the skip control does not render in live mode" for any session that used the toggle — the criterion only held on a fresh load. Fixed by hoisting the mode into the chat's state: the toggle now reports every accepted change via an optional onModeChange, and the skip gates on that. The server guard was always correct; this closes the visible half.

Surfaced — not fixed, and not this run's to fix:

  1. The demo seeder dropped its per-lead activity log (db/services/demo-data/index.ts:1259). createLead lost its logActivity("CREATE", …), so 49 of 61 seeded leads — every pending/backlog/delivered/survey/completed lead plus the whole 36-engagement backdated tail — end with an empty activity timeline, and the admin summary's "Activity entries" row under-reports. No comment justifies the removal. This is demo-seed-storyline (#795) code, already merged, not this run's diff — it surfaced only because the review covered the working tree. Recorded here so it is not lost; it belongs in intake against that run, not in this PR.

Surfaced for your judgement:

  1. The "skip" publishes the lead to the expert matcher. approveBrd sets bidPool = true, fires runLeadExpertMatcher and logs a brdApproved activity. That is correct — it is precisely the path a live approval takes, which is the point of reusing it — but a control labelled "Skip to approved BRD" having that reach is worth knowing before the first walkthrough, and worth a line in the ship note. No change recommended.
  2. Denying the BRD approval still claims success. Carried unchanged from the first pass and untouched by this build. Step index is derived from the assistant turn count, which cannot distinguish approve from deny: a denial still appends a turn, so the replay serves the success step and the assistant says "That's your BRD. It's now attached to this enquiry" when nothing was written. Acceptance criterion 2 is still not met as written. Fixing it means the replay model inspecting tool results and branching — a design change, not a content edit. A presenter will approve in practice; accepting it as a known limitation is defensible, and the alternative is back to Define. Your call at the gate.
  3. No reset affordance. Also carried. A second onboarding run resumes at the clamped final step rather than the greeting, and mark_blueprint_complete executes for real, so the demo tenant is left completed. demo-reset-ops (4 of 6) covers this — don't promise a back-to-back repeat walkthrough until it lands.

Config and hygiene, from production-readiness:

  1. Delete FAST_BRD from Vercel at merge time. With the globalEnv entry gone, turbo strips the variable from every build and it is inert — but it stays in the dashboard reading as live config. Remove it wherever it is set. Note the interaction with rollback: reverting this PR restores the code and the globalEnv entry, but only restores the old preview behaviour if that variable still exists. Delete it and a later revert gives you the chat, not the fast path.
  2. Socialise the preview behaviour change. Anyone who used a FAST_BRD environment to reach an approved BRD quickly loses that route unless they are on the demo tenant, where the skip replaces it. On other tenants the BRD is now a five-turn conversation that spends gateway calls.
  3. The new action uses the legacy role shim. skipCannedBrd declares allowedRoles: ["customer"] with no requiredPermission — legal (the file is not in migratedActionFiles, so the lint rule never fires) and identical to its neighbour approveBrd, but it adds a call site to the permission-cutover backlog. Intake, not a release gate.
  4. Docs not synced, and the gap moved. apps/docs/.../technical/demo-environment is the page a presenter would actually read — it covers isDemo and persona switching but not the canned/live toggle, the session cookie or the skip. The services package page still omits ai/canned/. Both are Ship's docs-sync obligation. No changelog entry either; defensible since nothing is visible to a real tenant, but the FAST_BRD removal is visible to preview users — either add the entry or state the omission is deliberate.

Environment findings (unchanged from the first pass)

  1. The AI Gateway is out of credits. A BRD run returned "Free tier users do not have access to this model". Billing, not code: BRDAgent uses the same gateway model on main, so the same click fails identically there. The live half of criterion 5 cannot be demonstrated until credits are topped up; the canned half can. Noted by decision rather than actioned.
  2. migrate-preview is a vacuous green. The GitHub preview environment has no MONGODB_URI, so the job takes its exit 0 skip path and reports success having migrated nothing, against a preview database CI never touches (preview-db-migrations/preview-db-wiring.md, cut on main during this run). Treat this PR's "Migrate preview database ✅" as unverified, not as a passing gate. An earlier reading of mine — that no preview tenant carries isDemo — was wrong and is withdrawn; the operator confirmed one exists.
  3. FAST_BRD bypassed canned mode entirely — the scope change this pass exists to cover. With the flag set, the BRD page rendered BrdFastPath instead of the chat, so the presenter toggle never mounted and generateBrdFast called the gateway directly, bypassing the agent, the route and the replay model. That explained the missing toggle, the immediate BRD generation and the gateway error, and withdrew three earlier isDemo hypotheses. Resolved by removing the flag outright (owner decision); the spec was revised and rebuilt accordingly.

Carried from the first pass — fixes still in the branch

  • The onboarding walkthrough never drafted its products. analyse_source returns two product candidates but persists nothing, and only profile was drafted, so the gap interview read an empty section and update_product_profile created the products from scratch with no name — leaving them slug-labelled with 4–6 fields. Added the products draft_section to step 3; two regression tests in registry.test.ts.
  • cannedToolResults was silently discarded on the BRD path. Replaced with buildCannedModelOnly, which throws naming the flow rather than letting a future transcript entry trigger the real external call.
  • chunkText dropped leading whitespace, diverging the stream from doGenerate. Fixed and covered.

Context note: main was merged into this branch at b73cdd4, bringing demo-presenter-switching (#794), demo-seed-storyline (#795) and the preview-db-migrations intake cut (#796). The seeded storyline is therefore on this branch, so the smoke pass will show directly whether the Halden transcript reads as a stranger beside it — a content-only follow-up per the spec, not a defect.

Context budget: within band — the three passes read the branch diff and the files it names.

06_ship/output/investor-update.md

A client walkthrough that no longer waits on a model

Who it's for: Presenters demoing to vendor partners What shipped: On the demo tenant, the onboarding and requirements assistants replay a recorded script instead of calling a model — with a switch back to live, and a one-click skip to an approved requirements document. Why it matters: Refine the Bridge / Q2 2026 Objective 1 — Establish Product-Market Fit with Vendor Partners: the demo is repeatable.

Dig deeper: https://github.com/sustentus/sustentus/pull/793 · no changelog entry — nothing changes for a real tenant

06_ship/output/release.md

Ship: demo-canned-agents

  • pr: #793 · merged: yes — 2026-08-13, squash, on the ticked Ready to merge box and green checks
  • CI: green after two fixes this stage — Quality Project failed on 086ae4d because removing the fastBrd branch left BrdQualificationShell a single return in a block body, tripping arrow-body-style and taking the tree to 46 warnings against the workflow's --max-warnings=45 ceiling (b7d50b8). Earlier in the run: two LanguageModelV3 typing failures in the services build (bc191af, 17104c0).
  • technical docs: technical/demo-environment (new Canned agent mode section — the switch and its cookie, the real writes, the skip's downstream reach, where the transcripts live, and the two known limits) · technical/packages/services (the ai/canned/ entry, the source-structure line, and the gateway line now qualified: canned makes no provider call)
  • business docs: no business docs impact — nothing here is reachable by a tenant without isDemo, so no persona capability, journey step or platform-overview claim changes
  • release notes: ship-note-only — no end-user note. The feature is invisible to every real tenant; the one user-visible effect (FAST_BRD environments now render the BRD chat) lands on internal preview users, not customers, so a help-centre changelog entry would announce nothing a reader could act on. This is the deliberate omission Verify's finding 9 asked to be stated.
  • sent: none — the ship note is written and will send itself on the merge (.github/workflows/ship-note.yaml), which has not happened

Dig deeper was filled before the merge, deliberately. The contract's step 5 says to fill those links afterwards, which was right when the agent sent the note by hand; it is wrong now. The workflow fires on pull_request: closed and reads the file as merged, so a <merged-PR URL> placeholder would go out verbatim in the email — a post-merge edit reaches the repo but not the inbox. The PR URL is knowable in advance, so it is in the file already, and there is no changelog link to wait for. Worth fixing in the Ship contract.

Acceptance check (vs spec)

  • Canned mode streams both transcripts with no AI Gateway request — the replay model is a LanguageModelV3; nothing under ai/canned/ opens a socket or reads a gateway credential. Verified by code review in Verify; not yet demonstrated on the preview.
  • Interactively equivalent to live — not met as written. Denying the BRD approval still advances the derived step index, so the replay serves the success turn and the assistant claims the document was attached when nothing was written. Verify finding 4; unchanged by either build round. Fixing it means the replay model inspecting tool results and branching — a design change, not a content edit.
  • Local writes execute, external calls replayed — only analyse_source is served from the transcript; every other tool runs for real.
  • isDemo: false cannot reach canned mode — resolveAgentMode returns live on the flag before it reads the preference; mode.test.ts asserts the refusal across nine values.
  • Presenter can switch canned ↔ live from both surfaces — session cookie, canned by default.
  • A malformed fixture fails loudly naming the file.
  • Zero behaviour change for isDemo: false tenants, save the deliberate FAST_BRD removal.
  • FAST_BRD is gone — component, action, page branch, env read and globalEnv entry; a repo-wide grep is clean apart from the ai-feature skill's rewritten note.
  • The canned skip approves the transcript's BRD onto the lead and lands on the workspace — skipCannedBrd writes through leadService.approveBrd, unit-covered by artifacts.test.ts.
  • The skip does not render in live mode or off the demo tenant — client gating fixed this run (it survived a toggle to live before 405fe4b), and the action re-checks the mode server-side.

What was decided at the gate, and what it does not cover

Both boxes were ticked and the run merged. Two things the gate decided by proceeding, recorded plainly so nobody later reads this merge as evidence they were resolved:

  1. Criterion 2 ships unmet. Its checkbox is still unticked in the PR body — deliberately. Denying a BRD approval replays the success turn; the walkthrough approves in practice, so it ships as a known limitation rather than going back to Define. It is documented for presenters on the technical/demo-environment page, not just here.
  2. The DoD smoke pass was never performed by an agent — on either Verify pass. The riskiest change on the branch (every environment that had FAST_BRD=true now rendering the chat) has no automated coverage; what stands behind it is code review, the unit suite, a green typecheck and build, and the operator's own walkthrough of the preview before ticking. If the BRD chat misbehaves in a former-FAST_BRD environment, this is the gap it came through.

Carried into merge and after

  • Delete FAST_BRD from Vercel. With the globalEnv entry gone turbo strips it and it is inert, but it stays in the dashboard reading as live config. Note the rollback interaction: reverting this PR restores the code and the globalEnv entry, but only restores the old preview behaviour if the Vercel variable still exists.
  • The AI Gateway is out of credits (Verify finding 10) — billing, not this branch: main fails the same click. The live half of the canned/live switch cannot be demonstrated until it is topped up; the canned half can.
  • migrate-preview is a vacuous green (finding 11) — the GitHub preview environment has no MONGODB_URI, so the job takes its exit 0 skip path and reports success having migrated nothing. Not a passing gate.
  • Referred out, not this run's: the demo seeder's dropped per-lead activity log belongs to demo-seed-storyline (#795); skipCannedBrd's legacy role shim is a permission-cutover intake item; the reset affordance is demo-reset-ops.

Context budget: within band — the Inputs table plus the two docs pages the change touches.