mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
codex/cloudplot-showcase-migration
249 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a5f775c2db | chore(examples): remove obsolete inspector configuration | ||
|
|
d7d774fc9b | Auto-merged main into codex/ent-1157-shared-clerk-session on deployment. | ||
|
|
f3f32aafff |
fix(showcase): tighten the offsite-expenses beat
The "Sort out my offsite expenses" beat had three problems on stage: the harness console was a black slab in a light-mode transcript, the tool activity grew a stack of finished steps that pushed the report card off the screen, and the run took a full minute. Console (banking skin): - Every colour is now a semantic token, so the pane follows the app into dark or light instead of being hardcoded dark. It was `bg-ink` with `text-white/45`-style overlays, which only ever looked right in one mode. - Collapsed by default. The status strip still carries the live state, so the run reads as alive while closed. - Still the FULL log when open. It is the detail view, and windowing it as well left two lines and nowhere to read the rest. Tool activity (shell, all skins): - Rolls to the last two lines; older ones are REMOVED, not collapsed. - Registration is a layout effect, and that is load-bearing. A new line renders before it is registered, and registering is what evicts the oldest, so with a passive effect the browser painted the in-between state: three rows for one frame on every tool call, and again when the end-of-run MESSAGES_SNAPSHOT remounts every line at once. Measured per animation frame over a full run: 12,734 frames, never more than two. - A shared registry rather than something simpler because CopilotKit renders one component per tool call and owns the container. CSS `:nth-last-child` needs siblings and the lines had one parent each; mount-order counters drift across the snapshot remount. Agent (run time 1m 0s -> ~45s): - Research is gated on the offsite dates. A charge dated outside the window is settled by its date whatever the merchant turns out to be, so half the researcher dispatches were buying nothing. Travel on the adjacent days is still kept in scope. - Filings go out in one command instead of one curl per row. The researchers already ran concurrently, so the serial per-row round-trips through the model were most of the wall clock, not the research. - Fetch and verify are one command; there is nothing to decide between the halves. - Analyst reasoning effort defaults to medium, overridable with BANKING_EXPENSE_EFFORT. Filing is idempotent, which the batching made necessary: the script got run twice and every charge was filed twice, so the report card claimed six filings while the ledger held twelve. The script now writes `filed.json` and exits early if it exists. That marker is cleared once per run, because the workspace is a fixed directory shared by every run and a stale marker would convince the next demo it had already filed and post nothing at all — the same bug wearing the opposite mask, and a quieter one, since a run that files nothing still writes a confident report. Deduping server-side on merchant+amount would have been wrong: Hotel Verrano legitimately appears twice at the identical 318.55 for the two nights of the offsite. Does this change make anything in .claude/skills/reskin/ wrong, incomplete, or misleading? No. Nothing there documents the harness console, the tool-activity renderer, or the analyst prompt; its only matches for "console" are `console.error` in unrelated template code. Verified on the real path against a live Intelligence stack: six rows filed, all status=pending, stable across 60s of polling, and present in the Pending Approval queue with their notes and approve/decline actions. |
||
|
|
6623dcfd4f |
fix(reskinnable-demo): make the app its own pnpm root
`pnpm install` in this directory did not install this app. Leaving the root workspace's member list was only half of it: pnpm walks UP from the cwd for a workspace root, found the repo's, and installed THAT — measured in CI (run 32398378642), "Scope: all 70 workspace projects", 4645 packages resolved for the monorepo, and this app left with no node_modules. The next command then failed as `sh: 1: eslint: not found`, which reads as a broken toolchain rather than an install that went elsewhere. A `pnpm-workspace.yaml` here stops the walk. `ignore-workspace=true` in an `.npmrc` does not — it is CLI-only in pnpm 10.10 (measured: the Scope line was unchanged), so the alternative would have been a flag every human and job had to remember. The five canary `overrides` move into that file, their supported home. They stay duplicated in `package.json`'s `pnpm` field for now, which is read only because this app pins `packageManager: pnpm@10.10.0` — three of the five (`@ag-ui/core`, `@ag-ui/encoder`, `@ag-ui/proto`) are pinned nowhere else, and a released `@ag-ui/core` rejects SUBAGENT_* events in the HTTP transport, so a packageManager bump would have silently killed the harness console. Verified read-only: `pnpm install --frozen-lockfile --lockfile-only` in this directory resolves the single project, satisfies the committed lockfile, and leaves it byte-identical. |
||
|
|
cf9de905ac |
fix(reskinnable-demo): pin and lock the agent's Python canaries
A colleague cloning this branch could not reproduce the demo. The JS half
is pinned exactly — this app left the root pnpm workspace and ships its
own `pnpm-lock.yaml` with `@ag-ui/client 0.0.59-canary.1786716392.0` — but
the Python half pinned nothing: `ag-ui-langgraph>=0.0.43` and no
`uv.lock`, so a fresh `uv sync` resolved the RELEASE.
Measured, on the release that `>=0.0.43` actually selects:
ag-ui-langgraph==0.0.43
emit_subagent_events accepted by LangGraphAgent.__init__: False
subagent symbols in ag_ui.core: NONE
That is this branch's headline feature — the streaming CLI console — and
it would have failed SILENTLY. `main.py` sets `emit_subagent_events` as an
attribute (copilotkit's subclass takes only four kwargs), so on a release
without the feature the assignment succeeds, lands on an object nobody
reads, and the service starts clean. No `subagentRunId` reaches the
browser, the console cannot separate the harness's work from the parent's,
and a reopened thread collapses a multi-minute run to one tool message.
Every gate stays green.
So `ag-ui-langgraph` and `ag-ui-protocol` are now `==` pins, and
`agent/uv.lock` is committed. `ag-ui-protocol` is pinned as a DIRECT
dependency although nothing imports it by name: it carries the SUBAGENT_*
event types and the adapter asks only for `>=0.1.15`, so left transitive
it resolves the release and undoes the other pin.
README: the quick start said `pnpm install # from the repo root — this is a
workspace package`. It is not one — it is absent from
`pnpm-workspace.yaml`, deliberately, so the canary line cannot leak into
the rest of the monorepo. A root install therefore installs nothing for
this app, which is a confusing first five minutes for anyone who reads it
and follows it.
Verified by cold start rather than by inspection: copied ONLY the files
git tracks (the five .py modules, pyproject.toml, the new uv.lock) into an
empty directory, ran `uv sync --frozen`, and got
ag-ui-langgraph 0.0.43.dev1786716392 with `emit_subagent_events accepted:
True` and the five SUBAGENT symbols present. Then booted that venv on a
spare port and replayed the browser's captured Q2-with-PDF payload
through it: RUN_FINISHED, with createReport carrying the invoice's real
line items. The live stack was not touched.
Reskin skill: checked, no impact. Its install/verify step is `pnpm dev`
inside this app, which is correct either way; the root-vs-here distinction
is a README concern and the skill never mentions the workspace.
|
||
|
|
ffbb01be08 |
fix(reskinnable-demo): stop banking's agent in stop-demo.sh
The teardown mirror of the previous commit. `./stop-demo.sh` stopped the
dev server, the docker stack and the native TEI — never :8124 — so
banking's Python agent survived every teardown.
That leftover is not merely litter. `run-demo.sh` health-checks :8124
before starting (so a re-run reuses a live agent instead of colliding on
the port), which means the next cold start silently ADOPTS the orphan and
serves whatever code it was launched with. Edit `agent/`, re-run the
script, observe no change, conclude the edit did nothing.
No `--keep-agent` flag to match `--keep-tei`: TEI has that flag because it
is slow to warm, and the agent boots in seconds, so keeping it would only
reintroduce the failure above.
Also corrects two things in the same breath:
* The Ctrl-C claim I got backwards one commit ago. MEASURED this time,
with the same shell construct the script uses: SIGINT reaches the
foreground process group, which the backgrounded children are still in,
but a NON-INTERACTIVE shell sets background jobs to ignore SIGINT
(POSIX) — so only the exec'd dev server dies (exit=-2) and the stack,
TEI and agent all survive. `nohup` is not what saves them; that covers
SIGHUP, a different signal. Both scripts and the README now say this.
* `ok "docker stack down${PURGE:+ (volumes removed)}"` printed "(volumes
removed)" on EVERY teardown, because the flag holds the string "0" when
unset and `:+` expands on non-empty. The action was always right
(`--volumes` is gated on `-eq 1`) — verified: the postgres/redis/minio
volumes are still there after a flagless run — but the line told anyone
reading it that their seeded data had just been deleted.
Verified with a full cycle through both scripts: `./stop-demo.sh
--keep-tei` reported the agent stopped and left the volumes in place, then
`./run-demo.sh` came back with `banking agent ready (200)` and `stack
healthy`. The idempotency guard was exercised against the live service and
reports "already up" rather than starting a second uvicorn.
Reskin skill: checked, no impact. It documents authoring a skin, not
running the stack; its one launcher line got the note it needed in the
previous commit.
|
||
|
|
b96a9449de |
fix(reskinnable-demo): start banking's agent from run-demo.sh
`./run-demo.sh` brought up the embedder, the Intelligence stack and the dev server, then handed over an app whose DEFAULT skin could not answer a single message. Banking's agent is a Python service (`agent/`, :8124) and it is not a compose service, so it had to be launched by hand — and nothing said so: no line in the script, no line in the README, no line in any markdown in this tree. The failure mode is the expensive kind. Nothing errors at startup: the stack comes up healthy, the app boots, the dashboard renders off the REST ledger, every pill is present. Only sending a message fails, and the six in-process skins keep working, so the obvious reading is "my machine is fine, the demo is broken". The script now starts the agent between the compose wait and `pnpm dev`, guarded on `/health` so a re-run reuses a live one instead of colliding on the port, and dies with `(cd agent && uv sync)` when the venv is missing — the same shape as the native-TEI branch above it. Also corrects the Ctrl-C line, which claimed Ctrl-C "stops only the dev server". Measured: the docker stack survives, and the dev server, the native TEI and now the agent all go down with the script. README: the quick start said `pnpm dev` and described OSS mode as needing only `OPENAI_API_KEY`. True for six skins, false for the default one. It now starts the agent too and says why the whole agent lives out of process. Reskin skill: updated, one sentence in the Verification list's step 2. A skin author runs `pnpm dev`, gets redirected from `/` to banking, sends a test message to see if anything works, and gets silence — then debugs their own registration. The skill now points them at `/<their-id>` or `./run-demo.sh`. Verified by doing it: stopped the dev server and the agent, re-ran the script, and it reported `banking agent ready (200)` and `stack healthy` without touching the warm TEI. Then walked beat 3d in the browser — the Q2 pill filed a report citing "the Meridian Creative Agency invoice from page 1", i.e. the model read the attached PDF. |
||
|
|
11838db020 |
fix(reskinnable-demo): carry an attached PDF to the model as a file block
Beat 3d was dead on banking: clicking the Q2 pill staged the invoice,
sent the message, and then nothing happened at all — no report, no error,
no failed message in the transcript.
`ag_ui_langgraph` hands every attachment to the model as an `image_url`
block, documents included, so the PDF was rejected before the first
token:
openai.BadRequestError: 400 - Invalid MIME type. Only image types are
supported. (code: invalid_image_format)
The exception is raised inside the model node, which kills the SSE
stream. The runtime sees `RUN_ERROR: terminated` with no cause and the
browser renders nothing. Worse, the crashed run is still checkpointed, so
every LATER message on that thread replays the rejected content and dies
the same way: one click on the pill killed the whole conversation, and
only restarting this service cleared it (`MemorySaver` is in-process).
`_repair_document_attachments` rewrites those blocks into LangChain
standard `file` blocks before the model call. It walks every message, not
just the newest, because the offending content comes back from the
checkpoint on later turns.
STOPGAP, not a design. The real fix is open upstream as
ag-ui-protocol/ag-ui#2476 (both adapters, plus the return leg so a
non-image attachment survives MESSAGES_SNAPSHOT). This service installs
the adapter from PyPI, so that fix cannot reach this venv until it is
published; the middleware's header says when to delete it.
No test: `agent/` has no python test runner, and standing one up for code
whose deletion is already scheduled is the wrong trade. The durable
tests — PDF, audio, video, filename, round-trip, legacy binary — ship
with the upstream PR instead. Verified here by replaying the browser's
real captured run payload against the service: 400 before, RUN_FINISHED
with `createReport` carrying the invoice's line items after.
Reskin skill: checked, no impact. Its beat-3d guidance is entirely the
CLIENT half (staging into the composer, the `AttachmentFailureCause`
union, do not copy `@/shell/attach`), and it names no model-side
conversion. A skin authored from the skill gets a `BuiltInAgent`, whose
converter already maps documents onto file parts — banking is the only
skin whose agent is a LangGraph service, so this failure is unreachable
from the skill's path.
Gates: lint clean, typecheck clean, 2460 unit tests pass. `pnpm build`
deliberately not run — the diff touches no TypeScript, and `next build`
would clobber the `.next` state of the dev server currently serving the
demo.
|
||
|
|
0163beab8e |
feat(reskinnable-demo): stream the harness into a CLI console via AG-UI subagents
Makes the offsite-expenses beat legible while it runs, and gives it its own
model, by taking the AG-UI subagent surface from the canary line.
## The expense analyst is now a real subagent
banking gpt-5.4, temp 0
│ banking's prompt; browser frontend tools; Intelligence memory tools
└─ expense-analyst gpt-5.6-sol, reasoning_effort=high
│ sandboxed shell, submit_expense_report
└─ merchant-researcher gpt-5.4, one per merchant, Tavily
Previously the beat was a section of banking's prompt, which left nowhere to put
per-beat configuration: model, effort and recursion limit are all agent-level and
there was one agent. It also meant banking's ~21,000-character rulebook rode
every one of the ~20 model calls the run makes, re-sending rules about markdown
tables while the agent read a CSV.
Reached as a `CompiledSubAgent` because a raw `SubAgent` spec has no `subagents`
field and this one needs its own — the per-merchant fan-out is a headline of the
beat, and a flat subagent could only research serially. Verified nesting
survives: a probe showed the analyst's `task` dispatches, the researchers'
`search_merchant` calls and the final report tool all reaching `astream_events`.
`gpt-5.6-sol` additionally needs `use_responses_api=True`: with function tools
and `reasoning_effort` it 400s on /v1/chat/completions. The first probe missed
that by asking the model a question with NO tools bound — a model probe for an
agent has to bind one.
## The console: one CLI window, streaming
`shell/subagents/subagent-activity.tsx` subscribes to the agent's event stream
and folds it into console lines. Reading `agent.messages` (the previous design)
was wrong twice over: messages materialise at the `MESSAGES_SNAPSHOT`, two per
run, so the pane sat still for minutes and then filled at once; and persisted
messages carry no `subagentRunId`, so the harness's narration could not be told
from banking's own reply.
The fold is pure and idempotent — every line keyed by the id of the thing that
produced it — so the same code serves the live subscription and a replay of the
thread's stored events when a conversation is reopened.
Three heuristics are deleted, each replaced by identity the protocol already
carried:
- the console's "first tool call" anchor -> the run's first `task` call, from
MESSAGE order (durable; the
event-derived version rendered
one console per delegation on a
restored thread — six, measured)
- `CONSOLE_TOOL_NAMES` suppression list -> `subagentRunId` presence
- `disable_streaming` on the researchers -> the canary's per-lane state
`shell/subagents/subagent-message-filter.tsx` keeps subagent narration out of the
conversation. It suppresses the PROSE and keeps the TOOL CALLS: an agent
routinely narrates and calls a tool in one message, and returning null for the
whole message hid the REPORT CARD — the run looked perfect and ended with nothing
to show. Shell-level and inert for a skin whose agent has no subagents.
## Canary stack, contained to this app
The subagent surface only exists on the canary line, and a released
`@ag-ui/client` <= 0.0.57 rejects `SUBAGENT_*` events in the HTTP transport
before any middleware runs, killing the stream. So the demo leaves the root pnpm
workspace and ships its own lockfile, pinning `@copilotkit/* 1.68.3-canary` and
`@ag-ui/* 0.0.59-canary` locally instead of imposing an unreleased protocol on
every package in the monorepo.
A 1.62.2-based canary was tried first and could not compile the app: it silently
rewound the CopilotKit API five minors under a demo written against 1.67.1, and
`OpenGenerativeUIActivityRenderer` (a public `/v2` export since ~1.63) was the
first thing to break. `workspace:*` is not a version, so the app had no recorded
lower bound on the API it needs.
KNOWN GAP, deliberately not fixed here: Nx discovers projects THROUGH the pnpm
workspace (there is no `workspaceLayout` in `nx.json`), so leaving it also
removes the demo from the repo-wide `nx run-many -t build` and `-t check-types`
sweeps. Verified — `nx show project deep-agents` and the other standalone
showcases return "Could not find project". No workflow names this demo, so it is
currently unbuilt and untype-checked in CI and needs its own job. Run the four
gates locally until that lands. Documented in `pnpm-workspace.yaml`.
## Fixes
- The run clock is keyed per run and read through an injected `ToolRuntime`
instead of taking the oldest open stamp across the process. Model calls AFTER
the report re-stamped the clock and that leftover became the next run's start:
a two-minute run reported 333s. Now 86s reported against 98s wall clock — the
gap is thread-naming and delegation, before the analyst's first model call,
which is what the tile claims to measure.
- `merchantKind` non-answers are rejected on the leading token, on hedging
language, and over 40 characters. With no search tool the model wrote a bare
"unclear"; with Tavily live it hedges in prose ("unknown (likely
bookbindery/bookshop retail, but not established for this exact merchant)"),
which an exact-match filter passed into a 60-character label glued to the
merchant name.
- `vitest` no longer externalises `@copilotkit/*`. Installing them from npm moved
`src/app/layout.tsx`'s stylesheet import under `node_modules/.pnpm/`, where
Node's ESM loader threw `Unknown file extension ".css"` and took out 16 suites
while naming a stylesheet nobody had touched.
- `agent/main.py` reads the demo's `.env` as well as its own, so `TAVILY_API_KEY`
works wherever an operator puts it. Two env files to keep in sync is a trap
whose failure mode is "the agent ignores a key that is plainly sitting in .env".
## Upstream finding (reported separately, not fixed here)
`@copilotkit/runtime` drops `subagentRunId` when persisting messages: 2888 of
3026 stream events carry it, 0 of 53 persisted messages do. Reproduced with
Intelligence removed entirely, so it is the runtime's message shape rather than
the platform store — and `@copilotkit/runtime`'s dist contains no occurrence of
the field at all, while `@ag-ui/core`, `ag-ui-protocol` and `@copilotkit/core`
all model it. One field threaded through would let the console rebuild from
message history and delete the event-replay seeding added here.
`CLAUDE.md`'s appended block is generated by `next dev`
(`next/dist/server/lib/generate-agent-files.js`) and committed per its own
instruction to keep the tree clean.
Gates: lint 0, typecheck 0, test:unit 2460 passed across 216 files, build 0.
Measured end to end in Intelligence mode: 14 rows, 9 merchants researched, 6
charges filed with ids read out of real 201 bodies, totals reconciling against
their own rows, 3220 events with 6 SUBAGENT_STARTED/FINISHED pairs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRubZT6AS6LCGkcE2KzcfA
|
||
|
|
a29bf0720a |
feat(reskinnable-demo): add the offsite-expenses pill and harden the report fields
THE PILL. The offsite-expenses beat had no entry point in the UI. The agent was
wired, the service was up, all four gates were green, and every verification I
ran POSTed directly at the runtime — so the beat was fully working and completely
unreachable for anyone opening the app. It is the ninth and last pill, after the
AWS charge, because it is the only one that does not answer in seconds.
`suggestions.test.ts` now pins it. That guard is not decoration: this pill has no
type referencing it, no matcher and no route, so nothing else in the tree
notices if it is dropped or retitled. It checks REACHABILITY, which is the
property nothing else was holding.
MERCHANT KIND HARDENING. `merchantKind` is a KIND ("hotel", "pharmacy") that the
report card prints inline beside the merchant name, where it has room for about
two words. The previous filter rejected exact matches of "unclear"/"unknown",
which was enough when there was no search tool and the model wrote a bare
"unclear". With Tavily live it hedges in prose instead — measured:
"unknown (likely wellness-related business)" and "unknown (likely
bookbindery/bookshop retail, but not established for this exact merchant)". Both
sailed through and would have rendered as a 60-character label glued to the
merchant name.
Now rejects on the leading token, on hedging language anywhere, and on anything
over 40 characters. The length cap is the backstop: it encodes the actual
constraint rather than a list of phrasings already observed, so it catches the
next wording nobody predicted. A measured run after the change leaked none, with
a longest surviving kind of 23 chars.
ONE ENV FILE. `agent/main.py` now loads the demo's `.env` in addition to its own,
with `agent/.env` winning on conflict. The app and this service need the same
keys, and asking an operator to keep two files in sync is a trap whose failure
mode is "the agent ignores a key that is plainly sitting in .env" — which is
exactly what happened when TAVILY_API_KEY was added to the demo's `.env` and the
service, reading only its own, never saw it.
`.env.example` documents TAVILY_API_KEY and BANKING_AGENT_URL, including what a
keyless run actually looks like: the research subagents are told plainly that no
search happened and instructed to report "could not establish" rather than
guess, so the run completes with several rows marked `unclear` and a "merchants
researched" tile reading 0. Nothing errors, which is why it needs writing down.
Measured with Tavily live: 14 rows, 10 merchants researched, 10 parallel
subagents, 31s, totals reconciling against their own rows, 7 charges filed with
ids read out of real 201 bodies, zero non-answer kinds. Research also CHANGES a
decision rather than only labelling one — The Copper Room resolved to
`restaurant_lounge` and moved from `unclear` to `expensable`.
Gates: lint 0, typecheck 0, test:unit 2460 passed across 216 files, build 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRubZT6AS6LCGkcE2KzcfA
|
||
|
|
e4eb96f8d7 |
feat(reskinnable-demo): make banking's agent the Python deep agent
Banking's `agent.ts` stops returning a `BuiltInAgent` and returns an `HttpAgent` pointed at the Python deep agent instead. Six skins still run in-process; banking is the one that does not. WHY THE WHOLE AGENT, not just the offsite-expenses beat. Threads are scoped per agent — `listThreads` takes `agentId` as a REQUIRED parameter, and measured against the running app the two lists are disjoint (banking 46 threads, banking-expenses 10). The v2 runtime has no handoff mechanism, and `defineTool`'s `execute` takes `(args)` with no emitter, so a tool cannot stream a multi-minute run. Under a second agent id a presenter could start the analysis, switch threads, and have nothing to come back to. One conversation list means one agent. The expense task spec is appended to banking's own prompt as a CONDITIONAL job rather than a second identity — it opens by saying it applies only when the user hands over a statement, and to ignore it otherwise. Ported to Python: - `agent/prompt.py` — banking's system prompt, extracted from the TS template literal programmatically and asserted equal, not paraphrased. 21,208 chars carrying 56 distinct rules; a dropped rule silently breaks a beat that still looks like it works. - `agent/report.py` — `render_report` and the a2ui operations builder. Component order, ids and the `columns` arithmetic match the TS builder; the unique surface suffix uses uuid rather than a timestamp so two reports in the same millisecond cannot collide. Carried over from the TS `BuiltInAgent` because each had a reason written beside it: the non-mini model (the teach-and-recall arc routes unreliably on mini) and `temperature: 0` (tool routing must be deterministic). Also sets `emit_raw_events = False`. It defaults to True, which piggybacks LangChain's internal events onto the AG-UI stream: a measured run streamed 27,950,261 bytes, and the same run with it off streamed 374,086 — a 75x reduction with an identical report. This matters more than it looks because the thread PERSISTS those events for replay, and leaving a running thread and coming back to it is the point of this beat. Nothing downstream reads RAW. Verified against the running app, not reasoned about: - `/info` reports banking as HttpAgent and the other six as BuiltInAgent. - Canvas beat: `render_report` -> TOOL_CALL_RESULT -> the a2ui middleware emits `activityType: a2ui-surface` with createSurface + updateComponents. - Frontend-tool beat: given showTransactions + showPendingApprovals the agent picks showTransactions, emits no result (the client executes it) and emits no prose — the prompt's "the rendered list is the single source of truth" rule surviving the port. - Intelligence mode: the run is handed the browser's tool AND recall_memory / save_memory / forget_memory / copilotkit_knowledge_base_shell. - Durability: a run whose client disconnected at 8s finished unattended and left a replayable thread. Removes `expenses-agent.ts` and the non-skin `banking-expenses` registry key; banking's own id now serves both. `AgentRegistration.createAgent`'s comment is rewritten — it described a second remote key that no longer exists — and now gives a grep to derive the BuiltInAgent/HttpAgent split rather than asserting it. Gates: lint 0, typecheck 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRubZT6AS6LCGkcE2KzcfA |
||
|
|
a6418bcc09 |
feat(reskinnable-demo): run the offsite-expenses beat on a LangChain deep agent
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
|
||
|
|
ec4439c8d9 | Merge origin/main into codex/ent-1157-shared-clerk-session | ||
|
|
b94e4bfb5d |
feat(reskinnable-demo): POST transactions so the harness can file charges
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. |
||
|
|
ccfcc7e68a | fix(reskinnable-demo): validate harness summary shape and correct the filing contract | ||
|
|
14b0ee2bff | test(reskinnable-demo): guard the OFFSITE-to-fixture invariant | ||
|
|
f3b29d36fa | feat(reskinnable-demo): harness prompt and scratch workspace | ||
|
|
d0a3706a65 | feat(reskinnable-demo): harness types + offsite expense fixture | ||
|
|
3bf6e30e9a |
feat(reskinnable-demo): open Aeronova's demo on a flight-cadence chart
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
|
||
|
|
e3d9c911a1 |
chore(reskinnable-demo): add a typecheck script and point the docs at it
`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
|
||
|
|
b7c144d94a |
fix(reskinnable-demo): make Rowan's queue pill move the user, not describe the move
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
|
||
|
|
784f2e7529 |
docs(reskinnable-demo): retire the last "all six skins" claims after bookstore
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>
|
||
|
|
6473cdcf9d |
feat(reskinnable-demo): merge main, and reconcile the docs with a seventh skin
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.
|
||
|
|
fb2aedb0e0 |
docs(reskinnable-demo): de-narrate the reskin skill and its guard comments
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> |
||
|
|
6740613fb8 |
docs(reskinnable-demo): de-narrate in-code comments that recounted build history
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
|
||
|
|
085c92e6cc |
docs(reskinnable-demo): cut historical prose from the reskin skill
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. |
||
|
|
050814b433 |
docs(reskinnable-demo): cut historical prose from CLAUDE.md and README
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. |
||
|
|
2ab26d66d5 |
docs: beat-5 coverage and reskin skill
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. |
||
|
|
79cbc7ef84 |
feat(bookstore): seeded procedure, prompt and pill
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. |
||
|
|
f1913bf485 |
feat(bookstore): cart discount and delivery UI
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. |
||
|
|
4ee549a616 |
feat(bookstore): the book club mechanism
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. |
||
|
|
1c4003d2a3 | fix(bookstore): seed the default memory bucket and stop claiming per-shopper isolation | ||
|
|
80727a47f7 |
docs(reskinnable-demo): document the bookstore skin and correct the reskin skill
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. |
||
|
|
3bde5f4443 |
feat(bookstore): assemble the Skin and register it across the shell
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. |
||
|
|
02fdf443ea |
feat(bookstore): the agent prompt, its six tools, catalog and demo pills
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. |
||
|
|
6855ad9cca |
feat(bookstore): layout chrome and the browse, book and cart pages
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. |
||
|
|
7a4f8dde79 |
feat(bookstore): generated covers, cards and the in-chat surfaces
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. |
||
|
|
22c8492922 |
feat(bookstore): cart and orders store with a per-shopper storage mirror
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. |
||
|
|
f9b649c59e |
feat(bookstore): per-shopper Intelligence identity, seeded memory and presenter reset
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. |
||
|
|
860063ee48 |
feat(bookstore): brand identity, theme tokens, nav and lock-safe link builders
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. |
||
|
|
e3c6b75293 |
feat(bookstore): data types, 24-book seed and pure query functions
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.
|
||
|
|
fbaa645bad |
docs(reskinnable-demo): finish the airline and keel beat-map cleanup
Follow-up to the previous commit, which fixed the two beat-maps' headers and their flagged risks but left the body still speaking in the future tense about work that has landed. Comment/doc only. src/skins/airline/data/beat-map.md - § "It is ADDITIVE" claimed `use-data.ts` (`useAirlineData`) "is untouched and still drives the trip, loyalty and disruption pages". Both are DELETED; every component reads `useAirlineLedger()` through `components/concierge-view.ts`. - Risk #5, "Two substrates, one passenger — they must not disagree on stage", told a later slot to "migrate BOTH readings" and not to touch either seed's AV1423 without the other. Marked RESOLVED, with the answer it actually got: the duplicate reading was DELETED rather than kept in sync, because a hand-synced pair of seeds was never going to survive a reseed and is a pair of figures that can contradict each other on a projector with nothing checking. - The beat table's four "(later slot)" cells, the beat-4 seed note, and the route list's "memory re-seed: later slot" — all shipped. src/skins/keel/data/beat-map.md - Beat 4's "the seeded memory (a later slot writes the file)" — written, and the scope it was written at (`user`, never `project`) named, since that is the choice the next author most needs and the one CLAUDE.md and demo-beats.md now flag. Skill impact: none beyond the previous commit. These two files are per-skin design records, not part of `.claude/skills/reskin/`, and the generalisable lessons in them (the two-substrates seed, the second clock) were already lifted into demo-beats.md § "Seeding memories" and SKILL.md § step 3 in that commit. Verified: pnpm lint, pnpm exec tsc --noEmit, pnpm test:unit (197 files / 2227 tests), pnpm build — all green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
acac2b7f27 |
docs(reskinnable-demo): correct the in-code comments the beat-parity wave slots could not reach
Comment-only, no behaviour change. Each of these sat OUTSIDE the boundary of the slot whose work falsified it, so each described the tree as it was mid-migration. All verified against the current tree before editing. src/shell/skin-contract.ts - `RuntimeProviders`: "airline needs neither, so omits it". Airline omits `RuntimeProviders` and DOES supply `useRuntimeProperties` — the two are SEPARABLE, and airline is now the worked example: one account holder, no switcher, so its hook reads no context and returns a frozen module constant. Do not mount an empty provider for symmetry. - `useData`: "Omit when a skin has no shell-managed data (banking …)". Every skin omits it now; documented with the grep, plus WHY the field is kept (the shape is legitimate, it just has no worked example left). - `CanvasSurface`: the "omit if the skin has no report canvas" branch is currently unexercised — every shipped skin has one. src/shell/agent-registry.ts - The logistics entry said it ships "neither `intelligence/seed-memories.ts` nor `intelligence/forget-memories.ts`" and is "identity plumbing only — do NOT read it as a durable-memory demo". It ships both (`ls src/skins/*/intelligence/`), and its `dev/reset` sweeps and re-seeds through them. Corrected, with the same properties-forwarding caveat the other five entries carry, and a note that "expensive half built, cheap half skipped" was its state for two releases. src/skins/keel/data/types.ts - The `THE REST SUBSTRATE` banner said the two substrates are "deliberately not merged yet", that `useKeelData` "holds runs in `useState` and ticks them on a 900 ms interval", and that "the pages still read it through `useSkinData`". All three are false: one substrate, one clock, `useSkinData` returns undefined. Rewritten to name the server-settled read (`settle-runs.ts`, called by both `GET /ledger` and `GET /runs/[runId]`) and to record WHY the deleted client ticker was a defect rather than a design choice — it was a second clock that painted progress the server never heard of, which the next re-read after any write silently rewound. - The `KeelData` interface header claimed to be "the interface every page, component, and tool codes against". It is not referenced by any code at all (`grep -rn KeelData src` returns only comments). Marked HISTORICAL, with a do-not-add-a-consumer note. NOT deleted: it is a doc pass, several comments across the skin describe the migration in terms of this shape, and removing an exported type is a code change for a separate commit. Flagged as a follow-up. src/skins/keel/data/store.ts - "it does not advance them on a timer, because the ticker lives in `useKeelData` on the client. Whichever slot migrates that hook has to decide where the ticker ends up" — decided: the server is the only clock now. src/skins/keel/data/beat-map.md - Header: "Keel today is `useKeelData`, an in-memory `useState` store, and it hits about one beat." Marked BUILT and reframed as the design record. - Risk #2 (the two-substrates/ticker question, correctly called "the biggest single risk in the migration") marked RESOLVED, with the answer (move the clock, do not relocate the ticker) and the generalised lesson. src/skins/airline/data/beat-map.md - Header: the tools/prompt/pages/pills were "later slots". All landed. - § "What this slot did NOT build": every row of the deferral table has shipped. Kept as the retrofit record — which is the most useful thing about it — with a third column saying where each landed, and the three flagged traps marked resolved (including "the reset route says memoryBeats: unarmed on purpose", which was removed in exactly the change that added the seed module, as instructed). src/skins/airline/data/fare-waiver-codes.ts - "⚠️ THE LINT GUARD DOES NOT COVER THIS SKIN YET." It does: both `src/skins/airline/tools.tsx` and `agent.ts` are in `withheldGateVocabulary`'s `files` glob. Also dropped its "COUNT the selectors" instruction (that count has rotted twice) in favour of the resolved-selector table in `skins-config.test.ts`, and spelled out that a green lint still leaves the three prose channels AND `waiverGround` — which matches no `*_CODES` pattern, so the rule cannot see it — as hand-review items. src/skins/airline/tools.test.ts - Header said `statusKeyedTerminalRender` "covers logistics only; airline's glob entry is a later slot's, so until it lands this file is the whole guard" and that `withheldGateVocabulary`'s glob "does not list airline yet either". Both globs list airline now. Also fixed "Three defect classes" over a list of five. src/skins/keel/skin.tsx - "exactly as it does for the four other REST-backed skins" → every skin; nothing sets `useData`. src/proxy.ts - "matching how the other three skins behave" → numeral-free. This was one of the two known-stale instances named in `skin-roster-docs.test.ts`'s header; that header is updated in the app-docs commit, and the remaining one (`e2e/inset-layout.spec.ts`'s hardcoded four-skin loop) is deliberately left — fixing it means adding assertions against skins the spec has never visited, which is a coverage change rather than a prose fix. docs/teach-mode/README.md - It correctly refuses to write the roster into prose, but its verified-by-role paragraph named only banking/commerce/logistics/people and its "so copy commerce or logistics" line named the only two skins with pinned replay behaviour. The `offerWorkflowRecording` grep now returns every registered skin, and `ls src/skins/*/teach-mode-directives.ts` — added as the mechanical discriminator for role #3 — returns four. Also: "logistics and commerce both" skip project-scoped rows in `forget-memories.ts` is now every skin but banking, replaced with the grep that proves it. Does this make anything in .claude/skills/reskin/ stale? No — the reverse. The preceding commit updated the skill for exactly these facts, and these comments were brought into line with it. Checked: `grep -rn "useKeelData\|use-data\|in-memory" .claude/skills/reskin/` names no path or symbol that no longer exists. Verified: pnpm lint, pnpm exec tsc --noEmit, pnpm test:unit (197 files / 2227 tests), pnpm build — all green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a58ea35b19 |
docs(reskinnable-demo): bring the reskin skill up to six demo-complete REST-backed skins
This IS the answer to CLAUDE.md's standing question for the beat-parity work:
"does this change make anything in .claude/skills/reskin/ wrong, incomplete, or
misleading for the next person authoring a skin?" It did, in all four files.
Every claim replaced was checked against the tree first, and replacements state a
derivation rather than a roster wherever one exists.
SKILL.md
- "`banking`, `people`, `commerce` and `logistics` are the four demo-complete
skins ... `airline` is the minimal contract surface" — false. Rewritten to "every
registered skin is demo-complete", so the choice is which one is CLEANEST for a
given problem, and a new "do not model a new skin on the ABSENCE of a field"
paragraph with the grep that derives what each skin actually sets. Airline's
restraint was the single most quoted licence to skip fields in this skill.
- The `useData?` contract row and the "4-2 split ... airline and keel are
in-memory and SET it" paragraph: zero implementors
(`ls src/skins/*/data/use-data.ts` is empty).
- The optional-slots list ("airline omits all of these EXCEPT toolLabels and
useData"): derived per field — the only real omissions in the tree are airline's
`sandboxFunctions` and `RuntimeProviders`.
- "banking, logistics, keel, people and commerce all five ship them, airline
none" (identity plumbing): all six ship `useRuntimeProperties` + `identifyUser`.
Added the point that the three parts are SEPARABLE, with airline as the worked
example of `useRuntimeProperties` without `RuntimeProviders` — do not mount an
empty provider for symmetry.
- "Only banking, people and commerce" register route + page readables: all six do.
- The `statusKeyedTerminalRender` glob comment ("logistics today; keel and airline
still carry the defect") — both are in the glob now; replaced with the grep, so
the sentence cannot rot again.
- The `agentRegistry` snippet handed out `airline: { createAgent }` as the
no-identity example. Rewritten so the identity-bearing form is the default.
- Authoring step 3 and the file tree taught `data/` + `useXData` as the substrate.
Now REST + `ledger-context.tsx`, with a new warning that time-dependent data must
be settled SERVER-side — a client ticker beside a server store is a second clock,
which is the defect keel had to be migrated off.
- § Verification step 1 and the authoring-order footer both claimed `pnpm build`
type-checks the whole app. It does not: `next build` only visits what the module
graph reaches, so it never opens a test file, and Vitest transpiles without
type-checking. Both now name `pnpm exec tsc --noEmit` as the only full
type-check, with four gates in cheapest-first order and the reason it matters
here specifically (several guards this skill asks for are TYPE-ONLY, so they are
decoration until tsc runs).
demo-beats.md
- Beat 3b: "this beat is impossible in airline and keel today" — false; both
register route + page readables. Replaced with the two greps, both of which now
return every skin, so a MISSING entry is the signal.
- Beat 3d: "In-memory skins can fake half of this" — no in-memory skin exists.
- Beat 6: the FIVE-channel leak list is now explicitly non-exhaustive, because
airline has a sixth nobody would look for: `Booking.waiverGround` is a
code-shaped token on a record the ledger publishes, so the LEDGER READABLE is a
channel. The transferable question is "what does my GET /ledger answer with", not
only "what did I put in a prompt".
- § "Seeding memories": the scope rule is rewritten and promoted to its own
flagged subsection. It described banking's `project` scope as the pattern. It is
now the minority and it is a trap for anyone copying a modern skin:
`forget-memories.ts` SKIPS project-scoped rows in every skin but banking
(`grep -ln 'scope !== "project"' src/skins/*/intelligence/forget-memories.ts`),
because project scope is global to the one shared Intelligence instance and
sweeping it would delete a sibling skin's seeds — so in such a skin a
project-scoped learned procedure CANNOT be cleared by the reset, and beat 6 opens
already-taught on the second run of the day, proving nothing while looking
perfect. `user` is documented as the default; banking is documented as
self-consistent the other way (project scope + a sweep that deletes everything)
and therefore not copyable. The beat-6 walkthrough's `scope: project` mention now
says so inline.
- § "Which skin to copy for what": every row re-derived. "the four at 9/9 beats",
"In-memory `useData` substrate → airline, keel" and "Minimal contract surface →
airline" were all false. Added rows for the three genuinely new references
(runtime identity with no context to read; a server-settled clock; an
entitlement-rather-than-authority gate) and for "raising an EXISTING skin", which
is what three of the six now demonstrate.
- The closing "do not use airline or keel as demo-completeness references" warning
is gone. What replaces it is the lesson the retrofits actually taught and which
generalises past any roster: keel shipped the full per-user identity plumbing and
then no seed file, so it got ZERO demo value from the hardest part of what it
built — build the seed file in the same phase as the plumbing.
- Pill-count prose: was wrong twice for the same reason. Now derivation-only.
templates.md
- Checked the `resolvePage` scaffolds first, because the `PAGES[key] ?? null`
object-literal pattern is a live bug (an object literal inherits
Object.prototype, so `PAGES["constructor"]` is truthy, `?? null` never fires,
and the shell's `if (!Page) notFound()` is bypassed — a 500 where a 404 belongs).
Both scaffolds ALREADY use `new Map()` and already explain why, so nothing to
fix; recorded here so the next reader does not re-check.
- `data/use-data.ts` pointed at `src/skins/airline/data/use-data.ts`, a DELETED
file. Rewritten to say plainly that nothing sets `useData`, that this template is
the only reference left, and to give the two reasons both skins migrated (beat
3d's artifact cannot outlive the tab in client state; a client ticker is a second
clock).
- The page and tools scaffolds defaulted to `useSkinData<<Id>Data>()` — i.e. they
taught the one shape no skin uses. Now default to the skin's own ledger context,
with `useSkinData` as the documented alternative.
- The `skin.tsx` scaffold's "airline omits every one below EXCEPT toolLabels +
useData — and airline hits one beat of nine" is false on both halves.
- "Mirror src/skins/airline/agent.ts (minimal)": airline's agent is 247 lines and
none of the six is minimal, because agent.ts is where most beats are enforced.
- The pill-count paragraph.
failure-modes.md — two new classes, in the file's voice
- § 13 "Nothing in this app type-checks a test file unless you run tsc yourself".
This is a lies-shaped defect, not a commands note: a green gate is a claim, and
three of them were green over a live TS2352 this run. It also silently voids a
class of guard this skill asks for, since several of the strongest assertions in
the tree are type-only. Cross-linked from § 7's "what would have to break for
this to go red", one level up.
- § 14 "A lookup keyed by URL input must be a Map, or the 404 branch never fires":
the prototype-chain bug above, why `Record<string, ComponentType>` cannot catch
it (the annotation is a lie about a plain object), and the § 11 class sweep — the
second instance was the operator→identity map in `user-id.ts`, keyed by a
CLIENT-forwarded `properties.userId`, where the same defect hands Intelligence an
`undefined` memory bucket and silently misroutes beats 4/5/6.
- § 10 now says the five-channel list is not exhaustive and documents airline's
`waiverGround` sixth channel plus why no identifier selector can see it, and its
"COUNT the selectors (covered files: four; any other in-skin file: three)"
instruction is replaced — that count had already rotted (`actions.ts` resolves to
two, and the number moved again when `statusKeyedTerminalRender` joined the
block). The check is the resolved-selector table in `skins-config.test.ts`, which
asserts the list BY NAME.
- The header's "everything here came out of one review of commerce" is now
"most of this", with §§ 13-14 attributed to the beat-parity run.
Verified: pnpm lint, pnpm exec tsc --noEmit, pnpm test:unit (197 files / 2227
tests), pnpm build — all green. skin-roster-docs.test.ts passes, including over
this skill's three files in its DOC_SET.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
d7675421a4 |
docs(reskinnable-demo): retire the two-substrate framing; all six skins are REST-backed and demo-complete
CLAUDE.md, README.md and .env.example described the app as it stood two waves
ago. Every claim below was verified stale against the tree before being changed,
and each replacement carries the command that derives it rather than a number or
a name list.
WHAT WAS FALSE, AND THE DERIVATION THAT PROVED IT
- "two different data substrates — banking/logistics/people/commerce REST-backed,
airline and keel in-memory ... to prove the contract is substrate-agnostic".
`ls src/skins/*/data/use-data.ts` returns NOTHING; both in-memory skins now read
REST ledgers (`ls -d src/app/api/*/v1` names six). Rather than delete the app's
central architectural claim, it is restated on the stronger footing the
migration actually earned: the proof is now that TWO SKINS CHANGED SUBSTRATE
with no change to the `Skin` contract and no change to the shell. Stated
honestly that no in-memory skin remains, so that shape has no worked example
left in the tree.
- The `useData` contract row ("the standard mechanism for the two in-memory
skins ... splits exactly along the substrate line"). It has ZERO implementors:
`grep -rn useData src/skins/*/skin.tsx` returns only six comments recording the
omission. Now stated plainly as an optional escape hatch nothing uses — live
rather than vestigial (the shell still runs the hook), with templates.md named
as the only remaining reference.
- The beat matrix. `airline` and `keel` showed ❌ on nine of ten rows and hit all
ten. Every cell updated. The per-skin GEN-UI COUNTS were removed rather than
corrected, and the derivation printed instead — the old command
(`grep -A3 useComponent .../tools.tsx | grep -c name:`) under-reports banking
by one, because banking registers a component in `pages/cards.tsx`; the
replacement counts the whole skin folder. Same for pill counts.
- Per-skin bullets for airline ("in-memory", "the minimal end of the contract",
omits eight optional fields) and keel ("in-memory", `useData: useKeelData`).
Derived per field:
`grep -nE '^\s+(Providers|CanvasSurface|...)[,:]' src/skins/*/skin.tsx`. Airline
omits exactly `sandboxFunctions` and `RuntimeProviders`; keel omits nothing.
Keel's parameterized-routes claim is TRUE and kept.
- "That is five of the six skins; airline is the only one that omits [identifyUser]".
`ls src/skins/*/intelligence/user-id.ts` returns all six, as do
`seed-memories.ts` and `forget-memories.ts`. Replaced with the three `ls`
commands. The "dev/reset is the wider set" note is kept as a WARNING while
recording that the two sets now coincide.
- "there is no `typecheck` script — `pnpm build` type-checks as part of
`next build`". Wrong, and it cost this run real time. `next build` type-checks
only what the app's module graph REACHES, so it never opens any of the 197 test
files; Vitest transpiles without type-checking; `tsconfig.json` DOES include
them. `pnpm exec tsc --noEmit` is the only full type-check in the tree and it
found a live TS2352 in a slot that had reported three green gates. § Commands
now names four gates in cheapest-first order; README's Testing block too.
- .env.example: "The other presenter-ready skins (commerce, logistics, people)".
All six ship `v1/dev/reset`; replaced with the route + button greps, and the
banking-gates-unconditionally difference recorded.
- New: the memory-SCOPE rule. `forget-memories.ts` skips project-scoped rows in
every skin but banking (`grep -ln 'scope !== "project"'`), so a project-scoped
learned procedure survives every presenter reset and beat 6 opens already-taught
on the second run. Banking is self-consistent the other way and documented as
the historical exception.
FIXTURE COUPLING — src/shell/skin-roster-docs.test.ts
Its "legitimate phrasings" list holds strings labelled "verbatim from the current
docs", three of which this commit made false in the docs ("the two in-memory
skins", "the four REST-backed skins", "five of the six skins"). Worth recording
precisely: the list is a SHAPE fixture passed straight to `findStaleCountClaims`,
not a doc mirror — it reads no file — so correcting CLAUDE.md alone would NOT have
turned the guard red. What it would have done is make the header comment a lie
and invite the next author to prune the entries, silently dropping the
discriminator they pin (an adjective between the numeral and "skins" is what
separates a subset claim from a total claim, and nothing else asserts it). So the
entries were moved to the PAST tense rather than deleted, the header now says
they are shapes and must not be pruned to match the docs, and three present-tense
phrasings from this commit were added.
The guard did its job on the way through: it failed on three sentences this
commit introduced ("the two skins" x2, "two of them"), all reworded numeral-free.
The header's "two known stale instances outside the doc set" note is updated —
`src/proxy.ts` is fixed in the following commit; `e2e/inset-layout.spec.ts`
remains, and is left alone deliberately because fixing it is a coverage change,
not a prose fix.
DOES THIS MAKE THE RESKIN SKILL STALE?
Yes, extensively — that is the point of this slot, and it is answered by the two
commits that follow (the skill, then the stale in-code comments). Per CLAUDE.md's
standing rule the skill is updated in the same PR.
Verified: pnpm lint, pnpm exec tsc --noEmit, pnpm test:unit (197 files / 2227
tests), pnpm build — all green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a255d9bcf3 |
chore(reskinnable-demo): put airline and keel under the beat-2 and beat-6 lint guards
Both skins now ship a withheld gate vocabulary and replay-safe terminal
renders, so both belong in the two guards that check those properties. Held
back until now on purpose: a glob covering an unfixed skin turns the tree red
for a whole phase, and a phase that cannot end green is a phase nobody can
bisect.
Verified clean BEFORE widening, not after:
- beat 2 (statusKeyedTerminalRender). Every remaining ToolCallStatus
reference in either skin is a comment or a
`=== ToolCallStatus.Executing && respond` HITL branch -- the interactive
affordance drawn while a response is awaited. Neither has a `.Complete`
terminal render, which is the shape the selector catches.
- beat 6 (withheldGateVocabulary). Each skin contributes exactly its two
agent-facing files. The three human filing FORMS stay OUT: each
legitimately imports its label map, because the operator reads it and the
agent learns the code by watching them choose one. A withheld catalogue
with no form is an unlearnable gate.
skins-config.test.ts's resolved-selector table is updated in the same commit,
which is the point of that table: it asserts, per file, the selector LIST that
`ESLint#calculateConfigForFile` actually resolves. Its existing keel row
expected three selectors and would have failed. Six rows added or changed,
including both filing forms, so the deliberate exclusion is pinned rather than
merely intended.
Also resolves a bug this commit introduced and then fixed, kept because the
comment is the fix: a glob star followed by a slash inside a block comment
CLOSES the comment, and the rest of the file becomes a syntax error. `pnpm lint`
caught it as `Parsing error: ',' expected` five lines below the real cause.
|
||
|
|
00162f5e1b |
feat(reskinnable-demo): give keel memory, a stored procedure and a teach loop
Merges blitz slot keel-teach. Conflict resolution note. Both teach slots hand-edited the same paragraph in agent-registry.ts describing which skins supply identifyUser, and BOTH sides were wrong by the time they merged -- air-teach's said "logistics and keel scope threads only" (keel-teach had just given keel durable memory), and keel-teach's said "skins without it (e.g. airline)" (air-teach had just given airline a resolver). Each was made false by the other's slot, in the same hour. Resolved by deriving instead of picking a side: ls src/skins/*/intelligence/user-id.ts -> all six ls src/skins/*/intelligence/seed-memories.ts -> all six ls src/skins/*/intelligence/forget-memories.ts -> all six All six skins now supply identifyUser AND use it for durable memory, so the generic-identity fallback is unreachable from the registry and is kept only for skins that do not exist yet. The comment now says that and points at the derivation, with the near-miss recorded so the next person does not re-add a hand-maintained list. |
||
|
|
2c698c3724 |
fix(reskinnable-demo): make keel's presenter reset re-arm the memory beats
The route restored the DATA STORE ONLY and said so loudly in its own header, because keel had no seed/forget pair. Now it has one, so the reset does the other half — and, critically, VERIFIES it rather than assuming. - Wipes learned memory in every bucket `memoryScopeUserIds()` reports, ASKED FOR rather than hardcoded. Bellwether shipped a hardcoded list and it could not possibly be right: `playwright.config.ts` pins `INTELLIGENCE_USER_ID`, which collapses the whole set onto one bucket the list did not contain, so the reset scrubbed buckets nothing was reading while a taught procedure survived. - Re-seeds every target bucket, then COMPARES the count against `seedTargets.length * SEED_MEMORIES.length`. `seedMemories` never throws — it counts stored rows and logs the rest — so without the comparison the route would answer `reset: ["store","memory"]` against a backend that had rejected every POST, and the presenter would walk on stage believing beats 4/5 were armed. - `reset: ["store","memory"]` is claimed ONLY when the wipe proved itself complete AND every expected seed landed. Partial and total shortfalls both 502, because a shortfall does not say WHICH memory is missing and the only caller branches on `res.ok`. The interrupted path reports MEASURED counters (`bucketsSwept`, not `forgot` — an empty bucket forgets zero rows, which is the normal state of a second reset in a row). - Every free-text field goes through `redactSecrets`: this route's gate is a demo convenience, not an authorization boundary, and `memoryError`'s cause can quote the backend address verbatim while a 401 body can echo the key it rejected. The address stays in the LOG, which is where a human debugging a reset reads it. - The OSS path (no Intelligence env) still answers `reset: ["store"]` and never the word "memory" — the most misleading string this route could return, because a presenter reading it stops looking for the reason beat 6 opened already taught. The test that pinned the old body is updated to that case rather than deleted. Does this make anything in `.claude/skills/reskin/` wrong? Checked: no. The skill already prescribes the seed-then-verify reset shape; keel was the outlier and is no longer one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e381d58d64 |
feat(reskinnable-demo): give keel one suggestion pill per beat
The pills are the demo's script — the presenter should never have to type — and keel's four were the whole list, covering one beat. Now twelve, in demo order. Eight beat-carrying additions (1, 3a, 3b, 3c, 3d, 4, 5, 6) plus keel's original four, which survive VERBATIM: spec §11's walk-through is scripted against their copy, and each is the only pill reaching its tool — grounded citation, the run plan-preview HITL, the persona-scoped approval queue, the a2ui canvas report. The beat map targeted eight-to-nine, and that figure is incompatible with its own other two instructions once the pills are counted: eight beat pills plus four survivors is twelve, and nine is only reachable by dropping an identity pill or leaving a beat without one. Both were checked against the tools and neither is available. The arithmetic is written out in the file header so the next reader does not "fix" the count by deleting a beat. Two things that would break silently and are now pinned by tests: - The beat-3d pill's message IS `BULLETIN_MESSAGE`, imported. `onSuggestionSelect` keys on that exact string; a retyped sentence takes the default send path, which DROPS attachments — the model then invents the bulletin's contents and files a durable brief that reads perfectly and proves the opposite of the beat. Asserted through the real `onSuggestionSelect` rather than by string comparison, so it fails if either side drifts, and every OTHER pill is asserted to return false. - Beats 3a, 5 and 6 target three DIFFERENT documents (STD-045 endorsed, POL-121 stale, POL-114 gated). Beat 6's unaided replay on POL-208 deliberately has no pill — a scripted sentence would let the room suspect it was rehearsed. Also asserted: no pill says "Knowledge" (the nav label is Register while the segment is still `knowledge`), and no pill names a variance code — a pill's message reaches the model, so it is the prompt's leak channel by another route. Does this make anything in `.claude/skills/reskin/` wrong? Checked: no. The skill's "one pill per beat, in demo order" rule is what this implements; the stale number is in keel's own `data/beat-map.md`, and the reconciliation is documented at the call site rather than by editing that record of the original design. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8b8b08603b |
feat(reskinnable-demo): wire keel's beats 3c, 4, 5 and 6
The tools, the prompt clauses and the mount points. Keel went from one beat to ten.
BEAT 3c — `showRegister`, a HITL maneuver rather than a link. The Register's four
controls existed and nothing drove them. Space x attention x sort x top-N arrive
from the query string through the one shared `data/register-levers.ts`, the confirm
card draws its chips from the SAME normalized record the URL is built from, and the
controls tint. Every lever is REQUIRED with an explicit not-pulled member ("all", or
0): logistics needed a fix commit for exactly this, because a model facing an
optional enum fills it anyway — omission is not a choice it can state — and it put
an empty board on screen under four confidently tinted controls.
BEAT 4 — `showRegisterSummary(note)`, plus a prompt clause forcing `recall_memory`
BEFORE any question about the library's shape and requiring the recalled preference
in `note`. Without the visible why the beat is invisible on stage.
BEAT 5 — `raiseReviewFlag` -> `sendOwnerNotice` -> `addDocumentNote`, all three
`useFrontendTool` and NOT HITL: banking's equivalent once opened a confirmation card
mid-procedure, a presenter moved on, and the next message failed the whole thread
with "Tool result is missing for tool call ...". Their vocabularies are ENUMERATED on
the schemas — the exact opposite of beat 6 — because the claim is that it already
knows the procedure. Nine distractor tools sit alongside, so "it picked the right
three" means something. The prompt adds FINDING IS NOT HANDLING and states that this
is a DIFFERENT procedure from beat 6's with no offer to record.
BEAT 6 — `fileReleaseVariance` plus the HITL chain `offerWorkflowRecording` ->
`awaitDemonstration` -> `saveLearnedProcedure` -> `save_memory`. All five leak
channels are closed: no readable, no `z.enum` (free `z.string()` whose `.describe()`
states the withholding), no code in any description, none in the prompt, and the
route refusals are relayed verbatim without enumerating the catalogue. The prompt's
ACTION DISCIPLINE clause also shuts the two doors a run gate would open — a persona
switch and the e-signature card — because neither can clear a gate about the
REVISION. The replay lands on POL-208 Rev C, a different record from the POL-114
Rev D taught on stage.
Mounts: `KeelProviders` (new) carries the shell's `RecordingProvider` +
`RecordingVignette` BELOW CopilotKitProvider — the only point enclosing both the app
card where the operator demonstrates and the chat card that reads the feed; a
narrower mount makes every `logStep` a silent no-op. The variance filing form goes
on the Register page and is deliberately ABSENT from that page's readable. The
presenter Reset button goes in the header behind the same gate the route enforces.
Does this make anything in `.claude/skills/reskin/` wrong? Checked: no — but it does
leave `eslint.config.mjs` and `src/shell/skins-config.test.ts` needing keel's globs
(withheldGateVocabulary for `keel/{tools.tsx,agent.ts}`, and keel is now free of
`ToolCallStatus` so it can join statusKeyedTerminalRender). Both files are the
orchestrator's; the exact additions are in the slot report and in
`data/variance-codes.ts`'s header. Until they land, keel's own
`tools-replay-safety.test.ts` and `agent.test.ts` are deliberately stronger than the
rule.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|