Commit Graph

1647 Commits

Author SHA1 Message Date
Maxim 98a6c41f2d docs(reskinnable-demo): revert the reskin skill's agentRegistry-test claim
The skill was edited earlier in this branch to say `grep -rln agentRegistry src
--include='*.test.*'` returns exactly one file, because the removed second harness
arm shipped `harness-slot.test.ts`. That arm is gone, so the grep is empty again
and the warning has to say so — a skin author who trusts "a test guards this map"
would skip the one append with no automated guard.

Caught by the standing question in this app's CLAUDE.md: does the change make
anything in `.claude/skills/reskin/` wrong? Here it did, in the exact silent way
that rule exists for — nothing type-checks a skill file, and a skin built from a
stale one still compiles, lints and renders.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp3i7qBNzWC9xVzsaz5KTZ
2026-08-19 19:13:27 +02:00
Maxim bc4fd1f380 refactor(reskinnable-demo): drop the routed-agent harness arm
The long-running expense harness now ships in ONE shape: a `defineTool` on
banking's classic agent. The second arm — a dedicated agent slot fed by
`BuiltInAgent`'s tanstack stream factory, with its own page at
/banking/deep-work — is removed so this change is reviewable on its own terms.

Deleted: `harness-agent.ts`, `harness-slot.test.ts`, `pages/deep-work.tsx` and
their tests. Unwired: the non-skin `banking-harness` key in `agentRegistry`, the
`deep-work` entry in banking's `PAGES` map, and `HARNESS_AGENT_ID`.

BREAKING for local envs: `EXPENSE_HARNESS_MODE` narrows from
`off|tool|factory|both` to `off|tool`, so `factory` and `both` now THROW. That is
deliberate — silently reading a retired value as `tool` would hide a stale `.env`
instead of surfacing it, and an operator who wrote `both` asked for something this
build no longer has. Verified end to end: with a stale `both` in `.env`, `pnpm
build` fails at `/api/copilotkit/[[...slug]]`, and the same build passes once the
value is corrected. `.env.example` and `mode.test.ts` both name the retired values
so the next reader is not left guessing.

