demo-canned-agentsrun.md03_define/output/spec.mdOnboarding 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.
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.
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.
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.
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.
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.
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.
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.
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.
analyse_source returns
its transcript result without a network fetch or a second model call.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.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.description/requirements a
live approval would write, and no request reaches the AI Gateway.isDemo: false
tenant by any input.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.FAST_BRD removes a dev
shortcut; reinstating an equivalent for real tenants is separate work, not this run.demo-seed-storyline once it lands is a content-only edit to the fixtures, not a
code change.view-as chrome — demo-presenter-switching.demo-reset-ops. Fixtures are deliberately outside the data
that reset touches.04_build/output/notes.mdfeat: 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 BRDpackages/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.ts — BRDAgent 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.ts — model widened from string to the
SDK's LanguageModel, so a model instance can be passed.ai/onboarding/blueprint-tools.ts — analyse_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.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.FAST_BRD removal + skip control)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).LanguageModelV3; nothing in canned/ opens a socket or reads a gateway credential.analyse_source is replayed.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.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.skipCannedBrd writes
description/requirements through leadService.approveBrd, identical to a live approval,
then lands on /workspace/<leadId>. No gateway call anywhere on that path.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.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.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.FAST_BRD tree — they need
re-running over this diff.pnpm test is the factory's (Quality → Run tests).05_verify/output/verify.mdSecond 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".
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.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).The only new attack surface is skipCannedBrd, a server action that writes lead data. It holds:
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.tenantId comes from resolveActionContext → getTenant()
(Clerk org); the Zod schema accepts leadId and nothing else.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.leadId reaches Mongo as an equality filter through the service layer.getCannedBrdDocument, written through the same path a live approval uses and rendered through
the unchanged client-side DOMPurify allowlist. No user input reaches 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.
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
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)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 coversFixed on branch:
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:
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:
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.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:
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.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.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.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.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.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.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.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.mdWho 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.mdQuality 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/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)isDemo,
so no persona capability, journey step or platform-overview claim changesFAST_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..github/workflows/ship-note.yaml), which has not happenedDig 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.
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.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.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.skipCannedBrd writes through leadService.approveBrd, unit-covered by artifacts.test.ts.405fe4b), and the action re-checks the mode server-side.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:
technical/demo-environment page, not just here.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.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.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.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.