Commit Graph

14751 Commits

Author SHA1 Message Date
Maxim 12f4a7342c fix(reskinnable-demo): stop resolvePage returning Object.prototype members
`/banking/constructor` answered 500 where it owed 404, and so did
/logistics/constructor, /people/constructor and the same URL for toString,
valueOf, hasOwnProperty and __proto__.

Three skins resolved pages out of an object literal:

    const PAGES: Record<string, ComponentType> = { "": Index, cards: Cards };
    return PAGES[key] ?? null;

An object literal inherits Object.prototype, so PAGES["constructor"] is a
truthy Function and the `?? null` never fires. src/app/[skin]/[[...rest]]/
page.tsx then does `if (!Page) notFound(); return <Page />` -- `!Page` is
false, notFound() is skipped, and React is handed something that is not a
component. Commerce and keel were already Map-backed and unaffected.

Each of the three now uses a Map, which has no prototype keys.

The real fix is the new guard, src/shell/resolve-page-prototype.test.ts: it
walks EVERY registered skin against every own key of Object.prototype, taken
from the prototype itself rather than hand-listed, on both a top-level and a
nested segment. It pins behaviour rather than implementation, so a skin that
prefers `Object.hasOwn` passes too, and it keeps holding for skins that do
not exist yet.

Two things learned writing it, both kept in the file:

  - The property is NOT "returns null for a prototype key". Keel resolves
    knowledge/<docId> for ANY docId on purpose and renders an in-page
    not-found body, so a non-null answer there is correct. The property is
    "never returns a value INHERITED from Object.prototype".
  - It carries a non-vacuity assertion (at least six skins registered) and a
    per-skin companion (the index still resolves), because a resolvePage that
    returned null for everything would otherwise pass every other assertion
    while serving a dead skin.

Mutation-verified: reverting people to the object literal turns it red.

Found by hand while wiring a fourth skin, not by any gate -- it type-checks
(the Record's index signature says ComponentType), it lints, and no test that
walks the REAL segments ever passes a prototype key.

airline has the same defect and is fixed by the air-wire slot merged next;
this guard is what will hold it green afterwards.
2026-08-12 16:20:28 +02:00
Ben Taylor d6ee68706f feat(runtime): add agentId to AgentRunnerConnectRequest (#6120)
Closes #5911

Adds an optional `agentId` field to `AgentRunnerConnectRequest` so
custom agent runners can hydrate messages when the cache is empty. Also
passes the already-available `agentId` in `handleSseConnect` through to
`runner.connect()`.

## Changes

- `packages/runtime/src/v2/runtime/runner/agent-runner.ts`: Added
`agentId?: string` to `AgentRunnerConnectRequest` interface
- `packages/runtime/src/v2/runtime/handlers/sse/connect.ts`: Pass
`agentId` to `runner.connect()`

## Verification

- Runtime package builds successfully (`pnpm --filter
@copilotkit/runtime build`)
- The change is purely additive — the field is optional and does not
affect existing runners
2026-08-12 09:19:08 -05:00
Maxim 68e3712e0c test(reskinnable-demo): drop the import() type annotation from airline's skin test
`oxlint`'s `consistent-type-imports` warns on `typeof import("…")` inside the
`vi.mock` factory. Spread the original module through a plain record instead;
`importOriginal` still supplies the REAL `HOTEL_CONFIRMATION_MESSAGE`, so a
pill whose text drifts from the constant still fails the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 16:10:55 +02:00
Maxim 7720d5c3c7 test(reskinnable-demo): guard airline's wiring, replay safety and withheld gate
Four guards, each for a defect that leaves the app compiling, linting and
rendering — which is why none of them was caught by the gates.

`skin.test.tsx` — the mounted skin object. `Providers` missing throws only when
a page that needs the ledger renders; `CanvasSurface` missing opens the canvas
region and draws nothing; `nav`/`resolvePage` disagreeing 404s a sidebar link;
a re-added `useData` puts a second seed of AV1423 back in the app. It also
pins the Map-backed `resolvePage` against prototype-chain keys, the way keel's
and commerce's do, and asserts the beat-3d pill interception fires the send
rather than merely returning `true`.

`tools.test.ts` — the drift guard `agent.ts` and `tools.tsx` do not otherwise
have. It cross-checks that every tool the beats need is registered, that
`render_trip_brief` is a SERVER tool listed on the agent (a client tool result
never produces an `a2ui-surface` activity), and that the prompt names no tool
nothing registers — a failure whose only other symptom is "I don't have a tool
for that", live. It also holds beat 2's invariant (no terminal render keyed off
`status`, no `result.match`, no `typeof result === "string"`) and beat 6's
withholding across all four greppable channels, including the PROSE one no lint
rule can see: the four justifying categories and the three decoys are checked
by name in both files.

`components/card-confirmation-card.test.tsx` — beat 3a, rendered. The digits go
to `/authorizations` and nowhere else, the sentence handed to `respond()` does
not contain them, an unreadable value is REFUSED with a reason on screen rather
than silently stripped, a double click cannot charge twice, and a
`FARE_NOT_CHANGEABLE` refusal is shown verbatim with nothing reported as
authorized. That last one is the only symptom the "second door around beat 6"
failure has on the client.

`components/concierge-view.test.ts` — one substrate, one AV1423. No
`use-data.ts` to read, no `useAirlineData`/`useSkinData` call site left, and no
read of the four seed constants the REST seed duplicates field for field. Plus
the two derivations that replaced stored values: the disruption follows the
flight (the old seeded alert said "55 minutes" whatever the ledger held) and
the seat map offers only seats the flight lists free.

`readables.test.tsx` gains beat 3b's THIRD LEG, which its own header said a
later slot had to add: the SCREEN AWARENESS clause in `agent.ts`, plus the
`loading` flag on every page readable.

Skill impact: checked, none. No skill file names any of these paths, and
SKILL.md § Verification's commands (`pnpm lint`, `pnpm test:unit`,
`pnpm build`) all still exist and all cover these.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 16:10:01 +02:00
Maxim 9d601ed03f feat(reskinnable-demo): wire airline onto the ledger and land beats 1, 2, 3a
Aeronova had a complete REST substrate and no UI attached to it. This mounts
the skin object, retires the second seed of Camila's AV1423, and lands the
three beats the substrate could otherwise only half-prove.

MOUNT AND FLIP. `skin.Providers` is now `AirlineProviders` (the one
`GET /ledger` read plus the shell's teach recorder), `nav`/`resolvePage` come
from `nav.ts`/`pages/index.ts` so `/airline/account` and `/airline/rebook`
stop 404ing, and `CanvasSurface` is `AirlineCanvasSurface`. `resolvePage` is
now Map-backed: the object literal it replaced resolved `/airline/constructor`
to `Object.prototype.constructor`, a truthy Function the shell renders as a
page — a 500 where a 404 belongs.

ONE SUBSTRATE. `data/use-data.ts` is deleted and `useData` is dropped from the
skin. `components/concierge-view.ts` projects the REST ledger onto the shapes
the check-in components were written against, so the flight, passenger, seat
map, disruption and rebooking options all come from `GET /ledger` and the REST
seed is the only authority for AV1423. Loyalty mileage, the redemption
catalogue and the bags stay seeded because the ledger models no counterpart —
they are beat 5's distractors — but the member identity is overwritten from
the ledger traveller so the tier appears in two places from one source. The
disruption banner is DERIVED from the flight's own status and delay rather
than stored, so it can no longer outlive the condition it describes.

BEAT 1. `showTrips` leads with the whole account as a trip wall; nine gen-UI
components in all, including the `showSeatMap` distractor `data/beat-map.md`
§ "Beat 5" requires and which was never registered.

BEAT 2. Every terminal render reads the recorded `result` through one shared
`ToolNote`; no `status === ToolCallStatus.Complete`, no `result.match`, no
`typeof result === "string"`. airline can now be added to
`statusKeyedTerminalRender`'s glob in eslint.config.mjs.

BEAT 3a. `authorizeWithCardConfirmation` renders `CardConfirmationCard`, which
POSTs the last four digits straight to `/authorizations` and hands `respond()`
one sentence that does not contain them. It is offered only through
`offerableOptions` — permitted change, money actually due — and it prints the
server's refusal verbatim, so it can never become a second door around
beat 6's gate.

BEAT 3b's third leg. `agent.ts` gains a SCREEN AWARENESS clause telling the
agent its context IS its view of the screen, plus the truncation and loading
rules. Every readable now carries a `loading` flag, so a screen that is still
spinning is not reported as empty.

BEAT 3d. `render_trip_brief` is a SERVER tool emitting
`buildTripBriefOps(briefId)` under `A2UI_OPERATIONS_KEY` — without it nothing
ever opens the canvas — and `fileTripBrief` returns the brief id for it.

Also lands beat 3c's `showRebookingSearch` (four levers plus top-N, confirmed
then navigated through `useSkinHref`), beat 5's three ordered writes, and beat
6's `fileFareException` with a free `z.string()` code and the withholding
stated in its `.describe()`.