Comments were REWRITTEN, not deleted. Nine files carried prose whose whole point
was the comparison between the two arms; leaving those in place would have
described an architecture that no longer exists. Each now states the surviving
limitation directly — harness progress rides a second transport and never reaches
the thread, so a mid-run reload loses the journey — and, where the alternative
shape is the reason a seam exists (`run.ts`'s split launch, `csv.ts`'s own module,
`progress.ts`'s deletability), names it as the alternative rather than as a live
second arm.

Gates: lint 0, typecheck 0, test:unit 2515 passed across 225 files (was 2531/227
— the two deleted test files), build succeeds with `/banking/deep-work` absent
from the route list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp3i7qBNzWC9xVzsaz5KTZ
2026-08-19 19:12:05 +02:00
Maxim 8ab81ddd4f fix(reskinnable-demo): stop attaching the statement csv — the provider rejects it
Reverts the visible attachment on the harness pill. @ai-sdk/openai accepts ONLY
images and application/pdf as file parts and throws UnsupportedFunctionalityError
on anything else, so a text/csv document part cannot reach the model through this
provider at all. Every run died with

  'file part media type text/csv' functionality not supported

which is why the pill stopped responding: staging, the chip, the filename and the
send were all fine, and the run failed one layer below anything the composer or
the staging chain can see. Neither the composer's accept filter nor the byte
check knows that rule.

Also reverted with it: the shell helper's `kind` machinery and the text/csv entry
in the composer's accept. Both were correct in themselves, but with no caller
they are dead paths pointing at something the provider refuses — and the
follow-up (a PDF rendering of the statement, if we do it) uses kind "pdf" like
every other beat.

KEPT from that commit: the router's multimodal handling. A turn with an
attachment arrives as parts rather than a string whatever the media type, so
that is right regardless — and it is what stops the Q2 beat's shape from ever
silently unrouting Arm C.

skin.test.tsx now pins the harness pill FALLING THROUGH, with the provider rule
written down beside it, so re-attaching a csv fails a test rather than a demo.

Reskin-skill staleness check: demo-beats.md's beat-3d section reverted with the
code, so it no longer documents a `kind` field that does not exist.
2026-08-14 20:14:31 +02:00
Maxim 462eede3f2 fix(reskinnable-demo): file charges at the port the app is actually on
The harness prompt hardcoded localhost:3000 for its filing curl while the CSV
read derived its URL from PORT, so running the demo on any other port left codex
POSTing into a dead socket. It failed quietly in the worst way: the run still
finished, summary.json was still written and the report card still rendered —
only every filedTransactionId was missing, which reads as "the harness chose not
to file" rather than "the harness could not reach the ledger". Filing is one of
the four beats this feature exists to show.

Both URLs now come from one `localBaseUrl()`, so they cannot diverge into
reading a statement from one app and filing charges against another.

prompt.ts had no test — a deferred minor from Task 2, now closed, since the
prompt is the only thing enforcing the beat. Five tests: the port, the shared
origin, the 3000 default, the offsite dates every verdict cites, and the
summary.json fields readSummary parses.

Reskin-skill staleness check: no impact — no contract field, gate, lint rule or
registration path changed.
2026-08-14 20:01:01 +02:00
Maxim 759c1fed7e feat(reskinnable-demo): stage the statement csv on the harness pill
The harness pill said "here's my personal card statement" and attached nothing.
It now rides the real file, through the same shell chain the Q2 invoice uses:
bytes verified before staging, staging confirmed before sending, every failure
reported to console AND alert rather than sending a prompt about a document that
never arrived.

Three things had to give:

- `@/shell/attach` was PDF-only — it sniffed %PDF magic bytes and forced
  application/pdf onto the File. It now takes a `kind` ("pdf" | "csv",
  defaulting to pdf so no existing skin changes), where each kind owns its MIME
  and its byte check. CSV has no magic number, so its check targets what is
  actually being guarded against: NUL bytes (binary mislabelled as text), a
  leading < or { (an HTML or JSON error page), and no delimiter in the first
  line. Mutation-verified — neutering it fails three tests.
- The composer's accept filter dropped .csv silently, so `accept` gains
  text/csv. The four mock composers in tests were updated to match, since they
  exist to mirror the real one.
- Arm C's router would have STOPPED MATCHING. A turn with an attachment arrives
  as multimodal parts, not a string, so a string-only match would have sent the
  expense job to the chat adapter with no harness and no error, while Arm A kept
  working. The router now reads the text parts, and still requires exact
  equality — concatenating a second text part must not turn it into a prefix
  match.

What this does NOT change: `defineTool`'s execute receives only its parsed args,
so Arm A's tool still fetches the same URL server-side. Same file, same bytes,
two readers. Dragging in a DIFFERENT csv would still have the bundled fixture
analysed — that is a different feature and attach-statement.ts says so where
someone would look.

New skin.test.tsx pins which pills are intercepted, following airline's idiom.
The exhaustiveness guard on the failure-cause union caught the new cause exactly
as designed, and the count assertion is what forced a test that actually drives
it.

Reskin-skill staleness check: YES, impact, fixed here. demo-beats.md's beat-3d
template showed a two-field AttachmentDocument as the whole story; it now covers
`kind`, why it must come from what the route serves rather than from the
filename, and the two other places a new kind has to be registered.
2026-08-14 19:55:04 +02:00
Maxim c8f0cb43b0 feat(reskinnable-demo): register arm C's agent slot, page, and route
Adds HARNESS_AGENT_ID (a non-skin key in agentRegistry), the gated registration,
and /banking/deep-work — a second chat in the app card pointed at the routed
factory agent while the assistant column keeps talking to banking's classic one.
Two engines on screen is the comparison.

Two plan corrections, both silent failures if followed literally:

- The plan wrapped the shell's ChatPanel in a nested
  CopilotChatConfigurationProvider with agentId=HARNESS_AGENT_ID, on the correct
  reading that react-core resolves agentId ?? parentConfig?.agentId ?? DEFAULT.
  But ChatPanel passes agentId={skin.id} to CopilotChat EXPLICITLY, and an
  explicit prop beats inherited config — every "harness" turn would have gone to
  banking's classic agent, with the page rendering and the chat answering
  normally. The page renders CopilotChat directly instead, which also avoids
  inheriting the shell's thread rail.
- The plan gated nav and resolvePage on armCEnabled(). skin.tsx is a CLIENT
  module and EXPENSE_HARNESS_MODE is deliberately non-NEXT_PUBLIC_, so that
  expression is inlined as undefined in the browser bundle and reads "off" in
  EVERY mode — the page would 404 even with the arm live. The registry gate is
  the real one; the route is ungated and the page states its requirement.

No nav entry, so the icon rail is identical on every deploy: /banking/deep-work
is reached by URL, which is how Task 12's walkthrough opens it.

The slot itself IS gated, unlike Arm A's unconditionally-registered gen-UI
renderer. A registered agent is not inert the way an unused renderer is, and
without the gate off and factory would be indistinguishable and armCEnabled()
dead code. harness-slot.test.ts pins all four modes — and is the first test in
the tree to import agentRegistry at all.

Reskin-skill staleness check: YES, impact, fixed here. SKILL.md told a new
author that grep -rln agentRegistry src --include='*.test.*' is empty as proof
that this append has no guard. That grep now returns this file, which would read
as "there is a guard" — there is not, for their skin. The paragraph now says
what the one test actually covers.
2026-08-14 19:17:21 +02:00
Maxim 967dcbb72d feat(reskinnable-demo): routed factory agent for arm C
Arm C is a SECOND agent slot in factory mode that routes: the expense pill goes
to the codex harness, every other turn to an ordinary chat adapter carrying
whatever tools the frontend forwarded. Banking's classic agent is untouched —
BuiltInAgentConfiguration is a strict union, so making banking itself a factory
would take its prompt and ~20 tools offline.

Four plan claims did not survive contact with the tree:

- RunAgentInput is NOT re-exported from @copilotkit/runtime/v2 (checked against
  dist/v2/index.d.mts). The router takes a structural type instead, which also
  lets its cases be plain literals with no cast.
- openaiText accepts NO apiKey — its second parameter is
  Omit<OpenAITextConfig, "apiKey">; it reads the env itself and throws per-run,
  inside the factory.
- "gpt-5.4" is not in that adapter's model union at all (gpt-5.4-mini, -nano
  and -image-2 are). Uses gpt-5.6, the same family as the harness's own
  gpt-5.6-sol.
- chat() takes an AbortController, not a signal — the same trap run.ts already
  documents. The factory context hands one over for exactly this.

fetchExpenseCsv moves to harness/csv.ts so both arms read the fixture through
one code path: a second copy with its own port fallback would quietly make the
comparison about the fixture rather than the streaming seam. It also keeps
as-tool.ts deletable if Arm C wins, which its own header promises.

Gate 2's findings are recorded where the code is: nothing here reads tool
arguments, because TOOL_CALL_END.input does not survive the converter and the
only surviving payload is TOOL_CALL_ARGS.delta — which is the path the client's
own arg accumulation already uses.

Reskin-skill staleness check: no impact from this commit — no contract field,
lint rule, gate or registration path changed. The registration change is in the
next commit, which updates the skill.
2026-08-14 19:15:39 +02:00
Maxim 89553821e4 fix(reskinnable-demo): make a harness run cancellable, unstick its console
Five findings from the four owed re-reviews, plus ruling R26.

The in-flight guard could not release. `defineTool`'s `execute` has no
cancellation hook, so nothing on the server hears a cancelled turn or a
reloaded tab; the guard came back only when a codex run nobody was watching
finally drained, and every retry across those minutes was refused before a
console had even rendered. A second call now SUPERSEDES the first and aborts
it through the real signal — the only thing that reaches `killTree` — and
every publish is gated on still owning the channel, so the superseded run
cannot land its terminal `error` frame in the new run's console. Both halves
are mutation-verified: removing the ownership gate yields
['error','thinking','done'], and restoring the throwaway AbortController
hangs the test.

The SSE route no longer replays a backlog that ends in a terminal frame. The
console mounts on TOOL_CALL_START, before the server clears the channel, so
on the second run of a session it could replay the previous run's frames,
hit that run's `done`, and freeze for the whole of the real run.

The console now says why it is empty after 45s instead of showing
"Starting the harness…" forever — the shape an `off` deploy takes when the
model calls a tool that was never registered (the AI SDK enqueues an invalid
tool call before flagging it, and the renderer is registered unconditionally).

R26 settled: prompt and tool list are now gated on the same condition, with
`buildBankingPrompt` exported so the pairing has a regression test. The ninth
pill stays clickable by design — `Skin.suggestions` is a static array on a
frozen contract in a client module and EXPENSE_HARNESS_MODE is a
non-NEXT_PUBLIC_ server env, so hiding it would take a contract or shell
change.

Also corrects tools.tsx's claim that a failed run returns an error string
(the runtime has no `tool-error` case, so that path never completes), and
mode.test.ts's cleanup, which wrote the string "undefined" when the var was
originally unset.

Reskin-skill staleness check: no impact — no contract field, gate, lint rule,
registration path or beat mechanism changed. `useRenderTool` remains the one
deferred SKILL.md addition, still owed by the task that lands this beat
complete.
2026-08-14 18:51:01 +02:00
Maxim a9eb9dcb16 feat(reskinnable-demo): wire arm A — mode flag, tool, gen-UI, pill
The long-running expense harness was fully built but unreachable. This makes
banking's beat clickable: a four-value EXPENSE_HARNESS_MODE flag, the tool
appended to banking's classic agent behind it, a chat renderer that turns the
live console into the report card, and a ninth suggestion pill.

EXPENSE_HARNESS_MODE is off | tool | factory | both, `off` when unset, and an
unrecognised value THROWS rather than falling back — a typo'd flag that quietly
disables the beat is the most confusing possible failure on stage, because every
other symptom looks like a working demo that chose not to call the tool.
`mode.ts` stays plain server-safe `.ts` because `agent.ts` imports it.

The renderer uses `useRenderTool`, NOT the `useComponent` the plan called for.
`useComponent` wraps `useFrontendTool` and hands its render ONLY the tool's
parsed args (`render: ({ args }) => <Component {...args} />`), so with no
`parameters` schema `{ status, result }` type-checks as `any` and is permanently
undefined at runtime: the slot would show the console forever and never the
report, with a green tree. `useRenderTool` registers a renderer in the same
registry without also registering a frontend tool of that name — right for a tool
the SERVER executes — and passes the real status union. `result` arrives as the
JSON-stringified summary, so it is parsed back tolerantly: a run that dies comes
back as an error string, and throwing there would take the whole transcript down.

The renderer is registered UNCONDITIONALLY even though the tool is gated; gating
it would drag the server-only mode.ts into a client module, and a renderer for a
tool nobody calls is inert.

Two plan errors corrected. suggestions.test.ts has no total-count assertion to
bump — it asserts that exactly ONE pill carries Q2_REPORT_MESSAGE, because
onSuggestionSelect matches by string equality. The harness router will match
EXPENSE_PILL_MESSAGE the same way, so this adds the analogous guard rather than
inventing a count. And .env.example does not claim the harness needs
OPENAI_API_KEY: it reads no such var, spawning `codex exec` and authenticating
through an existing `codex login`. The two prerequisites are documented apart —
the `codex` binary on the host PATH (pnpm never installs it) and OPENAI_API_KEY
for banking's own agent, the thing that routes to the harness.

Reskin skill: checked. Nothing became wrong — SKILL.md's and templates.md's
claims about useComponent vs useHumanInTheLoop/useFrontendTool render shapes all
remain true. One gap is now arguably open: neither file names useRenderTool, so
the skill has no answer for rendering a tool the server executes. Deliberately
NOT written yet, because no skin the skill's flow asks anyone to build needs it
(every skin's server defineTool results surface through CanvasSurface), and the
harness beat is half wired here — arm C and the pill router are later tasks.
Documenting a beat mechanism mid-flight is the same failure the rule warns about.
It belongs in the commit that lands the beat complete, as one bullet in SKILL.md's
gen-UI list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp3i7qBNzWC9xVzsaz5KTZ
2026-08-14 18:13:48 +02:00
Maxim e4ab59e7db fix(reskinnable-demo): respect viewer scroll and distinguish unfiled charges
Review fixes on the harness console and the expense report widget.

Console: autoscroll was unconditional, so a viewer who scrolled up to re-read an
earlier headline got snapped back to the tail within ~7s — during the very run
they were following. Now guarded by a near-bottom check SAMPLED WHEN THE FRAME
ARRIVES, before React appends the line: measured afterwards, the new line is
inside the distance and the guard always reads "at the bottom". Also
`scrollTop = scrollHeight` instead of `scrollTo({...})`, which jsdom does not
implement, so any future test rendering this component no longer throws.

Report widget: an expensable row with no filedTransactionId printed its decision
(honest) in the SAME green as a filed row, so the one row a presenter must not
miss read as a success. It now takes the negative tint, a heavier weight, and an
explicit "not filed" line. All three decision arms stay styled; semantic tokens
only.

The fixture could not have caught the matching fabrication bug: every row varied
decision and filed-ness together, so `decision === "expensable" ? "Filed" :
decision` passed the file. Adds the missing expensable-but-unfiled row and
asserts it reads "expensable", carries "not filed", and is tinted differently
from the filed row. Verified by mutation: keying off decision, tinting the
unfiled row green, and swapping a stat tile's value each turn the suite red.

Softens the stat-tile assertions per review — `data-stat` plus toContain rather
than comparing a parent's concatenated textContent, which coupled the test to
label depth and value/label order. A count landing in the wrong tile still fails.

Reskin-skill staleness check: checked, no skill impact — banking-local component
work, no contract field, hook, lint rule, gate or beat mechanism touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp3i7qBNzWC9xVzsaz5KTZ
2026-08-14 18:08:01 +02:00
Maxim 32bc7e65a4 chore(reskinnable-demo): gate arm C on converter reasoning support
GATE 2 PASS. `convertTanStackStream` preserves codex's reasoning: 289
characters arrived in `REASONING_MESSAGE_CONTENT.delta` and 289 came out
in the same field, over 7 thoughts. Arm C can show visible thinking in
the thread, so Tasks 10 and 11 are unblocked.

The probe taps the raw stream on its way into the converter, so both
sides are measured on ONE codex run — a separate raw run is not
comparable, since the model picks a different number of searches each
time. Measured: 91 chunks in, 74 AG-UI events out, and the 17-event
difference is exactly RUN_STARTED + RUN_FINISHED + 9 CUSTOM + 3
TEXT_MESSAGE_START + 3 TEXT_MESSAGE_END. Every content-bearing chunk
maps 1:1 with byte-identical text, so it is pass-through minus
envelopes, not re-interpretation. No double-wrapping, no duplicated
reasoning or tool envelope.

Three findings Tasks 10/11 have to build around, all in the findings doc:
TOOL_CALL_END loses `input` (parse TOOL_CALL_ARGS.delta instead, the
opposite of the raw-side advice); CUSTOM is dropped entirely, so
`sandbox.file` and `codex.session-id` do not exist on Arm C — two motion
channels, not three; and all assistant text collapses into one minted
messageId, so codex's three separate messages render as one bubble.

Reskin-skill staleness check: no impact. This adds a probe script and
touches no skin, no contract field, no shell file, no lint rule and no
gate — nothing in `.claude/skills/reskin/` describes the harness probes.

Findings appended under a "GATE 2 — converter" heading in
docs/superpowers/plans/2026-08-14-probe-findings.md, which is gitignored
(.gitignore:18 `superpowers/`) and so is deliberately not in this commit.
Gate 1's section was left untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp3i7qBNzWC9xVzsaz5KTZ
2026-08-14 18:07:27 +02:00
Maxim 994376e9f5 fix(reskinnable-demo): guard the harness tool's csv read, run errors, and concurrency
Six review findings on Arm A's defineTool wrapper:

- The CSV read moves into runExpenseHarness behind an injectable readCsv dep,
  inside the published-error path, and checks response.ok so a 404 body is
  never analysed as a statement for four minutes.
- clearProgress moves to the top of runExpenseHarness (deps.channel), which is
  what finally gives the fixed-channel constraint a regression test.
- RUN_ERROR now maps to an error frame carrying its `message`: a rejected model
  arrives as a chunk, not a throw, so the cause was being discarded in favour
  of readSummary's symptom.
- Tool calls render once on TOOL_CALL_END (which carries toolCallName AND the
  parsed `input`) instead of START plus a double-encoded ARGS frame.
- elapsedSeconds is floored at 0 and now exercised by a stepping test clock.
- A module-level in-flight guard makes 'one concurrent run per instance' real;
  it refuses before touching any channel.

Reskin skill: checked, no skill impact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp3i7qBNzWC9xVzsaz5KTZ
2026-08-14 18:01:22 +02:00
Maxim 9de6333cd0 test(reskinnable-demo): assert harness report values with their labels
Deliberate deviation from the task brief, whose test used
`toBeInTheDocument()`: `@testing-library/jest-dom` is not a dependency of this
app and `vitest.setup.ts` registers no matchers, so that form is an "Invalid
Chai property" at runtime and a type error under `pnpm typecheck`. Every other
component test here asserts on plain DOM values, so this follows the house style
rather than adding a shared dependency for one file.

Taken further than a presence check where it was cheap: each stat tile is
asserted as value-WITH-its-label, because a count under the wrong label is
exactly the bug `getByText("14")` cannot see. Likewise the filed marker now
asserts the unfiled row still shows its own decision, which is what proves the
marker discriminates.

One assertion was ambiguous as written: /day spa/ matched both the merchantKind
chip and the reason sentence ("A day spa — …"), so `getByText` threw on
multiple matches. Fixed as the exact string; the component was not reshaped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp3i7qBNzWC9xVzsaz5KTZ
2026-08-14 17:59:49 +02:00
Maxim dfa1d5dd22 feat(reskinnable-demo): expense report widget + live harness console
The two React pieces of the long-running expense harness beat:

- ExpenseHarnessReport — the payoff widget, SHARED BY BOTH ARMS and free of
  anything side-channel-specific (it imports HarnessSummary and nothing else),
  so the comparison stays about the four minutes in the middle. Summary amounts
  are positive (they mirror the CSV); the ledger's negative storage never
  reaches here, so no sign flip.
- HarnessConsole — Arm A only. Tails the progress side-channel over EventSource
  and closes it on a done/error frame, on transport error, and on unmount.

Semantic design tokens only, so every other skin's theme still applies.

Reskin-skill staleness check: checked, no skill impact — additive files inside
src/skins/banking/, no contract field, link builder, hook, lint rule, gate,
registration site or beat mechanism touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp3i7qBNzWC9xVzsaz5KTZ
2026-08-14 17:57:29 +02:00
Maxim 851cdccab7 fix(reskinnable-demo): keep harness console listeners alive across clear and disconnect 2026-08-14 17:56:07 +02:00
Maxim a759e04653 chore(reskinnable-demo): correct probe comments after the run.ts fix
The probe's comments described run.ts's PRE-FIX state and are false since
cd3f519dc0, which gave it a workspace block, gpt-5.6-sol and
`model_reasoning_summary: '"auto"'`. Downstream tasks read these comments as
statements of fact about the library, so a stale one is the same defect class
that fix pass just corrected — left behind in the sibling file.

- Deleted the PROJECTION_BUG constant and the fallback branch that matched it:
  run.ts now provides the capability, so that path cannot occur.
- Deleted createMirrorStream, which the fallback was the only caller of. Keeping
  a hand-copy of run.ts's `chat()` config would have to track its model, its
  workspace block and its reasoning config forever — a drift source, and the
  reason PROBE_MODEL/PROBE_REASONING_SUMMARY are gone rather than re-defaulted.
  Its four now-unused @tanstack imports go with it.
- Header: dropped the env-knob guidance, kept the NODE_OPTIONS explanation
  (still required), and recorded that the gate passed plus the three findings
  most easily got wrong, so the next reader does not re-run a paid probe.
- Noted that sandbox.file.diff never fired (it needs fileEvents.diff), and that
  a RUN_ERROR chunk means a failed run even when nothing throws.

Reskin-skill staleness check: no impact. Comment-and-dead-code only, in a dev
script; no skin, contract field, shell file, registration site, lint rule or
demo-beat mechanism is touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp3i7qBNzWC9xVzsaz5KTZ
2026-08-14 17:54:17 +02:00
Maxim f23a584703 feat(reskinnable-demo): run the codex harness inside a defineTool
Arm A of the harness comparison: mapChunkToProgress + runExpenseHarness +
analyzeExpensesTool. Chunk type strings and field names are Task 4's measured
values (REASONING_MESSAGE_CONTENT carries `delta` only, TOOL_CALL_START
carries toolCallName, tool calls arrive already resolved so only START renders).
execute clears the fixed progress channel first so a second run cannot replay
the previous run's trailing `done` frame, and the catch publishes an error
frame before rethrowing.

The brief's first runner fixture wrote `verdicts: []`, which readSummary
rejects by design; the fixture now writes one verdict rather than weakening the
guard.

Reskin skill: checked, no skill impact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp3i7qBNzWC9xVzsaz5KTZ
2026-08-14 17:51:35 +02:00
Maxim 6815bd43ab feat(reskinnable-demo): harness progress side-channel + SSE route 2026-08-14 17:46:42 +02:00
Maxim cd3f519dc0 fix(reskinnable-demo): make the codex harness stream actually runnable
Three blockers found by the gate probe's live run against the codex binary:

- withSandbox declares the sandbox-projection capability unconditionally but
  only provides it when defineSandbox carries a `workspace`, so omitting the
  block killed the run at middleware setup. Adds a minimal one, and corrects
  the comment that wrongly claimed a workspace block would bootstrap over the
  scratch dir (bootstrapWorkspace lands a source only for type "git"; verified
  expenses.csv survives).
- gpt-5.1-codex is rejected 400 on a ChatGPT-account codex login, arriving as a
  RUN_ERROR chunk rather than a throw. Switches to gpt-5.6-sol.
- modelReasoningEffort alone emits ZERO REASONING_* events; reasoning summaries
  must be requested explicitly. Adds model_reasoning_summary="auto" ("detailed"
  yields none). Verified: 8 REASONING_MESSAGE_CONTENT chunks, 298 chars of
  populated delta, no RUN_ERROR.

Reskin-skill check: no impact — banking-internal module, no contract field,
shell file, lint rule or registration site touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp3i7qBNzWC9xVzsaz5KTZ
2026-08-14 17:43:12 +02:00
Maxim 3f93b3ac42 chore(reskinnable-demo): probe and record codex chunk shapes
GATE 1 of the banking-harness plan. Adds scripts/probe-harness-chunks.ts, which
drives createExpenseHarnessStream over a one-row CSV and prints each distinct
chunk `type` once with a truncated payload, plus per-type text-character totals
so "the type exists" is distinguishable from "the type carries prose".

GATE: PASS. REASONING_MESSAGE_CONTENT carries reasoning text in `delta` and the
text is populated. Full findings, including three blockers the probe uncovered
in run.ts, are in docs/superpowers/plans/2026-08-14-probe-findings.md — which is
gitignored (.gitignore:18 `superpowers/`), so only the script is committed here.

Reskin-skill staleness check: no impact. This adds a dev script and touches no
skin, no `Skin` contract field, no shell file, no registration site, no lint
rule, and no demo beat's mechanism, so nothing in .claude/skills/reskin/ is made
wrong or incomplete by it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp3i7qBNzWC9xVzsaz5KTZ
2026-08-14 17:35:29 +02:00
Maxim e02af7d1e0 feat(reskinnable-demo): shared codex harness stream factory
Reskin-skill check: no impact. This adds a banking-internal harness module and
touches no `Skin` contract field, shell file, lint rule or registration site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp3i7qBNzWC9xVzsaz5KTZ
2026-08-14 17:18:42 +02:00
Maxim 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.
2026-08-14 16:01:28 +02:00
Maxim ccfcc7e68a fix(reskinnable-demo): validate harness summary shape and correct the filing contract 2026-08-14 15:59:32 +02:00
Maxim 14b0ee2bff test(reskinnable-demo): guard the OFFSITE-to-fixture invariant 2026-08-14 15:53:14 +02:00
Maxim f3b29d36fa feat(reskinnable-demo): harness prompt and scratch workspace 2026-08-14 15:47:39 +02:00
Maxim d0a3706a65 feat(reskinnable-demo): harness types + offsite expense fixture 2026-08-14 15:43:59 +02:00
Ran Shemtov fa13d52502 Merge branch 'main' into codex/crewai-full-d6 2026-08-14 09:37:42 +02:00
Maxim 335209b39a Merge branch 'main' into feat/reskinnable-demo-beat-parity 2026-08-13 19:55:27 +02:00
Maxim 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 6473cdcf9d.
2026-08-13 19:49:33 +02:00
Maxim 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 6473cdcf9d: the pre-commit hook fails on
a pre-existing @copilotkit/vue timeout unrelated to this app.
2026-08-13 19:13:32 +02:00
Maxim 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 6473cdcf9d: the repo's pre-commit hook fails on a
pre-existing @copilotkit/vue timeout unrelated to this app.
2026-08-13 19:07:42 +02:00
Alem Tuzlak 14f90410ff docs(examples): fix stale clone paths in v1 example READMEs (#6471)
<!--
Thank you for sending the PR! We appreciate you spending the time to
work on these changes.

Help us understand your motivation by explaining why you decided to make
this change.


**Please PLEASE reach out to us first before starting any significant
work on new or existing features.**

By the time you've gotten here, you're looking at creating a pull
request so hopefully we're not too late.

We love community contributions! That said, we want to make sure we're
all on the same page before you start.
Investing a lot of time and effort just to find out it doesn't align
with the upstream project feels awful, and we don't want that to happen.
It also helps to make sure the work you're planning isn't already in
progress.

As described in our contributing guide, please file an issue first:
https://github.com/ag-ui-protocol/ag-ui/issues
Or, reach out to us on Discord: https://discord.com/invite/6dffbvGU3D


You can learn more about contributing to copilotkit here:
https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md

Happy contributing!

-->

## What does this PR do?

Fixes three `examples/v1/*` README files whose "Clone the repository"
step `cd`s into a directory that no longer exists (leftover from when
examples were reorganized under `examples/v1/`). Following the README as
written fails at the first step with `cd: no such file or directory`.

- `examples/v1/chat-with-your-data/README.md`: `cd
CopilotKit/examples/copilot-chat-with-your-data` → `cd
CopilotKit/examples/v1/chat-with-your-data`
- `examples/v1/form-filling/README.md`: `cd
CopilotKit/examples/copilot-form-filling` → `cd
CopilotKit/examples/v1/form-filling`
- `examples/v1/state-machine/README.md`: `cd
CopilotKit/examples/copilot-state-machine` → `cd
CopilotKit/examples/v1/state-machine`

This matches the already-correct format in
`examples/v1/travel/README.md`.
Docs-only change, no code/behavior affected.

## Related PRs and Issues

- N/A

## Checklist

- [X] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [ ] If the PR changes or adds functionality, I have updated the
relevant documentation
- [X] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)
2026-08-13 18:55:16 +02:00
Jerel John Velarde bd09c3d790 chore(examples): drop grok showcase lockfile
A 13.9k-line new file trips the fork-PR supply-chain heuristic
(security_fork-pr-alert flags any added file over 5000 lines), and the
job cannot post its explanation because fork tokens are read-only.

Several showcases ship no lockfile; this one is not a pnpm workspace
member, so nothing depends on it.
2026-08-13 03:41:19 -07:00
Jerel John Velarde fe0e7cf28f feat(examples): add grok-generative-ui showcase
grok-4.6 runs xAI's X Search server-side, then composes the answer out of
real React components through five CopilotKit frontend tools. Every post
rendered is a real post the model found.

Registers next.config.ts in the build-config allowlist and adds the row to
the examples index.
2026-08-13 03:37:09 -07:00
KNChiu d7dd1bcfbe docs(examples): fix stale clone paths in v1 example READMEs 2026-08-13 10:55:32 +08:00
Ran Shem Tov a3ee26b424 Merge remote-tracking branch 'origin/main' into codex/crewai-full-d6 2026-08-13 00:11:19 +02:00
Maxim 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>
2026-08-12 23:11:38 +02:00
Maxim 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.
2026-08-12 23:09:35 +02:00
Maxim 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>
2026-08-12 19:53:14 +02:00
Maxim 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 085c92e.
2026-08-12 19:37:12 +02:00
Maxim 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.
2026-08-12 19:30:27 +02:00
Maxim 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.
2026-08-12 19:30:10 +02:00
Guido Vizoso 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.
2026-08-12 14:18:48 -03:00
Guido Vizoso 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.
2026-08-12 14:18:48 -03:00
Guido Vizoso 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.
2026-08-12 14:18:48 -03:00
Guido Vizoso 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.
2026-08-12 14:18:48 -03:00
Guido Vizoso 1c4003d2a3 fix(bookstore): seed the default memory bucket and stop claiming per-shopper isolation 2026-08-12 14:18:47 -03:00
Guido Vizoso 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.
2026-08-12 14:18:47 -03:00
Guido Vizoso 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.
2026-08-12 14:18:47 -03:00
Guido Vizoso 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.
2026-08-12 14:18:47 -03:00