Replaces the Codex CLI subprocess with a Python LangChain deep agent
(`examples/showcases/reskinnable-demo/agent/`) reached over AG-UI: sandboxed
shell execution via deepagents' `LocalShellBackend`, parallel per-merchant
research subagents, no external harness binary, and our own API key.
Registered as `banking-expenses` in the server agent registry. That required
widening `AgentRegistration.createAgent` from `() => BuiltInAgent` to
`() => AbstractAgent` — the runtime's own `agents` option is
`Record<string, AbstractAgent>` and the v2 tree contains no
`instanceof BuiltInAgent` branch, so the demo's type was stricter than the
runtime it described.
Measured end to end against the real runtime: 58 `execute` calls, 7 concurrent
`task` subagents (peak concurrency 4/4 on a dedicated probe), RUN_FINISHED,
eight charges filed with ids read out of real 201 bodies, and 8221 replayable
AG-UI events persisted on the thread after the client disconnected 8s in.
Correctness fixes found by running it rather than reading it:
- `submit_expense_report` DERIVES the totals from the verdicts instead of
accepting them as arguments. A measured run had every per-row amount matching
the CSV while the headline totals came back $1.00 and $0.20 high — the model
authored them. The report card prints the total directly above the rows it is
the sum of, so tiles and rows must not be able to disagree.
- `amount` is coerced at the tool boundary; it arrived as a string on all 14
rows, which silently defeats the widget's currency formatting.
- `merchantKind` values that are non-answers ("unclear") are dropped rather than
rendered beside the merchant name as if they were findings.
- The recursion limit is set on the AGENT, not via `graph.with_config(...)`,
which the AG-UI adapter drops — the run completed the whole analysis and then
died at LangGraph's default of 25 supersteps.
- A `clone()` override works around `copilotkit` 0.1.95 being incompatible with
`ag-ui-langgraph` >= 0.0.42, whose base `clone()` (called per request) passes
kwargs the SDK subclass does not accept. Every request 500s without it.
Deletes the superseded Node-side harness scaffolding: `prompt.ts` (the task
specification now lives in the Python agent's system prompt), `workspace.ts`
(the sandbox owns the workspace) and their tests. `types.ts` stays — it is the
shared vocabulary the report card and the tool renderer both import.
Also drops the three unused `@tanstack/*` dependencies.
Reskin-skill impact: checked. `.claude/skills/reskin/` documents the `Skin`
contract, and no field of it changed — `banking-expenses` is NOT a skin and must
never look like one. It is absent from `registry.ts`, `skinIds`,
`skinIdentities` and `LINTED_SKIN_IDS`, and has no route, theme or identity. The
one documented thing that did change is `AgentRegistration.createAgent`'s type,
which SKILL.md describes in its registration section; that is updated in the
follow-up commit that makes banking itself a deep agent.
Gates: lint 0, typecheck 0, test:unit 2458 passed across 217 files, build 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRubZT6AS6LCGkcE2KzcfA
Three names for one value were live in CopilotKit's own documentation, and
following the wrong one with a CLI-provisioned project yields an undefined
key:
- `INTELLIGENCE_API_KEY` — what `copilotkit project select` writes, used by
all 34 integration examples and the docs site.
- `COPILOTKIT_INTELLIGENCE_API_KEY` — the seven Channels package READMEs and
the packaged skills. Nothing ever read it.
- `COPILOTKIT_API_KEY` — the Slack and Teams examples, and the TSDoc on
`CopilotKitIntelligence` itself, which is what an IDE shows on hover.
`INTELLIGENCE_API_KEY` wins, because it is the name the CLI provisions and
changing it would break every scaffolded project in the wild.
`COPILOTKIT_INTELLIGENCE_API_KEY` is retired outright — no code read it.
`COPILOTKIT_API_KEY` stays readable as a deprecated alias in the two
examples that consume it, so an existing `.env` keeps working, and is
documented as deprecated everywhere it appears.
The skills reference also documented `organizationId`, sourced from a fourth
and fifth env name, as a `CopilotKitIntelligence` option. It is not one:
`CopilotKitIntelligenceConfig` has no such field, so the copy-pasteable
sample it appeared in would not compile. Removed from the samples, and the
prose that told readers to fetch a value for it corrected.
The Intelligence wiring itself was published only inside
`node_modules/@copilotkit/runtime/skills/`, and the only docs pages showing
`CopilotKitIntelligence` were the two Channels frontends — so a developer on
the plain web path had no page to reach it from. Adds
`/premium/connect-your-runtime`, which covers the wiring, how to confirm the
credential is actually consumed, and the self-hosted two-URL rule.
`scripts/validate-intelligence-env-names.ts` keeps this from drifting back.
It runs unfiltered in CI on purpose: the two workflows that would otherwise
cover it filter paths, and static/quality ignores `examples/**` — exactly
where the deprecated alias lives.
Checked the reskin skill: no impact. This adds one banking-only REST route and
one store adder; it touches no Skin contract field, no shell file, no lint rule
and no gate the skill names.
Beat 1 is the demo's first move, and it was answering "how do my trips look?"
with a trip wall. It now answers "How often do I fly?" with a picture: every
trip on the account laid out on a day scale, a today divider, the disrupted
ones called out, and the average gap between trips.
WHY A STRIP AND NOT BARS. The account holds seven trips across about ten weeks.
Monthly bars collapse that to three columns, hide which trips are disrupted, and
read as a stub on a projector. The strip uses all seven, and the GAPS are the
actual answer to "how often" -- which is why the summary quotes the average gap
rather than a count.
MEASURED against the shipped seed and the app's own clock, pinned in
data/flight-cadence.test.ts:
7 markers - 0 flown - 7 ahead - 2 disrupted - average gap 11 days
Note the clock. This app runs on a FIXED demo clock (`store.ts` publishes
`now: SEED_NOW`, 2026-07-14), not the wall clock, so every seeded trip is AHEAD
and the strip is forward-looking. "About every 11 days" is therefore the honest
answer, and it is a better one than any count of flights behind us.
Structure:
- `data/flight-cadence.ts` -- pure, no React, no Date. Takes `now` as an
argument and reads days out of the ISO string by civil-day arithmetic.
Both rules are load-bearing here: a `Date.now()` would put the divider in
one place on the server and another in the browser (the hydration class
this branch already chased once), and `new Date(iso)` on a string carrying
an airport's UTC offset re-expresses a 23:00 Lima departure as the next
day. `components/local-clock.ts` makes the same argument for display; this
is its data-side counterpart.
- `components/flight-cadence-chart.tsx` -- paints only. Receives `position`
already normalised to 0..1, so there is no date maths in a component where
nothing could unit-test it.
- `showFlightCadence` registered with `useComponent`, NOT `useFrontendTool`:
only a component replays out of thread history, which is what beat 2 asks
the audience to reload and see.
Three details worth keeping:
- Only flights someone HOLDS a booking on are drawn. The ledger's `flights`
also carries the rebooking candidates, and counting offers would inflate
the answer to the question being asked.
- An unreadable departure is DROPPED and counted, never placed at day 0. A
marker at the wrong point asserts a cadence that is false while still
looking like data.
- The helper takes a structural `{ id, flightId }` rather than `Booking`, so
it accepts the client's `BookingDto` without a cast -- and therefore cannot
see `waiverGround`, beat 6's sixth leak channel.
Tests: 12 on the helper (including the offset case, the drop-don't-relocate
case, and the seed figures), 7 on the component (every marker by flight number,
the cancelled trip named in WORDS and not only as a coloured dot, summary and
picture derived from one object), and `beat-1.test.ts` pinning the contract --
pill wording, registration via useComponent rather than useFrontendTool, the
prompt naming the tool and demanding prose alongside the chart, and no `Date`
in either new file.
Also uses airline's existing amber/negative tones from `trip-list.tsx` rather
than inventing a `warn` design token -- there isn't one; the vocabulary is
brand / positive / negative.
Gates: lint clean, tsc 0 errors, 214 files / 2448 tests, build exit 0.
--no-verify for the reason recorded in 6473cdcf9d.
`tsc --noEmit` is the only command in this tree that type-checks the 211 test
files -- `next build` visits only what the app's module graph reaches, and
vitest does not type-check at all. The docs already said so and told readers to
run `pnpm exec tsc --noEmit`; this makes it a script, so the command people are
told to run is one word and shows up in `package.json` beside the others.
Note this is a NEW convention here, not a missing piece being restored: no
package in this monorepo defines a typecheck script, so build-time checking is
the house norm and test files fall outside it everywhere, not just in this app.
This closes the DISCOVERABILITY half of that gap for this app only.
It does NOT make the check enforced. Nothing runs it unless a person or an
agent chooses to. Wiring it into CI is a repo-wide decision with real CI cost
across 45 packages and is deliberately not taken here.
Earned: a slot reported three green gates (lint, test:unit, build) and still
shipped a TS2352 in a test file, because none of those three look at test
files.
8 doc references updated from `pnpm exec tsc --noEmit` to `pnpm typecheck`
across README.md, CLAUDE.md, SKILL.md and demo-beats.md. Verified the script
runs clean under the new name.
--no-verify for the reason recorded in 6473cdcf9d: the pre-commit hook fails on
a pre-existing @copilotkit/vue timeout unrelated to this app.
Reported from the running demo: clicking "Oldest pending requests" often got a
prose reply --
Confirm the levers and I'll take you there: **pending** only, sorted by
**oldest first**, top **10**.
-- and nothing else. No tool call, no confirm card, no navigation. Beat 3c
failing while looking like it worked: the answer is correct and well formatted,
and "that was a maneuver, not a link" goes unproven.
ROOT CAUSE, and why the model was not disobeying. It was obeying a sentence
that reads two ways. `showRequestQueue`'s description said "Confirm the levers
with them first" without saying WHERE that happens. The HITL card IS the
confirmation -- it lists the levers and waits -- but nothing said so, so
confirming in chat satisfied the instruction as written. Two other things left
it with no reason to prefer the tool:
- `people/agent.ts` never mentioned `showRequestQueue`, or navigation at all.
Nothing connected "show me the oldest requests" to a tool call.
- `top` was `.optional()`, and an optional lever invites the model to go and
ask for the missing value first.
`logistics` hit this and was fixed; `people` never was, because nothing pinned
the fix. This applies logistics' shape:
- the description now says the card confirms, and says not to confirm in prose;
- the prompt gains MOVE THEM, DON'T DESCRIBE THE MOVE, naming the tool and the
"in front of ... rather than describe one" framing;
- every lever is REQUIRED, with 0 as the "no limit" sentinel. That needs no
page change: the render sets the `top` query param only `if (args?.top)`,
which is falsy at 0, so the page applies no limit.
`beat-3c.test.ts` pins all three. It is source-level on purpose -- what went
wrong is what the MODEL was told, which lives in `description` and the prompt,
and nothing else in this app checks either. Mutation-verified: reverting `top`
to `.optional()` turns it red.
NOT changed: commerce. Its `top` is `.int().positive().optional()` with a stated
reason -- omitting it is exactly what its `parseTopLever` honours -- so that is a
different, documented design rather than the same defect. Its prompt already
names its nav tool.
Reskin skill impact: YES, fixed here. demo-beats.md ss 3c now records the
two-readings failure, the quoted prose it produces, both halves of the close
(description AND prompt), and the note that commerce's optional `top` is
deliberate so nobody copies the wrong shape.
Gates: lint clean, 211 files / 2420 tests passing. Committed with --no-verify
for the reason recorded in 6473cdcf9d: the repo's pre-commit hook fails on a
pre-existing @copilotkit/vue timeout unrelated to this app.
<!--
Thank you for sending the PR! We appreciate you spending the time to
work on these changes.
Help us understand your motivation by explaining why you decided to make
this change.
**Please PLEASE reach out to us first before starting any significant
work on new or existing features.**
By the time you've gotten here, you're looking at creating a pull
request so hopefully we're not too late.
We love community contributions! That said, we want to make sure we're
all on the same page before you start.
Investing a lot of time and effort just to find out it doesn't align
with the upstream project feels awful, and we don't want that to happen.
It also helps to make sure the work you're planning isn't already in
progress.
As described in our contributing guide, please file an issue first:
https://github.com/ag-ui-protocol/ag-ui/issues
Or, reach out to us on Discord: https://discord.com/invite/6dffbvGU3D
You can learn more about contributing to copilotkit here:
https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md
Happy contributing!
-->
## What does this PR do?
Fixes three `examples/v1/*` README files whose "Clone the repository"
step `cd`s into a directory that no longer exists (leftover from when
examples were reorganized under `examples/v1/`). Following the README as
written fails at the first step with `cd: no such file or directory`.
- `examples/v1/chat-with-your-data/README.md`: `cd
CopilotKit/examples/copilot-chat-with-your-data` → `cd
CopilotKit/examples/v1/chat-with-your-data`
- `examples/v1/form-filling/README.md`: `cd
CopilotKit/examples/copilot-form-filling` → `cd
CopilotKit/examples/v1/form-filling`
- `examples/v1/state-machine/README.md`: `cd
CopilotKit/examples/copilot-state-machine` → `cd
CopilotKit/examples/v1/state-machine`
This matches the already-correct format in
`examples/v1/travel/README.md`.
Docs-only change, no code/behavior affected.
## Related PRs and Issues
- N/A
## Checklist
- [X] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [ ] If the PR changes or adds functionality, I have updated the
relevant documentation
- [X] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)
A 13.9k-line new file trips the fork-PR supply-chain heuristic
(security_fork-pr-alert flags any added file over 5000 lines), and the
job cannot post its explanation because fork tokens are read-only.
Several showcases ship no lockfile; this one is not a pnpm workspace
member, so nothing depends on it.
grok-4.6 runs xAI's X Search server-side, then composes the answer out of
real React components through five CopilotKit frontend tools. Every post
rendered is a real post the model found.
Registers next.config.ts in the build-config allowlist and adds the row to
the examples index.
The merge of main brought a seventh skin. These are the surviving count claims
outside the conflicted files, all of which the seventh skin falsified:
- `airline` was described as "the one PASSENGER-FACING skin"; `bookstore` is
also customer-facing, so it now names the pair.
- demo-beats.md still told a skin author "every registered skin is
demo-complete, so there is no partial precedent to copy". Bookstore IS a
partial precedent, deliberately, so the sentence now says so.
- Eight in-skin comments said "all six skins" while describing something that
is true of the WHOLE roster (the shared PDF primitive's coverage, the dark
treatment, and — load-bearing — the project-scope warning in three
seed-memories.ts files, where undercounting understates the blast radius of
a project-scoped sweep). All now say "every skin", which cannot rot.
Reskin-skill staleness check (CLAUDE.md standing rule): yes, demo-beats.md is
part of the skill and is corrected here.
Verified from examples/showcases/reskinnable-demo: `pnpm lint` clean,
`pnpm exec tsc --noEmit` 0 errors, and the roster/config drift guards plus the
touched skin tests pass (110 tests).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings in `bookstore` and 31 other commits from main.
WHY THIS MERGE CONFLICTED IN SEVEN FILES. Both sides hand-maintained the
same roster. This branch had just rewritten the docs around four
conclusions that were true when written:
- `useData` has zero implementors
- no in-memory skin remains
- every registered skin is demo-complete
- there are six skins
`bookstore` falsifies all four: it sets `useData: useBookstoreData`, so the
optional hook has a live implementor and an in-memory skin exists again; it
ships intelligence/{seed,forget}-memories.ts but no teach loop, so it is not
demo-complete; and it is the seventh.
Neither side was wrong. The resolution is the union, and where a list or a
count was load-bearing it is now the command that derives it -- which is the
convention this branch adopted precisely because two hand-maintained copies
of one roster is what produced these conflicts.
The de-narration this branch applied is preserved: main's older phrasings
carried retrospective prose that was deliberately removed, and it has not
been reintroduced.
Registration verified rather than assumed -- bookstore is present in
LINTED_SKIN_IDS, skinIds, skinIdentities, SkinRegistry and agentRegistry.
That last one has no drift guard at all, so a missing key there fails only
when someone sends a chat message.
Gates on the merged tree: lint clean, `tsc --noEmit` 0 errors, 210 test files
/ 2414 tests passing (up from 197/2227 -- bookstore's own, nothing dropped),
build exit 0.
COMMITTED WITH --no-verify, DELIBERATELY, WITH THE USER'S APPROVAL.
The pre-commit hook was bypassed. That is normally forbidden here, so the
reason is recorded rather than left to be guessed:
- This branch's ENTIRE diff against main is inside
examples/showcases/reskinnable-demo. `git diff --name-only origin/main...HEAD`
lists nothing outside it.
- The hook fails on `@copilotkit/vue` -> CopilotThreadsDrawer.ssr.test.ts,
"does not eagerly evaluate the Lit element module when the package entry is
imported". That test fails STANDALONE on this machine
(`npx nx test @copilotkit/vue` -> 1 failed | 1073 passed, exit 1), with no
merge in progress and nothing of ours involved. It asserts a lazy-import
property but enforces it with a 5000ms wall-clock timeout, so it fails
whenever module resolution is slow rather than when Lit is actually
eagerly evaluated.
- This is simply the first commit on the branch to touch packages/*, so it is
the first to make `nx affected` run that suite. Ninety earlier commits
touched only the demo app and never triggered it.
What WAS verified on the merged tree, by hand, before committing:
pnpm lint clean
pnpm exec tsc --noEmit 0 errors
pnpm test:unit 210 files / 2414 tests passing
pnpm build exit 0
npx nx test @copilotkit/runtime 138 files passing
That last one only passes because of a second pre-existing breakage fixed
along the way: packages/runtime's better-sqlite3 binary was compiled against
NODE_MODULE_VERSION 137 (Node 24) while .nvmrc pins Node 22 (127), so every
SqliteAgentRunner test threw on load. `pnpm rebuild -r better-sqlite3` fixed
it. That fix is environmental and is not part of this commit.
Two follow-ups worth someone's time, neither blocking:
1. The vue SSR test should assert the property (module not evaluated) rather
than time the import.
2. Nothing in the repo pins the Node version for native rebuilds, so a
contributor who once ran a task under Node 24 silently poisons
better-sqlite3 for every later Node 22 run.
Second pass of the history sweep. The first cleaned CLAUDE.md and README.md;
this finishes the reskin skill and the in-code comments that still recounted
who hit a defect, when it was found, and how long it survived.
Every rule, gate, command and checklist item is kept. What went is the
narration around them — "it named only the first four skins for two releases",
"caught by `eslint --print-config`, by hand, once", "drifted out of true three
review rounds running", "it shipped that way once", "one CR pass found sixteen
of them live", "measured in logistics", "each raised after the fact". Where a
cut would have left a rule reading as arbitrary, the mechanism is restated in
one present-tense clause instead: a hand-copied list rots silently and nothing
fails when it is stale; flat-config `rules` are REPLACED, not merged, so a
block silently drops every selector it does not restate; a schema leak is
routinely line-wrapped, so a source-text guard never matches.
Two stale cross-references fixed while in there: failure-modes.md quoted a
CLAUDE.md sentence that the first pass removed, and claimed the roster-docs
test header lists "two" known instances outside its doc set (it lists one).
Skill impact, per the standing rule in CLAUDE.md: this change IS the skill, and
it is prose-only — no contract field, link builder, lint rule, gate, beat
mechanism, skin identity or file path changed, so no template or verification
step needed a matching edit. The two doc properties `skin-roster-docs.test.ts`
depends on were preserved deliberately: templates.md keeps "the six shipped
skins" ahead of its brace glob, and SKILL.md keeps its "Six are registered —"
id list, since both are what arm the brace-glob and valid-id-list rules.
Gates: `pnpm lint`, `pnpm exec tsc --noEmit`, `pnpm test:unit`
(197 files / 2227 tests) and `pnpm build` all green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same cutting rule, applied only where a comment narrated what a past slot or
agent did rather than explaining the code: "a later slot owns that file", "as of
the beat-parity work", "two parallel agents each hand-edited this paragraph",
"this very paragraph did it once", "learned the hard way in the banking skin".
Every WHY stays; several are restated as present-tense properties (do not
reintroduce a client ticker, do not add a second seed of AV1423).
Four of these were also FACTUALLY STALE and are now correct:
- airline/attach-hotel-confirmation.ts claimed no pill carries
HOTEL_CONFIRMATION_MESSAGE; suggestions.ts has carried it since beat 3d landed.
- keel/attach-bulletin.ts said the same of BULLETIN_MESSAGE.
- keel/tools-replay-safety.test.ts said keel was not yet in the
statusKeyedTerminalRender glob; it is.
- airline/data/{store,trip-types,types}.ts described use-data.ts / useAirlineData
as "still live and still driving the trip, loyalty and disruption pages"; the
hook is deleted and the ledger is the only substrate.
skin-roster-docs.test.ts: comments only. No fixture entry, exemption or rule was
touched — the "legitimate phrasings" list still pins the numeral+adjective
discriminator, and the header still documents both false-positive shapes.
Reskin skill impact: checked — no rule, path or symbol the skill references
changed, so no skill edit is required beyond the prose pass in 085c92e.
Same rule as the previous commit, applied to SKILL.md, demo-beats.md,
failure-modes.md and templates.md: keep the rule and the mechanism that makes it
a rule, drop who hit it, when, how it was found and how long it survived.
Largest removals: the "there is no longer a partial skin to warn you off"
retrospective closing demo-beats.md, the CR-pass provenance header on
failure-modes.md, "this paragraph has now been wrong twice" under the pill
count, the three-copies-of-the-staging-chain incident report, and the
count-of-selectors paragraph that recorded its own rot. Past-tense incident
illustrations were restated in the present tense rather than deleted, so every
worked example still names its file.
Two stale claims fixed while passing through: templates.md § tools.tsx said only
banking, people and commerce key renders off `result` (every skin does), and
SKILL.md described `--nw-nav-inset-*` as recently retired rather than simply
absent.
Reskin skill impact: this IS the skill; the app docs move in the commit before
this one and the two are consistent.
Record the current state and the forward-looking instruction; drop the
retrospective narration around it. Removed: the "MIGRATION not a split"
substrate-history block, the three worked examples of past skill staleness under
the standing rule, "was the FIRST skin"/"the second skin built"/"the retrofits"
framing, "it did rot for two releases", the glass-engine replacement note, and
the TS2352-in-a-green-slot anecdote.
Every rule, gate, command, derivation and mechanism is kept; where a cut would
have left a rule reading as arbitrary the reason is restated in the present
tense (a nested thread rail compounds the assistant's floor; a client ticker is
a second clock; a template teaching a removed pattern still compiles).
Reskin skill impact: checked — the skill is edited in the following commit for
the same reason, so the two stay in step.
Bring the app's own documentation back in line with what the skin now does,
and record in the authoring skill the failure that a live run exposed.
CLAUDE.md had drifted in five separate places, each phrased differently
enough that keyword searches kept missing one: the seed catalog size, the
count of SKIPPED beat-map rows (twice), the beat-matrix cell for
stored-procedure replay, the intro paragraph's list of skipped beats, and a
claim that the seed file seeds "no procedure at all". An exhaustive audit of
every bookstore claim in the file — 36 of them — is what finally closed it.
Also documents a repo-level trap in § Commands: pnpm lint is ESLint only,
while lefthook's pre-commit additionally runs oxlint --fix and oxfmt --write
over staged files and re-stages the result. The two disagree (oxlint enforces
prefer-top-level type imports; ESLint does not), so a contributor can satisfy
the documented gate and still be silently rewritten at commit time. This
already misled a reviewer into filing a finding asking for the exact thing
the hook auto-reverts.
The reskin skill gains an empty-recall requirement for the stored-procedure
beat, in both templates.md and demo-beats.md, which previously documented
only the happy path. A skin author following either would ship the gap this
skin shipped: with the memory empty, the agent reported the miss correctly
and then offered to learn the procedure — beat 6's moment, arriving as an
improvised fallback. Both now require saying so and stopping, with no
guessing and no teach-offer, and cite the worked example.
Note the reference skin has the same gap: banking's beat-5 clause has no
empty-recall branch, and its only such instruction affirmatively calls
offerWorkflowRecording. Correctly scoped to its teach path, but it leaves a
pattern pointing the wrong way for beat 5. Left for a separate change.
The demo half of beat 5: a procedure the agent already knows, an instruction
to recall rather than improvise it, and a pill so the presenter never types.
- intelligence/seed-memories.ts: a kind "operational", scope "user" memory
naming addToCart -> swapEdition -> applyPromoCode -> setDeliveryBy in
order, and explicitly excluding the three distractors. The procedure is
SEEDED, not taught — it is recalled. Scope is "user" and never "project",
which would return the memory for every user of a shared instance.
- agent.ts: clause 7 calls recall_memory FIRST, runs all four steps in order
without confirmation, and states that finding the club is not running the
procedure — reporting the pick, code or date and stopping is the failure
mode, not a partial success. It scopes openCheckout out, so the run ends
with a filled but unpaid cart, and refuses the teach-offer: this is a
recall, not a teaching moment.
- The empty-recall branch exists because a live run went off-script the
moment the store was empty. With nothing recalled the model said so
correctly and then offered to LEARN the procedure — which the clause
already forbade, and which is beat 6's moment. It now says plainly that
nothing was found and stops, without guessing the pick, edition, code or
date from the catalog or cart: an invented answer that looks right is worse
than an honest failure, because on stage the two are indistinguishable.
The four tool names are frozen string literals shared by the prompt and the
seed, and no test reads either, so renaming one breaks the beat with a green
suite. A drift guard is the next commit's concern, not this one's.
Price the cart through the three-argument cartTotals and show what the club
run actually did to it.
The discount is rendered as up to TWO rows, not one. discountCents is a
single scalar, so a single row labelled with the club would render
club-plus-credit under the club's name and silently misattribute the credit —
and that combined case is reachable exactly when the applyStoreCredit
distractor misfires, the most scrutinised second of the demo. splitCartDiscount
recomputes the club-only discount and takes the credit as the remainder, so
both parts are attributed honestly and clubPart + creditPart === discountCents
holds for every case, including a credit that exceeds the subtotal (the club
keeps its full percentage; credit takes only the applied remainder).
Also adds the delivery-by badge, wishlist and reminder counts, and the same
figures on the page readable so "what's on my screen" agrees with what the
agent says. card_last4 remains the only card datum that leaves the checkout.
Everything the saved book-club procedure needs in order to run: the club
constant and its computed next-meeting date, the edition pair the swap moves
between, discount-aware pricing, the six store writes, and the twelve
registered frontend tools.
- data/club.ts: BOOKSTORE_CLUB (pick, promo code, 15%, meeting weekday),
nextMeetingDate/nextMeetingISO (UTC-only by design) and localCalendarDay,
which re-anchors the caller's LOCAL calendar day onto UTC midnight. Without
it a presenter west of UTC demoing on a Thursday evening gets next
Thursday: at 2026-12-31T23:00-08:00 the naive path skips a full week.
- data/seed.ts: a 25th book, the club pick's paperback, sharing workId
"trust" with the hardcover so swapEdition has a real work to move within.
- data/query.ts: cartTotals gains an optional pricing object and returns
subtotalCents/discountCents alongside totalCents, which stays the
POST-discount amount charged. Inputs are sanitised so
0 <= discountCents <= subtotalCents holds for any input, including a
non-finite credit or discountPercent.
- data/use-data.ts: promoCode, deliverBy, storeCreditCents, wishlist and
reminders persist under one extras key with a field-by-field validator;
six writes returning WriteResult; placeOrder prices through cartTotals and
consumes all three sticky fields. swapEdition merges into an existing
target line rather than duplicating a bookId, and setDeliveryBy's
past-check reads the local calendar day so the club's own date is never
refused.
- tools.tsx: the club readable (the only agent-reachable source of the promo
code), the three procedure writes, the three distractors that genuinely
work, and discount-aware pricing in both the cart readable and
openCheckout's render so the total the agent speaks matches the cart page,
the checkout form and the order record.
Every registration uses [] deps and reaches the store through dataRef:
useFrontendTool keys its effect on JSON.stringify(deps), so a callback in a
dep array stringifies to a constant and pins the pre-hydration store.
Reskin skill: checked, no impact — skin-internal data, store and tool
wiring; no Skin contract field, registration, routing or gate changed.
Answers the standing question in CLAUDE.md — this work found the skill wrong, so
the fixes ship with it.
Rule 1 on tool deps told authors to 'pass the data each closure reads' without
noting that useFrontendTool keys its effect on JSON.stringify(deps). A Map, a Set
or a function stringifies to a constant, so the registration is inert and the
closure never refreshes — the skill's own words for the bug it warns about
described the fix it recommended.
The useData template taught a bare useState(SEED) and said nothing about a
storage-mirrored variant, so an author needing one writes a hydration effect and
trips react-hooks/set-state-in-effect immediately.
Roster prose across CLAUDE.md, README.md, .env.example and the skill now covers
seven skins. Most count claims were rephrased without a numeral rather than
renumbered, so the next skin cannot re-falsify them — skin-roster-docs.test.ts
is what caught them, and its roster fixtures are updated to match.
resolvePage uses a Map, never a plain object: segments[0] is untrusted URL
input, and an object lookup walks the prototype chain, so /bookstore/constructor
would resolve a Function where a ComponentType is declared and crash React
instead of 404ing. skin.test.tsx pins that with the prototype-chain keys.
An unknown book slug resolves the detail page and renders a not-found body
rather than 404ing — the agent hands out these links, and 404ing a renamed book
would break a deep link.
Registration is four files, not two: both registries plus skins-config (whose
test asserts skinIds and skinIdentities match the live registry, and which
LOCK_SKIN is validated against) and eslint.config.mjs, where the id joins
LINTED_SKIN_IDS — the array the URL-contract selectors interpolate, so without
it lint is blind to this skin.
The prompt is where the beats are enforced: recall memory before recommending
and name the recalled preference in the note, never ask for or repeat card
digits, never emit a markdown table where a gen-UI component exists.
No temperature is set. gpt-5.4 rejects the parameter and logs that it is
unsupported on every run, so pinning it alongside a comment claiming
determinism would assert a guarantee the model discards.
Tool registrations read live store data through a ref and close with empty deps
where a dep cannot re-register them: useFrontendTool keys its effect on
JSON.stringify(deps), so a Map or a function stringifies to a constant and the
closure keeps its first values forever. openCheckout additionally must not
re-register mid-call — placeOrder mutates the cart, and a teardown would lose
respond() and fail the thread.
Every render keys off the recorded result rather than status: a reopened thread
replays with a stored result and no status transition, so a status-keyed render
looks correct live and blanks on reload.
The route readable in the layout plus one readable per page is what makes the
screen-awareness beat work: asking on two pages must give two different correct
answers. All four payloads are deliberately disjoint.
The active segment comes from useSkinSegments, not a pathname slice — the shell
hook strips a leading skin id rather than a fixed offset, so it stays correct
under a LOCK_SKIN deploy where the segment is absent entirely.
The presenter reset is a full-page assign, not a router.push: it clears storage
with removeItem, bypassing the store, and the store has no storage listener, so
only a document load re-reads it. A client navigation would leave the cart
visibly full right after a successful reset.
The cart page has no checkout button by design — checkout is the agent's beat.
Covers are typographic and generated rather than sourced images: 24 scans would
be a licensing problem, would not reskin with the theme, and would read as stock
photography in a demo whose argument is that the UI belongs to the product.
checkout-card carries the security boundary — onSubmit receives only the last
four digits, the other digits are cleared from state at that boundary, and both
sensitive inputs are type=password because this card appears on a projector. Its
receipt mode re-derives from a replayed result so a reopened thread shows a
receipt rather than a blank form.
filter-bar keeps the ebook lever even though no seed book has that format: the
agent can set format=ebook via browseWithFilters, and a missing lever would make
an agent-applied filter invisible, which is the one thing the component exists
to prevent.
Reads through useSyncExternalStore rather than useState plus a hydration
effect: the effect form fails react-hooks/set-state-in-effect, and a useState
lazy initialiser that reads storage makes the server and client markup
disagree. layout-preferences.tsx is the shell's sanctioned pattern.
getSnapshot caches the parsed value and getServerSnapshot returns a frozen
module-level constant — cart and orders are arrays, and a fresh array per call
fails Object.is and infinite-loops during hydration.
The storage mirror exists for the durable-thread beat: its proof is a hard
reload, and a useState-only cart empties at exactly that moment.
Memory is scoped per shopper, so the same suggestion pill answers differently
for Maya (one seeded taste preference) and Guest (none). That contrast is the
demo's headline claim, so identifyUser never derives a scope from userRole —
both shoppers share the role and a role-derived scope would merge them.
forgetAllMemories deliberately skips scope:'project' rows: project scope is
global to the Intelligence backend instance rather than partitioned per
product, all skins share one instance locally, and banking seeds a
project-scoped procedure memory a bookstore reset must not destroy.
The reset route maps raw shopper ids through resolveBookstoreUserId before
clearing. Passing the raw ids would clear scopes nothing writes to and no-op
while reporting success.
Theme values are space-separated HSL channels, not hex: globals.css wraps every
token in hsl(), so a hex value yields invalid CSS and the whole skin silently
falls back to the gray :root defaults.
href.ts and nav-target.ts route every URL through useSkinHref. A hardcoded
/bookstore/... path puts the tenant segment back in the address bar on a
LOCK_SKIN deploy, and concatenating onto the builder's base emits the
protocol-relative //book/x because that base is '/' under a lock.
The seed test encodes the demo's falsifiability rule: the literary and
translated shelves must carry hardcovers and over-$20 titles, or the
recalled 'paperback only, under $20' preference has no visible effect.
cartTotals returns { itemCount, totalCents } — no tax and no shipping, so a
separate subtotal would duplicate the total and could drift.