Skill impact: checked. The `Skin` contract, the link builders, the
registration sites and the demo beats are all unchanged — this slot only
implements them. `.claude/skills/reskin/` names no file this change deletes
or renames (`data/use-data.ts` is airline's own, not a template path).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 16:09:07 +02:00
Ben Taylor 4383a86198 fix(runtime): populate request headers in runtime error context (#6287)
## Summary

`CopilotRuntime` exposes an `onError` callback, but the v1-to-v2
delegation drops it before server request processing. Runtime failures
therefore can't provide the incoming request headers that applications
use to correlate errors with a user or tenant.

## Root cause

The legacy constructor retains the callback type, while delegated
runtime options omit it. Common, SSE, and Intelligence handlers consume
failures inside their own boundaries, before a generic endpoint hook can
reconstruct the legacy event.

## Changes

- Add one internal runtime reporter that snapshots incoming Fetch
request headers.
- Attach the configured legacy callback to the delegated runtime
instance.
- Route common, SSE, and Intelligence agent-run failures, including
standard `RUN_ERROR` events, through that reporter exactly once.
- Redact sensitive request headers (authorization, proxy-authorization,
cookie, set-cookie, x-api-key, api-key, and the CopilotKit public-key
header) before they reach the `onError` event.
- Preserve responses, stream close behavior, telemetry, cleanup,
logging, endpoint hooks, and callback isolation.
- Add production-path regressions.

## Compatibility

The existing `CopilotErrorEvent` type and optional
`context.request.headers` field remain unchanged. Header names and
values come from the failing request's Fetch `Headers` object, with
sensitive credentials stripped before the event is emitted. The callback
receives a fresh record, so mutation cannot alter the request or a later
event.

## Out of scope

React provider propagation, Chat, CopilotMessages, the deprecated
runtime-client hook, redaction policy, HTTP response exposure, v2
endpoint-hook semantics, and non-agent runtime routes remain outside
this slice.

## Related PRs and Issues

Addresses #2716.

Scope follows
https://github.com/CopilotKit/CopilotKit/issues/2716#issuecomment-5086936254.
Runtime error-routing precedent:
https://github.com/CopilotKit/CopilotKit/pull/2143.

## Test plan

- [x] Runtime error regression and reporter tests, 12 + 4 tests passed.
Covers public routing, sanitized header propagation with credential
redaction, callback rejection containment, snapshots, mutation
isolation, and malformed requests.
- [x] SSE and Intelligence telemetry tests, 7 + 14 tests passed. Covers
agent-run failures, `RUN_ERROR`, setup boundaries, exact-once reporting,
telemetry, cleanup, and response preservation.
- [x] Runtime preservation suites, 57 + 129 + 56 tests passed. Existing
request, endpoint-hook, and runtime-library behavior remains intact.
- [x] Typecheck, formatting, and lint passed on changed runtime files;
lint reported four pre-existing warnings.
- [ ] CI green (`static / quality`, `test / unit` on Node 20/22/24).
2026-08-12 08:59:06 -05:00
octo-patch 30986613d8 feat(runtime): add MiniMax built-in models 2026-08-12 21:51:55 +08:00
Maxim a3ade9a52a fix(reskinnable-demo): make the beat-3d staging test deterministic
Two wave-2 agents reported `stage-attachment.test.ts > "stages only once the
chip is queued AND finished encoding"` intermittently failing the whole suite
with `expected false to be true`, always while several agents were building
concurrently.

The report is real, and the cause is the test harness rather than the code
under test. The three SUCCESS-path tests shared the tight `FAST` budgets built
for the EXPIRY branches: a 40ms `readyMs` ceiling around a real `encodeMs: 10`
timer. `waitUntil` measures its budget with `Date.now()`, so a descheduled
vitest worker spends the ceiling without the event loop ever running the timer
it is waiting for.

Measured in the real vitest/jsdom environment, reproducing the exact timer
structure, 60 samples per condition:

  quiet                       p50=6ms   p95=12ms  max=12ms  (budget 40)
  60 busy loops on 10 cores   p50=11ms  p95=18ms  max=39ms  (budget 40)

A sample landed 1ms inside a 40ms budget. That is the reported failure.

Fixed WITHOUT weakening the property, and the property is now asserted more
directly than before:

- `PATIENT` budgets for the paths that succeed. A success-path wait is
  condition-based and returns the instant its predicate holds, so a generous
  ceiling costs zero wall clock — it only stops a loaded worker from expiring a
  budget never meant to be reached. `FAST` stays exactly as it was for the
  expiry tests, which still need it small.
- The ordering test no longer TIMES the encode, it DRIVES it: a new
  `encodeMs: "manual"` fixture mode latches the `uploading` -> `ready`
  transition behind a `finishEncoding()` the test calls. So the two halves are
  asserted separately and in order — queued-but-encoding must NOT stage, and
  only finishing the encode may.

The old shape read only the end state, inferring "waited for ready" from "ready
by the time it finished". Verified by mutation: deleting the production ready
wait (`const ready = true`) left the previous assertion GREEN, and leaves the
new one RED. The production file is byte-identical to HEAD.

Evidence: full suite 6/6 green (175 files / 1910 tests); the fixed file 6/6
green under the 60-busy-loop load that pushed the old budget to 39/40ms.
`promotions.test.tsx` drives fetch through held promises and microtask flushes
with no real timers, and `recording.tsx` already clears its hold timer on
unmount, so neither had a wall-clock budget to lose; both stayed green across
all runs, including three full-suite runs under 4x oversubscription.

Reskin-skill check (per CLAUDE.md standing rule): checked, no skill impact. The
skill references this file only for the fifteen-cause exhaustiveness gate, the
`[attach:<cause>]` log regex, and `Beat3dTimings` being injectable to force an
expiry — all three unchanged and still accurate. Nothing in `Skin`, the
production module, or any surface a skin author touches changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 15:28:26 +02:00
Maxim dbb411ee1f feat(reskinnable-demo): give keel an impact-brief canvas (beat 3d) 2026-08-12 15:01:10 +02:00
Maxim 36d619c4de feat(reskinnable-demo): give keel its policy register surface (beats 3b, 3c) 2026-08-12 15:01:08 +02:00
Maxim 934ed789e6 feat(reskinnable-demo): give airline a trip-brief canvas (beat 3d) 2026-08-12 15:01:07 +02:00
Maxim d9f74b6f6c feat(reskinnable-demo): give airline its screen surface (beats 3b, 3c) 2026-08-12 15:01:06 +02:00
Maxim d7f43ae1ae feat(reskinnable-demo): give keel a route readable and on-screen readables
BEAT 3b, both halves. `layout.tsx` registers the ROUTE readable — the
path, the highlighted nav entry and, on the two parameterized routes, the
id of the record open — so the agent knows WHERE the operator is. Each
page then describes its own contents, so "what's on my screen?" answers
differently on the Register than it does on `knowledge/<docId>`.

The document page grows the register overlay beside the corpus prose,
which is what gives the second ask something of its own to be about: this
document's review debt, its attestation coverage, its pending revision
and the bodies that have not endorsed it. That readable is registered
UNCONDITIONALLY, before the not-found early return, so an unknown docId
is described rather than answered with "I cannot see the screen".

Unmeasurable attestation coverage travels as null, never 0. A model
cannot discount what you omitted and will restate a zero as an all-clear,
out loud.

`pages/on-screen-readables.test.tsx` is the guard that matters: it stubs
`useAgentContext`, renders each page, and asserts the readable's rows
against the rows the DOM actually painted, element for element and in
order — the drift no source grep can see, and the failure that survives a
live demo unnoticed. It also pins beat 3c's four levers against the
rendered board and the four tinted controls, on a 24-row fixture
deliberately larger than the nine-document seed so any later cap is
exercised.

`pages/parameterized-routes.test.tsx` pins the property keel is the only
skin to have: `knowledge/<docId>` and `runs/<runId>` still resolve AND
still render their record, and an unknown id is still an in-page
not-found body rather than a 404.

Skill impact: checked. `.claude/skills/reskin/demo-beats.md` § 3b names
banking, people, commerce and logistics as the skins with a route
readable plus per-page readables, and tells the reader to DERIVE that
list with `grep -rln useAgentContext src/skins/*/layout.tsx` rather than
trust the sentence. Keel now answers that grep, so the derivation is
correct without an edit — and the beat matrix in CLAUDE.md is the
orchestrator's to update once every keel slot has landed, not this one's
to change mid-flight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:57:15 +02:00
Maxim e21b6a0689 feat(reskinnable-demo): make keel's knowledge page the policy register
BEAT 3c. Four levers — space, attention class, sort and top-N — all
arriving from the query string, all filtering through
`data/register-levers.ts`, and all four controls VISIBLY tinted when set.
It is the controls that light up rather than the rows, because a filtered
list alone asks the room to take the maneuver on faith.

The page renders the lever module's output and reimplements none of it,
so a value the schema can advertise and the view will not honour is not
expressible. An unrecognised value normalizes to null: the view renders
as it does with the lever absent and the control stays untinted.

One pipeline publishes TWO lengths — `matching` under the levers before
truncation, `visible` after it — and the caption, the rows and the
readable all read that one result, so "Top N of M" cannot report that the
filters did nothing.

Served at the `knowledge` segment rather than a new one: the register IS
the parent of `knowledge/<docId>`, the route a citation lands on. Only
the nav LABEL changes, so `resolvePage`, `navigateTo`'s page enum and
every citation href are untouched. Row links go through `useKeelHref`.

`now` comes from the snapshot's own `asOf` rather than the wall clock —
the rows, the tiles and the readable are then measured at the instant the
server measured the register, a test pins the clock by pinning the
fixture, and no impure clock read happens during render.

Two sections are marked-but-absent at the foot of the page — beat 3d's
filed Impact Briefs and beat 6's operator variance form — so the next
author adds them rather than discovering the page has no room.

Skill impact: checked, none. This changes one skin's page, not the `Skin`
contract, the registration sites, or any gate a skin must pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:56:46 +02:00
Maxim 5cd3a1bbe3 feat(reskinnable-demo): add keel's ledger context over the REST substrate
One snapshot read of `GET /api/keel/v1/ledger`, shared by every consumer
under `KeelLedgerProvider`, so the register board, the KPI tiles and the
beat-3b readables can never describe the register a fetch apart.

Shipped UNMOUNTED on purpose: `skin.tsx` belongs to a later slot,
`useKeelData` is still wired, and both parameterized routes still render
from it. `useKeelLedger()` therefore falls back to a standalone read
outside the provider rather than throwing — keel's own `useRole` takes
the same position — so a page can adopt it before the provider lands.

Decides the migration's open question, WHERE TIME LIVES. Keel's run
engine ticks on a 900ms client interval today while the server holds
runs as state only, and keeping both after the migration would put two
clocks on one set of runs: the client's local advance would paint
progress the server never heard of, and the next refresh after any write
would silently rewind it. Time lives on the SERVER, and the client's
only interval RE-READS. That is defensible rather than tidy because
`engine.tick` is pure and duration-driven, so settling on read yields
exactly the value the client interval would have converged to. The poll
here calls `refresh`, never `tick`, and only while a run is running.

The two follow-ups that must land with the consumer flip are written out
in the module header: settle runs in both read routes, and delete
`useKeelData`'s ticker in the same change.

Skill impact: checked. `.claude/skills/reskin/` describes the `Skin`
contract and the registration sites, none of which this touches — it
adds a skin-internal data hook alongside the substrate the beat map
already documents.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:56:23 +02:00
Maxim 6f7051deba feat(reskinnable-demo): give airline on-screen readables and a lever board
Beats 3b and 3c, which `data/beat-map.md` records airline has never hit.

BEAT 3b — a route readable in `layout.tsx` (via `useSkinSegments`, never a
fixed-offset `pathname.split`, which reports the wrong page under LOCK_SKIN),
plus a per-page on-screen readable on all five pages. The three in-memory pages
gain theirs immediately and are LIVE — Trip, Aeronova Club and Disruptions now
answer "what's on my screen?" differently, which is the beat.

`seat-map.tsx` grows an exported `orderedSeats` + `isSelectableSeat` so the Trip
readable lists the seats the map actually painted, in paint order, rather than
re-deriving them — the commerce 5-rows-against-6 bug, which fails silently.

BEAT 3c — `pages/rebook.tsx`, a passenger's rebooking search with window, stops,
cabin and sort levers plus a top-N, all read off the query string through the
shared `readLevers`/`applyLevers` the API route also runs, and all five controls
tinted when set. ONE pipeline publishes `matching` and `visible`, so the caption
("Top 5 of N matching flights") can never say the filters did nothing while the
rows say they did. The trip picker is the search's SUBJECT, not a lever, and
never tints.

`pages/account.tsx` keeps the account visibly CAMILA'S — her name, tier and card
in the header, her own trips first under "Your trips", and the two companions
nested under their own named cards as saved travellers described by their
relationship to her. `data/beat-map.md` names this page as where the rejected
operations-desk reframe would creep back in, so a test asserts the shape: no
table, no traveller column, a per-traveller list each.

Both new pages are UNREACHABLE until a later slot wires `skin.tsx` — they read
`useAirlineLedger()`, and `nav.ts` + `pages/index.ts` exist to make that a
two-import swap. `useAirlineData` and the three existing pages are untouched.

Tests: `readables.test.tsx` guards OMISSION (source grep, anchored inside the
`useAgentContext` call so it cannot pass on a heading or an import) and
`pages/on-screen-readables.test.tsx` guards DRIFT (renders each page and
compares the readable's rows against the painted DOM, element for element and in
order). Note the third leg of beat 3b — the agent's SCREEN AWARENESS prompt
clause — is NOT yet written: `agent.ts` belongs to another slot, and
`readables.test.tsx` says so where the assertion would go.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:53:55 +02:00
Maxim 7fc55dbf01 feat(reskinnable-demo): add airline's REST ledger context, unmounted
`AirlineLedgerProvider` + `useAirlineLedger()` over `GET /api/airline/v1/ledger`
— one fetch, shared, plus the cross-instance revalidation bus logistics uses so
a write that goes straight from a chat card to a route (beat 3a's card
confirmation) still refreshes the screen.

A context rather than logistics' per-instance hook: Aeronova publishes ONE
cross-cutting snapshot, and beat 3b asks the agent to describe exactly what the
passenger can see, so two panels disagreeing about the ledger is the specific
failure this must not have.

SHIPPED UNMOUNTED on purpose. `skin.tsx` belongs to a later slot, so nothing
renders the provider yet and `useAirlineData` remains the live substrate for the
Trip / Loyalty / Disruptions pages — `data/beat-map.md` § "It is ADDITIVE". The
hook THROWS outside its provider and names the three edits that mount it, rather
than returning an empty ledger: a blank account on stage is indistinguishable
from a seed that failed to load.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:53:07 +02:00
Maxim 4ecfeeaaff feat(reskinnable-demo): give airline a durable trip-brief canvas
Beat 3d, the "out" half — airline's first `CanvasSurface`. It renders the Trip
Brief `POST /api/airline/v1/briefs` filed, read back off the app over
`GET /briefs`, so what the room sees is the artifact that survives deleting the
conversation rather than a replay of what the model said.

The two-column split IS the argument, not decoration: the left column is what
only a reader of the attachment could know (hotel, confirmation number, the
22:30 desk cutoff, the cancellation deadline), the right is what only Aeronova
holds (booking, traveler, arrival station, scheduled arrival), and the headline
banner on top is the collision of the two — "AV1423 gets into Lima at 23:00;
Casa Miraflores stops taking arrivals at 22:30". A flat list of the same fields
would render identically and prove nothing.

`arrivesAfterLastCheckIn` is honoured as the tri-state it is: an unmatched
document paints the muted UNCHECKED banner and prints its dropped ledger fields
as "not on file", never the reassuring green one over a comparison nobody made.

Why there is no `A2UIProvider`/`A2UIRenderer` here, unlike the other four
skins: their briefs are compositions the agent selects, so their ops carry a
component tree and their catalogs carry the renderers. Aeronova's brief is one
durable server-settled record of fixed shape — the only selection left is WHICH
brief, so that is all `canvas/trip-brief-ops.ts` carries. `readBriefId` is
deliberately tolerant, so a later slot that emits a richer tree still resolves
the right brief; a named-but-missing brief refuses to fall through to the
newest, because showing a different brief under this run's headline is worse
than showing none.

Shipped UNMOUNTED: `skin.tsx` still omits `CanvasSurface`, and no tool emits
the ops yet. Both file headers name exactly what the later slot must wire.

Tests drive the real substrate end to end — the document facts go through the
route, the server settles the ledger half, and the canvas renders what `GET
/briefs` returns — rather than a hand-written brief object, which would pass
while the two halves disagreed about field names.

Reskin-skill check: no impact on the `Skin` contract, lint rules, registries,
routing or beat mechanisms. Worth the orchestrator's attention though: SKILL.md
and CLAUDE.md both describe `CanvasSurface` as "this skin's own a2ui report
surface", and this one activates off the same `a2ui-surface` activity while
rendering the durable record directly. Flagged, not edited — `.claude/skills/`
is outside this slot.
2026-08-12 14:52:06 +02:00
Maxim ad2780f3db feat(reskinnable-demo): stage the airline hotel confirmation as an attachment
Beat 3d, the "in" half. Aeronova now has the beat-3d attachment wrapper the
other three demo-complete skins have, built on the shell primitive rather than
a fourth private copy of the chain:

- `HOTEL_CONFIRMATION_MESSAGE` — one string shared by the pill and the
  interception, so a drift cannot send the prompt WITHOUT the file (the failure
  that leaves the model inventing the document's contents).
- `sendHotelConfirmationMessage` — the pill path.
- `attachHotelConfirmationByHand` — the presenter's paperclip fallback.

The booking is NAMED (`?booking=bkg-av1423`) rather than left to the route's
default, so a reseed that moves Camila's Lima trip 404s the fetch and ABORTS
the pill loudly instead of quietly attaching some other traveler's room.

Shipped UNMOUNTED: `skin.tsx`, `suggestions.ts` and `tools.tsx` belong to a
later slot, and the file header names exactly what that slot has to wire.

Everything load-bearing — composer lookup, PDF byte check, the four bounded
condition waits, the abort rule, the dual console/alert reporting — is
`@/shell/attach`, verified once in `src/shell/attach/stage-attachment.test.ts`.
The new tests cover only what is genuinely airline's and silent when wrong: the
three parameter values, that the named booking still resolves a confirmation
through `hotelConfirmationFor`, and that the composer chip's filename matches
the one `GET /hotel-confirmation` derives from the confirmation number.

Reskin-skill check: no impact. The `Skin` contract, the lint rules, the
registries, routing and the beat mechanisms are untouched; `templates.md`'s
"DO NOT IMPLEMENT THE CHAIN" guard is what this file follows.
2026-08-12 14:51:18 +02:00
Maxim 54e05fb3f7 fix(reskinnable-demo): type the empty-items case so tsc can check it
`BriefImpacts({ props: { items: [] } } as Parameters<...>[0])` is a TS2352
error: an empty array literal infers `never[]`, which does not sufficiently
overlap `RendererProps<{ items: string[] }>`, so the assertion is a mistake
rather than a widening. `[] as string[]` restores the overlap.

Worth knowing WHY this survived a green slot. Nothing in this app's gates
type-checks test files:

  - `pnpm build` (next build) only type-checks what the app's module graph
    reaches, and no test file is imported by the app;
  - vitest does not type-check at all, so the test passed while being
    ill-typed;
  - there is no `typecheck` script, and CLAUDE.md names `pnpm build` as THE
    type-check gate.

`tsconfig.json` DOES include `**/*.tsx`, so `pnpm exec tsc --noEmit` catches
it -- that command is the only thing in the tree that does.

Found by an explicit tsc run over the slot before merge, not by the slot's
own three green gates.
2026-08-12 14:45:28 +02:00
Maxim d4ed9072d7 feat(reskinnable-demo): render keel's filed Impact Brief on the shared canvas
Beat 3d's outbound half. `canvas/impact-brief-ops.ts` expands a brief the store
already holds into a2ui operations under its own surface id
(`keel-impact-brief`), and `canvas/impact-brief-components.tsx` contributes the
three renderers. `canvas-surface.tsx` spreads both into keel's ONE report
catalog, so the ops report is untouched and the two surfaces are told apart by
surfaceId rather than by catalog.

Where this deliberately departs from ops-report.ts: the figures ARE in the ops.
Run KPIs tick, so the report binds live `useSkinData`; a filed Impact Brief is
immutable the instant `POST /briefs` returns, so its values are read out of the
stored record. The tool takes a `briefId` and nothing else, which is what keeps
every string on the canvas server-sourced rather than the model's second telling
of what it just filed — and it makes the surface replay-safe with no fetch.

`carried` is re-derived against the LIVE register instead of stored: "the
library does not hold POL-118" is a claim about the register NOW, and a reseed
that adds the ref must be able to change the answer. That is the row the beat
rests on, so it is drawn as a finding, and "never released" is kept distinct
from "not in the library" — two facts `POST /briefs` goes out of its way not to
merge.

`bulletin-citations.ts` now EXPORTS its canonical-ref reduction (previously a
private `canonical`) so the canvas asks the same question of the same refs; a
third private copy is how the two surfaces come to disagree about POL-118.

Shipped UNMOUNTED for the tool half: `renderImpactBriefParams` and
`buildImpactBriefOps` are exported for a later slot's `agent.ts`. The canvas
half needs no wiring — keel already sets `CanvasSurface`.

Skill check: no impact on `.claude/skills/reskin/`. The `Skin` contract, the
link builders, registration and the lint gates are untouched; the skill's one
reference to `canvas-surface.tsx` (SKILL.md:536, naming it as the file behind
`CanvasSurface`) is still accurate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:38:58 +02:00
Maxim 7e38f97c76 feat(reskinnable-demo): stage keel's regulatory bulletin into the composer
Beat 3d's inbound half for keel. `attach-bulletin.ts` is the three per-skin
values and nothing else — the URL, the filename and the message — over the
shell-owned staging chain in `@/shell/attach`. The knowledge space is named in
the URL rather than left to the route's `DEFAULT_SPACE`, so a corpus rename
404s the fetch and ABORTS the pill instead of quietly serving a different
space's bulletin under a filename that says privacy.

Shipped UNMOUNTED: both mount points (`chatHeaderActions` for the paperclip,
`onSuggestionSelect` for the pill) live in `skin.tsx`, and the pill itself in
`suggestions.ts` — a later slot's files. `BULLETIN_MESSAGE` is exported so the
pill and the interception key off one value and cannot drift.

Tests drive the real chain against a fake composer rather than mocking
`@/shell/attach`: a mock would prove keel passes three values to a function and
nothing about whether those values work. The fifteen failure causes stay
covered once, in the shell.

Skill check: no impact on `.claude/skills/reskin/`. This follows
templates.md's "DO NOT IMPLEMENT THE CHAIN" wrapper shape verbatim and adds no
new pattern; nothing the skill references was renamed or deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:37:30 +02:00
Maxim 1dabe60372 feat(reskinnable-demo): give keel a policy-register REST substrate
Merges blitz slot keel-substrate. Harbor Point Health keeps its identity as a
knowledge and operations desk; the substrate adds the half every beat needed --
the policy REGISTER, the lifecycle state of the nine corpus documents.

The corpus/register split is load-bearing: prose only changes when an author
edits it and stays a static server-safe module, while review dates, attestation
coverage and the pending revision are operational state the demo mutates.
GET /documents/[docId] joins the two server-side, which is how the
parameterized knowledge/<docId> route survives the migration.

- Beat 6 gate: 403 UNENDORSED_REVISION on releasing a revision its governance
  committee has not signed. Symptom only -- it names the document, the revision
  and the body that has not signed, never a way through. Unlock is a ratified
  publication variance under a justifying code.
  Justifying: PATIENT_SAFETY_ALERT, ACCREDITATION_FINDING, REGULATORY_MANDATE,
  INCIDENT_CONTAINMENT. Decoys: COMMITTEE_CALENDAR, EDITORIAL_CLEANUP.
  COMMITTEE_CALENDAR is load-bearing -- it is the real reason a person reaches
  for an interim release, so a bluffing agent picks it and stays blocked.
- Beat 3a is not an override: /countersignatures re-runs the same
  checkReleaseAuthority(), pinned by a four-test block whose companion case
  proves a justifying variance DOES lift the same block, so a route that
  refused everything could not pass.
- Scope discipline: no beat here decides anything clinical. The gate is about
  who may RELEASE a revision, not what the policy says.

dev/reset returns reset: ["store"] and reports memoryBeats "unarmed" rather
than a bare ok, because keel has no intelligence/ module yet -- the
incompleteness is legible in the artifact instead of hidden in a comment.

Reskin skill impact: deferred to the docs slot on purpose -- airline is landing
the same shape in parallel and both would conflict on the same paragraphs.
2026-08-12 14:06:12 +02:00
Maxim 992d578476 feat(reskinnable-demo): give airline a passenger-facing REST substrate
Merges blitz slot air-substrate. Aeronova stays a passenger concierge and
reaches beats 3a, 3c and 6 through ENTITLEMENT rather than hierarchy: a fare
rule refuses as hard as an approval authority and needs no operator.

- Beat 6 gate: 422 FARE_NOT_CHANGEABLE, naming the fare condition only.
  Unlock is a fare exception filed under a waiver category and approved.
  Justifying: SCHEDULE_CHANGE_TRIGGERED, MEDICAL_DOCUMENTED,
  BEREAVEMENT_DOCUMENTED, MILITARY_ORDERS. Decoys: CHANGED_PLANS,
  FOUND_LOWER_FARE, ELITE_COURTESY -- what everyone actually tries.
- Grounding: an exception lifts only when its category matches what the
  booking documents, so the unaided replay is a procedure rather than a
  memorized literal. This adds a SIXTH leak channel, Booking.waiverGround,
  which store.snapshot() strips; the passenger reads the same fact as prose.
- Beat 3a is not an override: /authorizations re-runs the same fare check,
  so the card's last 4 cannot release what the gate blocks.

Three gated bookings, one (AV5KD1) unlockable by nothing, which is what makes
the decoys real rather than theoretical.

Reskin skill impact: deferred to the docs slot on purpose -- keel is landing
the same shape in parallel and both would conflict on the same paragraphs.
2026-08-12 14:05:54 +02:00
Maxim 3da30eec82 feat(reskinnable-demo): add the airline skin's v1 API routes
Twelve routes over the passenger-facing substrate: one `/ledger` snapshot, the
booking reads, beat 5's three ordered writes, beat 3a's card-confirmed change,
beat 6's gate plus its two-step unlock, beat 3d's generated document and the
brief it files, and a gated `dev/reset`.

Three properties are load-bearing and each is pinned by a test that goes red
alone when the property is removed:

- `POST /authorizations` runs the SAME `checkFareChange()` as the ordinary
  change route, so a valid card confirmation on a non-changeable fare is still
  422. Its test walks EVERY option on all three gated bookings, discovered from
  the live ledger rather than hardcoded, and a companion assertion says what the
  unlock path IS so the block cannot pass by refusing everything.
- The change route stops at 402 when money is due, which is what makes the card
  the only path to a paid change rather than a step the demo could skip.
- Neither the filing route nor the approve route ever reveals whether an
  exception lifts. Their responses are asserted field-for-field identical
  between a justifying category and a decoy — a `lifts` flag would hand over the
  withheld catalogue one probe at a time.

`/hotel-confirmation` generates the document per request and fails loud with a
logged cause, because a silent 404 there aborts beat 3d's pill with nothing to
debug. `POST /briefs` settles the ledger's facts in every direction —
overwritten on a unique match, dropped when there is none, and reported in
`settled`/`unmatched` so the tool can tell the agent rather than silently
overrule it — while leaving the document's own facts model-authored, which is
the beat's whole proof.

`dev/reset` reports `memoryBeats: "unarmed"` on purpose: airline has no
seed-memories module yet, and a reset route without one looks identical to a
reset route with one.

Reskin-skill check: no impact. All twelve routes are new; no shared route
helper, contract, lint rule, gate or registration changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 13:59:09 +02:00
Maxim 88be5ea8ce feat(reskinnable-demo): give the airline skin a passenger-facing REST substrate
Aeronova keeps its in-memory concierge store and gains a server. ADDITIVE
throughout: `use-data.ts` is untouched, every existing page still reads it, and
the new seed is written to AGREE with it about Camila's AV1423 (same flight,
route, cities, aircraft, gate, times, delay, and the same three alternatives)
because both substrates are on screen in the same demo.

The gate is on the FARE, not on a person's rank — see `data/beat-map.md`.
`checkFareChange` refuses a Basic Economy or promo ticket naming the fare
condition and nothing else, short-circuits to a free involuntary change on a
cancelled flight or a schedule move past four hours (which is what keeps beat 5
clear of beat 6), and lifts only for an approved exception whose category
matches the circumstance the booking's record documents.

That grounding is what makes the two taught cases genuinely unlike each other:
without it, a demonstration on the schedule-change booking replays on the
medical one as a memorized literal. A third booking documents nothing at all, so
the decoys are real rather than theoretical — every category in the catalogue
files honestly against it and releases nothing.

The catalogue is WITHHELD from the agent, and `store.snapshot()` strips
`waiverGround` for the same reason: it is a code-shaped token mapping one-to-one
onto a justifying category, and no other skin has a grounded gate, so it is a
sixth leak channel the failure-modes list does not name. The passenger reads the
same fact as prose in `fareNotes`.

Beat 3a's card confirmation is a second factor and never an entitlement
override; beat 5's vocabulary lives in a module sharing no token with beat 6's,
asserted both ways. 30 rebooking options on the cancelled return leave ten rows
under beat 3c's own lever set, asserted directly so a reseed that thins the
board fails a test instead of a demo.

Reskin-skill check: no impact. Every file here is new; no contract field, link
builder, lint rule, test, gate, registration, routing boundary, beat mechanism
or skin identity changed. The obligations this creates for LATER slots — the
`withheldGateVocabulary` glob, the seed-memories module, the exception form —
are recorded in `data/beat-map.md` rather than in the skill, because they are
facts about this skin and not about authoring skins in general.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 13:58:50 +02:00
Maxim 949de51de8 docs(reskinnable-demo): map the airline skin's passenger-facing demo beats
Aeronova's beat map, written before any substrate code, per demo-beats.md.

The premise it is built on: authority does not have to be ORGANIZATIONAL, it
can be ENTITLEMENT. A gate that says "your fare does not permit this" refuses
exactly as hard as one that says "you lack approval authority", and it needs no
hierarchy at all — so Aeronova stays a passenger-facing concierge and still
reaches beats 3a, 3c and 6. Everything the beats wanted from an operator has a
passenger analogue that is more familiar, not less: fare rules for an approval
authority, a fare exception for an escalation, the account's own bookings for a
board, the rebooking search everyone has used for a work queue, the card
confirmation on a paid change for a sign-off PIN.

Also records, for the slots that follow: the withheld-catalogue lint glob does
not list airline; the reset route deliberately reports its memory beats as
unarmed; and the two substrates (this one and `useAirlineData`) are both live
and deliberately agree about Camila's AV1423.

Reskin-skill check: no impact. This commit adds a file and changes no contract,
lint rule, test, gate, route shape or skin identity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 13:58:26 +02:00
Maxim 7e59e92568 feat(reskinnable-demo): add keel's REST API over the policy register
Puts keel on the same substrate as banking, logistics, people and commerce.
The route tree, one line each:

  GET  /api/keel/v1/ledger                        the one snapshot read
  GET  /api/keel/v1/documents/:docId              { doc, record }, joined server-side
  POST /api/keel/v1/documents/:docId/release      BEAT 6 — the gated write
  POST /api/keel/v1/documents/:docId/flag         BEAT 5 step 1
  POST /api/keel/v1/documents/:docId/notices      BEAT 5 step 2
  POST /api/keel/v1/documents/:docId/notes        BEAT 5 step 3
  POST /api/keel/v1/variances                     BEAT 6 — file a draft
  POST /api/keel/v1/variances/:id/ratify          BEAT 6 — the other half
  POST /api/keel/v1/countersignatures             BEAT 3a — the e-signature release
  GET  /api/keel/v1/bulletin?space=               BEAT 3d — the ingested document
  GET  /POST /api/keel/v1/briefs                  BEAT 3d — the durable artifact
  GET  /POST /api/keel/v1/runs, /runs/:runId, and the step approve/reject/cancel writes
  POST /api/keel/v1/dev/reset                     presenter reset, gated

`knowledge/:docId` is served by `GET /documents/:docId`, which returns the
corpus prose and the register row together so the two can never be fetched a
moment apart; `runs/:runId` by `GET /runs/:runId`, which returns the run and
its playbook together.

Three separations the tests exist to hold:

- `countersignatures` re-runs the SAME `checkReleaseAuthority()` the release
  route runs, so a valid e-signature PIN on an UNENDORSED revision is still
  refused with `UNENDORSED_REVISION`. Without that, beat 3a is a second door
  around beat 6, the agent takes it, the teach arc never fires and nothing
  fails.
- `POST /variances` refuses an uncatalogued code WITHOUT enumerating the
  catalogue, while `flag` and `notices` deliberately DO name their valid sets
  — beat 5's vocabulary is given to the agent, beat 6's is withheld.
- `dev/reset` reports `reset: ["store"]` and never "memory". Keel has no
  intelligence/seed-memories.ts yet, so it cannot re-arm the memory beats,
  and a route that claimed otherwise would stop a presenter looking for the
  reason beat 6 opened already taught.

Skill impact: checked, none. Adds a skin-local API tree; no `Skin` contract
field, registration site, link builder or lint/test gate the reskin skill
documents is affected. Keel is still correctly absent from eslint.config.mjs's
`withheldGateVocabulary` glob — it has no agent-facing gate file yet, and the
slot that writes `tools.tsx` owns adding it.
2026-08-12 13:28:44 +02:00
Maxim 5bbcf93323 feat(reskinnable-demo): add keel's policy-register data substrate
The half of Harbor Point Health that every demo beat needs and that the
in-memory skin never had: the LIFECYCLE state of the nine corpus documents.
The corpus keeps supplying a document's words; the register supplies its
review dates, attestation coverage, effective revision and the revision
waiting to be released.

Strictly ADDITIVE. `useKeelData`, its 900 ms run ticker and the pages that
read it through `useSkinData` are untouched — `types.ts` gains the register
types below a marked divider and `personas.ts` gains one strict
`findPersona` (the falling-back `getPersona` is right for a render and wrong
for a mutation, which would otherwise attribute an unknown id to Ana Reyes).

The load-bearing modules:

- `release-authority.ts` is beat 6's gate and the ONLY gate. Its refusal
  names the symptom — document, revision, and which body has not endorsed —
  and nothing about a way through.
- `variance-codes.ts` is the unlock catalogue, four justifying codes and two
  decoys, WITHHELD from the agent; the header states the withholding the way
  logistics' escalation-codes.ts does.
- `signing-pin.ts` is beat 3a's second factor, and says out loud that its
  validity is FORMAT-ONLY and is not an authentication control.
- `attention.ts` models attestation coverage as a tri-state, so a document
  nobody is assigned to reads as unmeasurable rather than as 0%.
- `register-levers.ts` owns beat 3c's four levers in one normalized record,
  with an explicit "not pulled" sentinel inside each enum.
- `bulletin-citations.ts` / `bulletin-pdf.ts` build beat 3d's ingested
  document, carrying exactly one policy ref the register does NOT hold —
  re-checked against the live register, and dropped rather than
  misattributed once a reseed adds it.

Skill impact: checked, none. This adds a skin's internals under
src/skins/keel/data/; it touches no `Skin` contract field, no link builder,
no registration site, and no lint rule or test gate the reskin skill names.
2026-08-12 13:28:11 +02:00
Maxim a32af92121 docs(reskinnable-demo): map keel's demo beats before the substrate
Authored per .claude/skills/reskin/demo-beats.md, before any substrate code:
the tools, pages, prompt and pills of every later keel slot come out of this
file. Records the two design decisions that are easiest to get wrong later —
beat 3a's e-signature PIN is a SECOND FACTOR that re-runs beat 6's release
gate rather than a second door around it, and beat 6's publication-variance
catalogue is withheld from the agent while its human filing form stays open.

Skill impact: checked. This adds a beat map in the shape
.claude/skills/reskin/demo-beats.md already prescribes; it changes no
contract, command or gate the skill documents.
2026-08-12 13:27:45 +02:00
Maxim 8d29771164 Merge remote-tracking branch 'origin/main' into feat/reskinnable-demo-beat-parity 2026-08-12 11:46:00 +02:00
Murat Sari 8c670653ce fix: align Angular 20 support and resolve packed smoke paths (#6452)
## What broke

The regression was introduced by commit fec70d086 (feat(angular):
checkpoint 2 - core and package), merged through PR #6109 as b07482da5.

That commit established Angular 20 as the package’s compiler and support
floor, but the demo remained on Angular 21 after 8b13fbcb7 (build:
update ng).
It also introduced the packed smoke runner without canonicalizing macOS
temporary paths, allowing /var/... and /private/var/... to resolve
  inconsistently.

  ## Why I made this change

I moved the demo back to Angular 20 so it exercises the lowest supported
Angular version, aligned the Angular 20 dependencies and support
contract on
20.3.27, and canonicalized the packed consumer directory before starting
the SSR server.

This keeps the demo, package metadata, tests, and lockfile consistent
while making the packed smoke test reliable across symlinked temporary
  directories.

  ## Changes

  - Align the Angular demo with the Angular 20 support floor.
  - Update Angular 20 dependencies and support-policy tests to 20.3.27.
- Resolve the packed consumer directory to its real path before
launching SSR.
2026-08-12 11:28:22 +02:00
Ben Taylor 04c4198a14 docs: fix Copilot Runtime reference links (#5296)
## What does this PR do?

Fixes stale Copilot Runtime documentation links that still point to
`/concepts/copilot-runtime` and now route users to the existing
`/backend/copilot-runtime` page.

This updates both the source JSDoc and the generated reference MDX so
the current docs content and future regenerated reference docs stay
aligned.

## Related PRs and Issues

- Closes #2082

## Testing

- `rg -n "concepts/copilot-runtime" packages
showcase/shell-docs/src/content` returns no matches
- `rg -n "backend/copilot-runtime"
packages/runtime/src/lib/runtime/copilot-runtime.ts
packages/react-core/src/components/copilot-provider/copilotkit-props.tsx
showcase/shell-docs/src/content/reference/v1/classes/CopilotRuntime.mdx
showcase/shell-docs/src/content/reference/v1/components/CopilotKit.mdx`
- `git diff --check`

## Checklist

- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] 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-11 22:14:00 -05:00
Maxim 62aa7e944e docs(reskinnable-demo): record logistics as the fourth demo-complete skin
The beat matrix in CLAUDE.md had logistics ticking beats 4, 5 and 6 while
still showing ❌ on beats 2 and 3a-3d, which had shipped earlier on this same
branch. Every cell in the logistics column is now derived from the tree rather
than from the commit log:

  Gen-UI count      grep -A3 'useComponent(' src/skins/logistics/tools.tsx
                    | grep -c 'name:'                                  -> 6, not 5
  Replay-safe       the write tools carry structured results
  Planner PIN       beat 3a
  Readables         grep -rln useAgentContext src/skins/*/layout.tsx
  Four levers       status, class, sort, top-N all arrive from the query string
  Rate sheet        beat 3d

Prose that contradicted the matrix is corrected in the same pass, in the five
places a skin author actually reads: README's "three of the six skins", the
skill's routing table ("the only three at 9/9"), its "do not use airline,
logistics or keel as demo references" warning, SKILL.md's short version, and
templates.md's pill-count paragraph. Pill counts are derived too and the real
spread is 4 to 10 (banking 8, people 9, commerce 9, airline 5, logistics 10,
keel 4), so the paragraph now says plainly that neither end of that range
predicts coverage.

Where a number was load-bearing it is replaced with the command that derives
it, continuing the convention this branch established: the matrix note now
carries the gen-UI derivation alongside the three that were already there, and
says why -- this table is prose, it rots silently, and logistics spent two
releases showing red on beats it had already shipped.

Reskin skill impact: YES, and it is fixed here. Four claims in
.claude/skills/reskin/ named logistics as a non-demo reference or counted three
demo-complete skins. Left standing they would have routed the next skin author
away from the only worked example of retrofitting beats onto an existing skin.
src/shell/skin-roster-docs.test.ts (27 tests) passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 04:40:38 +02:00
Maxim b5800c35b6 docs(reskinnable-demo): fold logistics into the teach-mode roster
The standing rule at the top of CLAUDE.md, answered out loud: YES, this change
made things in `.claude/skills/reskin/` and its neighbours wrong. Logistics is
now a FOURTH teach-mode skin, and four separate places were written against
three.

The DERIVATIONS the previous commit introduced all still read correctly —
`grep -l offerWorkflowRecording src/skins/*/tools.tsx`,
`grep -rln useAgentContext src/skins/*/layout.tsx` and
`ls src/skins/*/intelligence/seed-memories.ts` each pick logistics up
automatically, which is exactly what they were converted for. What rotted was
the PROSE around them:

- `docs/teach-mode/README.md` enumerated a role-by-role verdict for every skin
  the roster grep returns. The grep now returns four and the paragraph named
  three, so a reader would have found logistics uncertified with no way to tell
  whether that was an omission or a verdict. Logistics satisfies all five roles;
  it is also a second worked example of the pinned replay directives, so "so
  copy commerce" now names both.
- That file's memory-SCOPE divergence note gave only half the argument for
  `user` over `project`. The half it was missing is the decisive one: a skin
  whose `forget-memories.ts` skips project-scoped rows — logistics and commerce
  both do — has a presenter reset that physically cannot un-teach a
  project-scoped memory, so the second run of the demo opens already taught and
  the beat proves nothing while looking perfect.
- `failure-modes.md` § 10 described withholding the vocabulary from the agent
  and never said to BUILD the surface the operator demonstrates on. Withholding
  perfectly with no filing form is an unlearnable gate — which is the state
  logistics was actually in. Points at the worked example and the two
  load-bearing properties of it.
- `demo-beats.md` said the pre-bar skins "ship four or five" pills. Logistics
  ships ten. Fixed, and the sentence now says so as the reason to run the count
  rather than trust the prose.

`CLAUDE.md`'s beat matrix gains logistics' teach-a-procedure row, and its
logistics bullet is corrected: it claimed the skin omits `chatHeaderActions`
and `onSuggestionSelect`, which beat 3d set two commits ago.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 04:30:31 +02:00
Maxim eb88fa6139 feat(reskinnable-demo): wire Meridian's teach chain and beat-6 pill
The client half of beat 6. The REST gate, the decoy catalogue and the pure-REST
proof already shipped; what was missing was the loop that lets the agent learn
the unlock by watching.

HITL chain, in order: offerWorkflowRecording -> awaitDemonstration ->
saveLearnedProcedure. The REPLAY chain is deliberately not new — a later
request on a different gated shipment goes through fileEscalation then
commitMitigation, the very write that was refused. Nothing is special-cased for
the replay, which is the point: the agent applies ordinary tools in an order it
was never told.

`agent.ts` gains an ACTION DISCIPLINE clause that recalls FIRST and branches on
what comes back, so a taught agent does not keep offering to be taught, and
that names the plausible substitutes it must not reach for — including the PIN
card, which confirms who is acting and never how much they may spend.

The five leak channels, each checked by hand because lint sees only two:
readable (none registered), schema (fileEscalation's `code` stays a free
z.string()), tool description (states the withholding), prompt (names
escalation codes as the one thing NOT in context), refusal body (asserted
against the whole catalogue in data/blocked-by-authority.test.ts).

The pill's comment states only figures `blocked-by-authority.test.ts`
re-derives from seed.json, and says plainly which parts of the beat no test in
this repo asserts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 04:29:58 +02:00
Maxim f1f0329a0c feat(reskinnable-demo): pin Meridian's teach-chain replay directives
`docs/teach-mode/README.md` names two replay invariants and says to copy
commerce, because commerce is the only skin whose behaviour on them is pinned
rather than hand-rolled. This is Meridian's sibling of that module.

Both failures are invisible at runtime — the app compiles, the cards render,
the demo runs, and the card simply states something the thread does not
support:

- SURVIVES REPLAY. The recorder REPORTS its step count inside the directive
  and the card prints the reported number. A card recounting `\d+\.\s` matches
  in its own prose miscounts every step label containing a numeral, and
  freight labels carry them constantly.
- A SETTLE IS NOT AN ANSWER. Both buttons on the save card settle with a
  string, so `typeof result === "string"` says the card was answered and
  nothing about the answer. `classifySaveProcedureResult` never guesses
  "saved": an unrecognized settle renders as "already answered", never as a
  receipt for a durable write that may not have happened.

Every test round-trips a builder through its reader, so the wording and the
matcher cannot drift apart.

Scope is `user`, not `project`, and the module says why:
`intelligence/forget-memories.ts` deliberately SKIPS project-scoped rows so a
Meridian reset cannot delete a sibling skin's seeded memories — which means a
project-scoped beat-6 procedure would survive every presenter reset and the
second run of the demo would open already taught.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 04:29:40 +02:00
Maxim 6cf392b43d feat(reskinnable-demo): add Meridian's planner escalation filing form
Beat 6's part 5. `data/escalation-codes.ts` reserved `ESCALATION_CODE_LABELS`
for "a planner's filing form" and then noted that no such form existed — so
the gate was withheld from the agent and unlearnable by it, because there was
nowhere for a human to demonstrate the unlock. This builds the form: the ONE
surface in the skin where the code vocabulary legitimately appears, because a
HUMAN reads it and the agent learns the code by WATCHING the planner choose.

- `data/authority.ts` grows `blockedByAuthority`, deriving the gated cases from
  the same pure option calculation the mitigate route recomputes with, so the
  form can never advertise a cost the gate would not check.
- `components/escalation-form.tsx` lists justifying codes and decoys TOGETHER,
  unmarked and in catalogue order. A form that flagged the working ones would
  make the demonstration a guided tour rather than an exercise of knowledge
  only the operator has.
- Both write paths bracket themselves with the shell recorder, and the filing
  step carries the code as DATA (`logStep(label, code)`) — that is what
  `getDemonstratedCode()` reads. It records the code the planner ACTUALLY
  filed, decoy included: a recorder that silently corrected them would report
  a procedure nobody demonstrated.

`blocked-by-authority.test.ts` asserts against `seed.json` itself that the
network still offers TWO gated shipments — one to teach on, a different one to
replay on — because the case taught on stage is released by the demonstration.
A fixture cannot notice that going to one.

`on-screen-readables.test.tsx` gains a planner-auth stub: `usePlannerAuth`
throws outside its provider by design and these pages render bare.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 04:29:22 +02:00
Maxim 48f523b72b feat(reskinnable-demo): mount the shell recorder in Meridian's providers
Beat 6 needs the teach-mode recorder, and it has to wrap BOTH the app card
and the chat card: the demonstration happens on the Control Tower while the
card that reads `steps` / `getDemonstratedCode()` lives in the transcript.
A provider around only one of them makes every `logStep` from the other a
silent no-op — `useRecording` returns inert fallbacks outside a provider, so
nothing throws and the feed is simply empty, discovered on stage.

Imported from `@/shell/teach` rather than re-implemented. Three skins each
shipped a private copy and they diverged; this is the one module.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 04:29:02 +02:00
Maxim 6d59e02ab6 fix(reskinnable-demo): correct the beat-5 pill's distractor count
The comment said "the seven other registered write tools" and then listed the
three real writes among them, which is both wrong and self-contradictory — and it
was a hardcoded count in exactly the place this repo keeps learning not to put
one. Replaced with the distractors by name plus the grep that enumerates the real
set, so the next tool added to `tools.tsx` cannot silently falsify it.

Checked, no reskin-skill impact: this is a comment inside one skin's pill list.
2026-08-12 04:03:05 +02:00
Maxim c05463a63a docs(reskinnable-demo): retire the "three skins have memory" count claims
THE STANDING RULE, ANSWERED. Adding beats 4 and 5 to logistics falsified five
sentences in `.claude/skills/reskin/` and two in CLAUDE.md — every one of them a
hardcoded count of which skins ship `intelligence/seed-memories.ts`, and every
one true when written:

- demo-beats.md § Presentation requirements ("the only three with
  seed-memories.ts + forget-memories.ts; logistics restores its data store only,
  so it cannot reset beats 4–6")
- demo-beats.md § Which skin to copy for what ("the only three with a seed file")
- demo-beats.md's "do not use these as demo references" paragraph (logistics has
  "no memory prompts, no memory tools and no seed file")
- templates.md § seed-memories ("two of the only three in the repo")
- templates.md § copy forget-memories from COMMERCE ("All three skins have one")
- SKILL.md § intelligence ("`banking`, `commerce` and `people`")
- CLAUDE.md's agentRegistry bullet and its beat matrix

Each is replaced with its DERIVATION rather than a corrected number —
`ls src/skins/*/intelligence/seed-memories.ts`, `ls -d src/app/api/*/v1/dev/reset`
— which is the fix failure-modes.md § "counts rot" already prescribes and which
the next skin cannot re-falsify. The two skin-count guards in
`skin-roster-docs.test.ts` caught both halves of this: the new prose, and the now
dead COUNT_EXEMPTIONS entry for a phrase that no longer exists. That exemption
list is now empty, with a comment saying to prefer a derivation over adding a new
one — an exemption is a promise to hand-recount a subset forever, and that file
exists because the promise is not kept.

The one substantive addition rather than a correction: the memory half of a reset
is now called out as a SEPARATE, narrower question from having a reset route at
all, because a skin with a route and no seed file has a Reset button that looks
identical and cannot re-arm beats 4–6.

Nothing else in the skill went stale. The `Skin` contract is untouched, no link
builder or hook changed, no lint rule or gate moved (`withheldGateVocabulary`
already covers logistics' `tools.tsx` and `agent.ts`, and no file's selector set
changed), registration and the client/server boundary are unchanged, and beat 6's
mechanism is exactly as it was.
2026-08-12 04:00:14 +02:00
Maxim efa2e91aa3 feat(reskinnable-demo): seed and reset Meridian's memory (beats 4 and 5)
"It already knows me" is a FILE. Logistics had the full per-user identity
plumbing and no seed file, so it got zero demo value from the hardest part of
what it had already built; this is the file, its sibling wipe, and the reset that
uses both.

WHAT IS SEEDED. Beat 4's topical preference (read the queue by lane, anything
past its promised date first, exposure in whole thousands — three checkable
behaviours, one per flag on `showExceptionSummary`) and beat 5's operational
procedure, whose own text says "run all three immediately, in order, without
asking for confirmation" and "this is NOT the procedure for getting a mitigation
past the planner's approval authority".

WHAT IS DELIBERATELY NOT SEEDED. Beat 6's procedure. The refusal names the
problem and never the fix, and the vocabulary that lifts the gate is withheld
from the agent through the readables, the tool schemas, the prompt and the 422
body — seeding it here would hand it over through the one channel none of those
guards watch. `seed-memories.test.ts` asserts the absence, including "escalation"
in any casing, because a paraphrase leaks the shape of the answer just as well.

BOTH BUCKETS, not just the planner's. The client's `properties` frequently do not
reach `identifyUser` on the run path, so recall looks at the DEFAULT bucket;
seeding only Rosa's leaves it empty and beat 4 fails with the agent cheerfully
saying it has no saved format while the memories sit stored one id over. Banking,
people and commerce all measured the same thing. Switching planner therefore does
NOT re-scope memory yet — `user-id.ts` says so rather than implying otherwise.

The reset now ASKS `memoryScopeUserIds()` which buckets exist instead of carrying
a list: `resolveUserId` short-circuits on a pinned `INTELLIGENCE_USER_ID` (which
Playwright sets), so a hardcoded list would scrub buckets nothing reads while the
run uses the pinned one — silently, behind `ok: true`. `PLANNER_IDENTITY` also
becomes a Map, because it is keyed by untrusted client input and a plain-object
lookup resolves "constructor" truthy and then yields an undefined memory scope.

`seedMemories` never throws, so the route COMPARES against a knowable expectation
and answers 502 for a short seed or an unfinished wipe rather than reporting a
success it did not earn. `forget-memories.ts` is taken from commerce's, the only
version that refuses to claim a bucket is empty without proving it.

Tests split in two: `route.test.ts` keeps the real store (and now asserts beat
5's writes are cleared), `route.memory.test.ts` mocks it for the classification
cases — `vi.mock` is hoisted per module, so one file cannot do both.
2026-08-12 03:59:55 +02:00
Maxim 6a2be764d4 feat(reskinnable-demo): wire beats 4 and 5 into Meridian's agent and pills
The agent-facing half. `showExceptionSummary` carries the three preference flags
plus the `note` slot beat 4 is graded on; `raiseShipmentWatch`, `notifyCarrier`
and `postShipmentNote` are the three writes beat 5's stored procedure fires, and
four plausible-but-useless freight actions are registered beside them so "it
picked the right three" is a claim rather than a tautology.

All three writes are `useFrontendTool`, never `useHumanInTheLoop`, and that is
load-bearing. The seeded procedure says "run all three immediately, without
asking for confirmation"; a HITL card mid-procedure opens an interrupt a
presenter moving on leaves unresolved, and the NEXT message then fails the whole
thread with "Tool result is missing for tool call …". Banking hit exactly that.
They are also registered globally, so "handle it" works from any page — a beat
whose claim is that one vague sentence was enough cannot start with "first
navigate to the Control Tower".

Prompt: EXCEPTION SUMMARIES USE THE SAVED FORMAT forces `recall_memory` before
answering anything about the shape of the queue and requires the preference to be
named in `note`; A QUIET CARRIER OR A STUCK SHIPMENT FOLLOWS A SAVED PROCEDURE
states plainly that this is a DIFFERENT procedure from getting a mitigation past
the approval authority and that the agent must not offer to record anything;
FINDING IS NOT HANDLING closes the "here is what I would do" failure; GENERAL
MEMORY pins scope "user" so a saved row cannot leak into the other five skins
sharing one backend.

The withheld gate vocabulary is untouched: no escalation code, and no paraphrase
of the mechanism, reaches `tools.tsx` or `agent.ts`. Beat 5's own vocabulary
lives in `data/handling.ts` and deliberately shares no word with it.

The two new pills sit after the 3x pills in demo order, and their comments record
what was MEASURED — the routing split from pill 1, the four lane groups, the
"$240k" formatting, and why PO-88251 is the target rather than PO-88213, which
already carries two other beats.

⚠️ Both beats are RUNTIME-CONDITIONAL. Without Intelligence there is no
`recall_memory`: beat 4 degrades to a reasonable summary with an empty band and
beat 5 to the agent asking what the planner would like done. Neither errors, and
neither is the beat.
2026-08-12 03:59:30 +02:00
Maxim 9162fa4de5 feat(reskinnable-demo): add logistics' recalled exception summary (beat 4)
Beat 4's audience conclusion is "it remembers me, and it will TELL me what it
remembered", and the second half is the one that is easy to drop. An agent that
silently obeys a recalled preference produces an answer nobody in the room can
tell apart from a normal one, so `ExceptionSummaryList` has a marked band at the
top carrying the agent's own sentence naming the preference it applied — the same
slot banking's `showSpendSummary` fills through its `note` parameter. That band
IS the beat.

Three flags, not one, for the same reason banking and commerce landed on three:
a single-clause preference reads as a coincidence. Group by lane or by carrier,
float anything already past its promised date, print exposure as whole thousands
or to the dollar.

The selection is pure and split into `data/exception-summary.ts` so the part the
preference actually CHANGES can be tested without a provider stack, and every
assertion in that suite is a DIFFERENCE between the two settings of one flag
rather than a snapshot of one setting. A preference that does not move a row is a
claim the audience has to take on faith.

Every prop on the component is optional even though the tool schema declares them
required: renders are handed streaming arguments, so this really is called with
`undefined` in every slot on the way to the real values.
2026-08-12 03:59:10 +02:00
Maxim 7f30d21815 feat(reskinnable-demo): add logistics' handling trail — watch, notice, note
Beat 5 needs about three writes the stored procedure can fire, each producing a
change the room can SEE. This is the data and UI half of that: a watch flag, a
templated carrier notice and a marked note, all landing ON the shipment record so
`GET /shipments` already returns them and both surfaces that paint a shipment —
the Control Tower board and the shipment card — paint them with no new read path
to keep in sync.

Three things are deliberate rather than incidental:

- The vocabularies live in `data/handling.ts`, a separate module from
  `data/escalation-codes.ts`, and the contrast is the point. The escalation
  catalogue is beat 6's gate vocabulary and is WITHHELD from the agent; this one
  is beat 5's procedure vocabulary and is deliberately given to it, enumerated on
  the tool schemas. Separate modules stop a future edit reaching for "the codes
  file" and importing the withheld one into `tools.tsx`. Neither vocabulary uses
  the word "escalation", because beat 5 and beat 6 are the easiest pair in this
  demo for the model to confuse and a shared word is a standing invitation.
- The 🚨 marker is forced by the STORE, not requested from the caller. A note
  that reads like every other note is invisible from the back of a room, and a
  model can phrase its way out of an instruction but not out of `markNote`.
- The actor is derived server-side from the resolved planner and the carrier is
  copied off the shipment, so neither can be whatever the model typed. Same rule
  `POST /decisions` already applies to `decidedBy`.

New routes rather than widening `PATCH /shipments/[id]`: that PATCH is an
allow-list of `status`/`etaCurrent` precisely because any writable field feeding
the mitigate endpoint's pricing is a side channel around the authority gate.

`store.reset()` needs no new line — the three fields live on the shipment and
`seed.json` carries none of them — but the test asserts it anyway, because a
board that opens with last run's watch flag already on PO-88251 makes the stored
procedure look like it ran before anyone asked.
2026-08-12 03:58:49 +02:00
Maxim 5e4c5d8bfa fix(reskinnable-demo): resolve the rate-brief carrier by identity, not spelling
The carrier-scoped settlement I added in the last commit compared the
model-supplied carrier with ===, and the model reads that name off a PDF whose
masthead is carrier.toUpperCase(). "PACIFIC STAR LINE" is a plausible and
CORRECT read that resolved to no lanes — so every row fell into the no-match
branch, every prior rate was stripped, the card labelled the whole sheet "new
lane", and the agent announced that lanes the network has carried for years are
new service. A regression introduced by the fix: matching used to be
carrier-independent, so case had no effect on settlement at all.

lanesServedBy now compares canonicalized names, alongside findCarrier (the
network's own spelling) and carriersOnFile. The route stores the network's
spelling, so an artifact filed from a shouting masthead is still titled
"Pacific Star Line", and GET /rate-sheet resolves the same way — both beat-3d
routes now agree on WHO the carrier is as well as on what it serves.

An unknown carrier is REFUSED (422 UNKNOWN_CARRIER, naming the carriers on
file) rather than settled against nothing: a silent total-strip files an
artifact that contradicts the document on every row, which is the worst
outcome available here and the one the settlement exists to prevent. It also
removes the asymmetry where the read route answered a loud 404 and the write
route degraded quietly.

Also re-points the 0-folding test. It sat on a lane the carrier does not serve,
where the no-match branch strips the value regardless, so reverting optionalRate
left it green. The fold is only decisive in the AMBIGUOUS branch (the one that
preserves the model's value) and in whether the lane is reported in
noPriorRateOnFile — there are now two tests, one on each, and reverting
optionalRate fails both.

Verified by mutation: an unnormalized comparison fails the mis-cased-carrier
test; a bare requireRate fails both fold tests. Confirmed live over REST —
?carrier=pacific%20star%20line returns the same 200 PDF, POST with "PACIFIC
STAR LINE" settles SHA-LAX to $0.45 and leaves SHA-OAK with no prior rate, and
an unknown carrier answers 422 listing the four on file.

Skill staleness (CLAUDE.md standing rule): checked, no impact. This is a
per-skin data-resolution fix; demo-beats.md's beat-3d guidance already says to
scope the match by whatever the document is a statement about, and says nothing
about how a name is compared.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 02:55:42 +02:00
Maxim d1c431b57f fix(reskinnable-demo): settle prior rates and move the PDF wrap to the shell
Review found the beat's two guards each closed half a property.

SETTLEMENT. POST /briefs screened only the direction observed live — a prior
rate claimed on a lane the network does not carry. The mirror is the same lie
on the same row: OMIT the field for a lane the carrier does serve and the card
labels it "new lane", telling the room the network has never carried a lane it
carries. A wrong value stored verbatim renders "down 94.8%" under a document
printing "up 15.6%". The route now SETTLES every row against that carrier's own
lanes: overwrite from the ledger on a unique match, drop when there is no match,
and leave the model's reading standing only where the app genuinely cannot tell
which lane the sheet meant — reporting that case too. `??` was rejected: it
repairs the omission and stores the wrong value.

Scoping by carrier is what removed the ambiguity that justified settling one
direction only (SHA-LAX ocean is two lanes network-wide, one per carrier), and
it is the more honest reading anyway: a rate another carrier gets is not a rate
we hold with this one. `lanesServedBy` moves to the store so the route that
BUILDS the document and the route that files the brief cannot disagree.

WRAPPING. `wrapProse` was a shell concern sitting in a skin: computed entirely
from PDF_METRICS, fixing a property of buildPdf, and duplicated-in-waiting by
commerce and people, which emit derived prose too. Worse, it measured the RAW
string while the writer draws pdfEscape(toAscii(text)), so a sentence near the
budget wrapped wrong. buildPdf now wraps non-mono lines itself, measuring the
escaped-and-folded form; mono lines stay exempt, since wrapping a columnar line
would break the alignment it exists to keep. Commerce's 89-character summary was
running off the page and now wraps — its test read drawn lines as sentences and
now rejoins continuations.

Also: the correction sentence agrees its verb with the count (it is read aloud),
and a prior rate of 0 folds to absent at the door, since a stored 0 was read
three ways — "new lane" by the card, `0` by the readable, "$0.00 -> $0.49" by
the agent.

Verified by mutation: removing the settlement fails five tests, weakening it to
`??` fails exactly the wrong-value one, and measuring the raw string fails the
escaping test. Re-rendered both documents from a live server — no drawn line
passes the right margin, every byte < 0x80.

Skill staleness (CLAUDE.md standing rule): yes, updated here. demo-beats.md's
trap 3 becomes "handled for you, for prose only" with the escaping reason the
wrap cannot live in a skin, and the artifact rule now names all three directions
plus the scope-the-match lesson. Still deferred to the once-per-phase matrix
pass: CLAUDE.md's beat matrix and the closing "logistics hits one beat of nine"
paragraph.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 02:38:46 +02:00
Maxim 318c56479c feat(reskinnable-demo): ingest a rate sheet into a durable brief (beat 3d)
A generated carrier rate sheet rides in as a real attachment and its lane
rates land in a RateBrief filed to the store, listed on the Decision Log —
deleting the thread leaves it in place, because it belongs to the app.

Named a rate brief, and stored separately from both existing "brief"
meanings: renderBrief's a2ui canvas surface is a RENDER that dies with the
thread, and Decision is a mitigation on one shipment (no shipmentId, no
mitigation kind and no single cost fits an ingested sheet).

The document is content over @/shell/documents, so the ASCII fold, the
Courier columns and the /Length and xref arithmetic come from the shell.
What is this skin's own is the content: every sentence is derived from the
rows — a flat lane gets none — and the one invented row is keyed by carrier,
re-checked against the live network, and dropped rather than misattributed.

Two properties the primitive cannot own, both found by rendering the page:
buildPdf never wraps, so derived prose is wrapped here against PDF_METRICS
and asserted on the emitted bytes; and an optional prior-rate field gets
filled by the model anyway — the first live run copied the quoted rate onto
the one lane the sheet prints as new, so the artifact said "flat" under a
document that says there is no prior rate. POST /briefs now settles that
field itself (absence of the lane IS the answer) and returns what it
stripped, so the agent narrates the correction instead of being overruled.

Staging is a thin wrapper over @/shell/attach, so the fifteen detection
causes and the abort-on-failure rule are shared rather than copied a fourth
time.

Skill staleness (CLAUDE.md standing rule): yes, and updated here.
demo-beats.md gains a third PDF trap (no wrapping), the artifact-must-not-
contradict-the-document rule with the server-settles-ledger-facts fix, and
the wrapper/builder lists now name logistics. Deliberately NOT updated: the
beat matrix in CLAUDE.md and demo-beats.md's "do not use logistics as a
demo-completeness reference" paragraph — tasks 8-12 left both as a
once-per-phase edit, and this is the last logistics beat in the phase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 02:13:15 +02:00
Maxim 62ef1db8c4 fix(reskinnable-demo): correct the PIN card's review findings (beat 3a)
Four fixes from Task 12's review, none behavioural on the happy path.

The coherence test pinned the half the card does not print. It tied the
predicate to `length` — which the card uses only as maxLength — while the
text the planner READS is `hint`, related to PIN_LENGTH by nothing. A hint
saying "4-digit" beside length 6 kept the suite green while the card
advertised a format it refuses, which is precisely what that design exists
to prevent. Now asserted; mutating the hint to say 4 fails that test and
only that test.

The route claimed a security property it does not have. A comment said the
refusal must not "leak whether a well-formed PIN would have matched some
other planner's" — there is no matching of any kind, so 111111 authorizes
as readily as anything else. The clause is gone and the header now says
out loud that validity is FORMAT-ONLY by design, that INVALID_PIN means
"not six digits" rather than "wrong PIN", and that the real control on
this route is checkAuthority(). A stage demo may skip the secret; code may
not imply one it does not implement.

The card could strand itself on "Authorizing…". `submitting` was set
before the write and cleared only on the failure paths, on the assumption
that respond() unmounts the card — the caller's behaviour, not this
component's guarantee. It is a three-state `phase` now, with a settled
"Authorized" state carrying its own copy, so nothing sits disabled and
unexplained and the write still cannot be issued twice.

The PIN is checked BEFORE the 404 and 422. Those are answers — which
shipments exist, which mitigations they carry — and an unauthenticated
caller was getting them for free. A test holds the ordering, since it is
invisible in normal use.

Skill staleness: checked, no impact. failure-modes.md § 12 (added in
f9b78ba29b) describes the authority separation, which is unchanged here;
none of these touch the contract, registration, routing or any gate a skin
author must pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 01:41:39 +02:00
Maxim f9b78ba29b feat(reskinnable-demo): add logistics' planner-PIN authorization (beat 3a)
An agent-initiated mutation whose sensitive payload the agent never sees:
the planner types their PIN into a card in the chat, it POSTs straight to
REST, and respond() carries only a confirmation sentence.

One helper returns both the guidance the card prints and the predicate its
button compares against, so the card cannot advertise a format it refuses.
It refuses an unreadable value rather than stripping characters — the typed
value IS the write here, so a lenient parser would turn "-482913" into a
real authorization — and reports "nothing typed yet" separately so an
untouched field is not scolded.

THE PIN IS A SECOND FACTOR, NOT AN AUTHORITY OVERRIDE. It confirms who is
acting, never how much they may spend. The card offers only the cheapest
option under the planner's authority (and > $0, because absorb is always
free), says "this needs an escalation, not a PIN" when none qualifies, and
takes no kind/amount argument from the agent — and the route recomputes the
cost and runs the very same checkAuthority() the ordinary write runs. A
valid PIN on an over-authority option is still refused with OVER_AUTHORITY.
Without that, the PIN would be a second unlock path around beat 6's
escalation gate: the agent routes around the gate, the teach arc never
fires, and nothing fails. route.test.ts pins it; deleting the check turns
that one test red and leaves the other 1208 green.

Verified live in the inspector (OSS SSE mode): the tool call streams
{"shipment":"PO-88213"} and the recorded tool result is exactly "reroute
authorized on PO-88213 at $572. The PIN stayed in the card and was never
sent to you." The digits appear in no AG-UI event and in no run body —
only in the POST to /api/logistics/v1/authorizations. The write landed:
shp-4821 is resolved, reroute, $572, with a Decision Log record.

Beyond the brief's file list: a "Release the reroute" pill (the beat's own
entry point), a toolLabels entry, and an exported notifyDataChanged() so
the board catches up after a write that bypasses actions.ts. CLAUDE.md's
beat matrix still shows logistics' 3a as ❌ — as it does for beats 2, 3b
and 3c, which tasks 8-11 also left for a single docs pass at the end of
the phase.

Skill staleness (CLAUDE.md standing rule): checked, and the skill DID need
an update. failure-modes.md gains § 12 — "a second factor is not an
authority override" — because a PIN that silently becomes an authority
bypass is exactly this app's characteristic bug (a control that works
perfectly and quietly kills an adjacent beat), and beats 3a and 6 sit on
the same write in every skin that has both. § 10 now points at it. The
contract, registration, routing and lint/test gates are untouched, so no
other part of the skill moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 01:30:43 +02:00