The blocks route no longer relies on its zod enum to stop a TypeError:
addBlockToDashboard goes through requireDashboard, which throws a coded
NOT_FOUND that storeErrorResponse maps to 404. The enum's job is now the 400
that names the allowed values.
ReportCanvas's null branch is NOT exercised by bookstore and exec omitting
CanvasSurface: bookstore emits no a2ui-surface at all and exec's are all
block:-prefixed, which useLatestCanvasSurface skips. Both comments now match
CLAUDE.md's wording.
publishPack's EMPTY_DASHBOARD (422) and NOT_FOUND (404) arms carry a human
message; the packs route and publish_board_pack spread only code and breaches,
so the receipt fell back to spelling the enum as words. EMPTY_DASHBOARD has no
phrasing of its own in REFUSAL_PHRASES, so that message is its whole
explanation. BAD_COUNTERSIGN still answers { error } and nothing else.
A2UIProvider.processMessages never throws: it catches the processor error,
console.warns it, records the message in its error state and returns void,
bumping the store version counter only when the op list applied. Both block
surfaces wrapped the call in try/catch and latched the ops hash in the
success path, so the catch was dead code and the hash latched on REJECTED
lists too - the next snapshot carrying that list was skipped as a duplicate
and the card stayed blank forever.
Both call sites now record the version before calling and latch on a later
render only if it advanced. The exec grid additionally moves its A2UIProvider
from the grid down to each BlockCard: a shared provider has one error slot
(any sibling's success cleared the failing block's banner) and one version
counter (a sibling's success would read as this block's rejection having
applied). The failure line now names the block from the grid's own context,
since only 2 of the processor's 6 rejection messages name a surface.
askCopilot swallowed a rejected runAgent to the console, leaving the user turn it had just posted unanswered with no on-screen signal; it now also raises a dismissible error line in the layout. The presenter reset never logged and reported the route's 403 FORBIDDEN gate as a generic 'Reset failed', which reads as a broken reset rather than a disabled flag; it now logs and names the refusal, tolerantly matched on the body's error or the status in the message so a contract change degrades to the generic arm. The OGUI design brief told the model to read the host's --surface/--canvas custom properties, which never resolve inside the sandbox iframe (bare CSS reset, no app stylesheet); it now inlines literal hsl values, as commerce's and bookstore's briefs do. Adds a shell test pinning every id in the client SkinRegistry to a matching entry in the server agentRegistry and back, derived from both exported structures - a miss there 404s every run of a skin that otherwise renders perfectly, with nothing in tsc, lint or the neighbouring config suite to catch it. Pins the previously unguarded catalog, sandboxFunctions and toolLabels ride-alongs, and clears the shared module spies between cases.
CopilotKitProvider compares its array props by reference against the first
render and console.errors on any change, ungated by NODE_ENV. The layout
passed sandboxFunctions: skin.sandboxFunctions ?? [] — a fresh literal on
every render — so airline and bookstore (the skins that contribute none)
logged that error on every render after the first. The empty array is now a
module-level constant, and the a2ui / openGenerativeUI config objects are
memoized so the provider stops rebuilding its activity-renderer list each
render.
The report handoff pill also stopped claiming rendered on the canvas for
content the canvas will not claim; that arm now says a surface was generated
without promising where. The block: dispatch contract is unchanged.
The canvas claimed any a2ui-surface activity whose content blockSurfaceIdFrom
could not read — malformed content, a stringified envelope, a missing ops
array — which blanked the page behind a bare "Back" for a skin with no
CanvasSurface: the exact failure the old comment claimed to prevent.
Classification is now explicit and biased against the canvas: scan every op
for a surfaceId, parse a stringified envelope only far enough to recognise a
block: tile, and treat anything unreadable as skip rather than report.
Id-less surfaces get a position-derived fallback id instead of a shared
constant, so dismissing one no longer suppresses every later one, and an
id-less OGUI activity claims the canvas instead of discarding an earlier
surface. agent.messages is narrowed rather than cast, warning once on drift.
The dateline was period close + 5 days unconditionally, so on the 1st-4th
of every month it printed a date that had not happened yet (probed on
2026-09-04: "5 September 2026"). Clamp it to today, keeping the calendar-day
arithmetic that survives a DST fallback.
Also unifies the memo on one locale (en-US dates alongside its en-US
currency), makes the exported builder refuse a non-overrun instead of
Math.abs-ing the sign away, fixes a TZ teardown that wrote the literal string
"undefined" into the environment for every later test, and covers the two
untested route arms.
Exec ships two sets of token values (it is dark-capable) and had no contrast
guard, so six text pairs sat under 4.5:1: light --brand on --brand-soft at
2.71:1, light --positive at 3.55:1 on its soft and 3.90:1 on the card, dark
--negative at 4.31:1 on the card and 3.82:1 on its banner, and dark
--brand-violet at 3.91:1 on --brand-soft.
The brand pair could not be nudged: under a deep --brand-foreground the two
pairs --brand anchors pull in opposite directions and 4.5:1 on both is
arithmetically impossible. The light block now takes the shape every sibling
uses -- a deep brass fill under a white label -- and the dark block re-values
both sides.
Adds src/skins/exec/theme.test.ts, ported from the commerce guard: it parses
the shipped declarations, composites alpha tints before measuring, and derives
each pair's render sites by grepping for the class pair, so a citation cannot
rot and a pair that stops rendering fails instead of looking like coverage.
Also corrects the dark block's comment, which claimed only --brand and
--brand-soft were re-valued while the block already moved --brand-indigo and
--brand-violet too; the claim is now asserted rather than written.
normalizePeriodLever computed a trim for its blank test and returned the
untrimmed raw, so ?period=%202026-02 narrowed every row away under a tinted
control and added a phantom duplicate option. It now returns the trimmed
value, validates the YYYY-MM shape, and treats the 'any' sentinel
(case-insensitive) as no lever — and execNavTarget normalizes through the
SAME functions, so neither end of the URL can emit a lever the page ignores.
Top-N's comparator subtracted magnitudes directly: a zero-planned point's
Infinity led the list the table renders as '— n/a', and its NaN made the
comparator return NaN, leaving the surviving rows up to the engine.
Non-finite variances now rank last, deterministically.
formatMetricValue had no default arm, so an unhandled unit rendered a blank
plan and actual; it now prints the raw value and shouts. The department
vocabulary is one exported list in metric-rows rather than two hand-copied
ones, and the beat-3b readable carries each row's on-screen variance string
beside the raw fraction (keel's deriveRegisterKpiTiles rule).
The fixed strip narrowed to the CEO audience while the seeded exceptionList block on the same page carries no audience and lists every breach, so the page showed zero exceptions above and three below, and the readable told the agent exceptions: [] about a screen showing three. The strip is the COMPANY's exception feed — that is why the CFO page deliberately has none — and store.ts's publish gate already treats that block as covering every metric, which is what makes the CEO pack refuse on dsoDays. The derivation is now one exported function the strip renders and the readable reports. Tests cover the strip/block agreement over the seeded ledger, the def-less drop rule and the drill-in href, and drop the impossible dsoDays/distribution fixture.
ExceptionList printed breach rows through the sign-coloured Delta, so a positive-variance breach rendered green beside the CEO strip's red; it now colours by breach status via Delta tone=alert. Its empty state claimed 'no variances awaiting explanation' about a list that also carries explained rows, and printed that same no-variance line for a ledger with points but no metric defs — no-data now reports through the block-error surface, and the wording says what it means. VarianceBar renders a department missing from the latest period as an explicit unavailable row instead of dropping it and silently rescaling every other bar. Heading/Text report an unresolved {path} ref instead of painting it as an empty label. Adds the missing coverage: TrendLine (window narrowing, default window, latest-period footer, one-point arm), InitiativeTable, and a pin control that must lose its buttons when it flips to Pinned.
burnRate and dsoDays were tagged cfo alongside opex, so all three seeded breaches were CFO-only and every CEO-scoped exception view came up empty on a ledger carrying three live breaches. Burn is the runway conversation and DSO is the CEO's own collections initiative; opex stays CFO-grade. Also documents the two unreachable catalog surfaces honestly: ExceptionList.audience (block specs carry no audience, so every pinned list runs at the default 'both') and Text (nothing emits it).
assertValidBlockSpec now validates months as a positive integer, metricId against the catalog's own enum, and rejects kind-inapplicable props (KIND_PROP_UNSUPPORTED) that buildKindComponent used to drop in silence. The kind set derives from a satisfies Record<BlockKind, ...> table, so a new kind fails to compile instead of being rejected at runtime, and extractSurfaceId searches all three op containers rather than short-circuiting on a present-but-empty one.
A dashboard with no metric-bound and no exceptionList block made
`referencedMetrics` yield `includesAll: false` and an empty metric set,
so `publishPack`'s breach filter matched nothing and an empty CFO board
published `ok: true` with three unexplained breaches on the ledger —
reachable from the grid's per-block Remove buttons, and it erased the
demo's climactic 422 refusal. It now refuses under its own returned
`EMPTY_DASHBOARD` code (422), which the packs route and the agent tool
already forward verbatim off the discriminated result.
Also in the store: `months` only narrows for a positive finite value
(0 meant "all 24 periods", -3 sliced the OLDEST 21 — an inverted
window); unknown dashboard ids throw the mapped `NOT_FOUND` instead of
a raw TypeError; `pack.narrativeIds` carries one id per
(metricId, period); and the seed-window / drafts-retention lifetimes are
documented as deliberate.
In `store-errors.ts`: a non-Error throw carrying a coded message now
maps, where `String(error)` prefixed "Error: " (cross-realm) or gave
"[object Object]" (plain object) and turned a 404 into a re-thrown 500.
Three comment claims corrected — the Map's real failure mode is a
truthy prototype function reaching `Response.json` as a RangeError, not
a silent 200; commerce's `CODES` holds `{status, message}`, a different
shape; and the table's scope excludes `build-block-ops`' codes and
`publishPack`'s returned refusals.
Tests: window contract, per-dashboard gate scoping, pack blockIds and
narrativeIds, a real "down" swap, and a variance assertion pinned to
hand-computed numbers so a denominator swap no longer survives.
fileNarrative dropped the narratives route's field-specific message for a
bare status; publishPack parsed both response bodies unguarded, so a
non-JSON 500 rejected with a SyntaxError and a body without an error
field settled the publish card with undefined; resetDemo keyed its
best-effort refresh on a reset array that a bodiless 500 after
store.reset() cannot carry; the first-load Retry button had no in-flight
state, so a retry failing with an identical message repainted nothing;
and refresh captured loaded in its closure, churning the context memo
and able to misroute a late failure to the first-load panel.
Empty settles no longer claim a card was never answered, refusals relay the producer's own message instead of a bare enum code, render_metric_block's error-shaped results render as refusals rather than a checkmarked chip, and the ledger refresh after a filing is gated on a settle that actually wrote. Exact renderers show an in-flight line now that they take the tool off the shell's spinner chip, the HITL abort sentinel is matched tolerantly and pinned by a drift test against the installed packages, and the countersign card's PIN hint, radio group and dark-theme contrast are fixed.
Validate `code` outside the zod schema so no zod issue about it can exist, and answer a bad code with the same BAD_CODE refusal agent.ts uses. Replaces the `options`-stripping replacer, whose comment claimed a guarantee it never had: zod 3.25's invalid_enum_value repeats the whole option list inside the issue `message`, which the replacer passed through untouched. Also gives the mirrored metricId list compile-time parity with the MetricId union, trims `code`, and covers the leak, metricId, source-default and malformed-payload paths in tests.
file_variance_narrative filed cleanly against a (metricId, period) the ledger
holds no row for — a green "filed" that cleared nothing while the publish gate
went on refusing with nothing to connect the two. It now refuses that as a
correctable error result (never a throw: the runtime has no tool-error arm),
and a filing against a real but non-breaching period still stands while SAYING
it cleared no open breach. `code` is trimmed before it is judged, so a code
copied verbatim with its stray whitespace is no longer refused BAD_CODE on a
path prompt rule 6 forbids retrying.
render_metric_block accepted compare on a trendLine, months on a metricTile and
department on a varianceBar, dropped them in buildKindComponent and persisted
the phantom into the stored spec; it now names the unsupported (kind, prop)
pair back to the model instead.
EXEC_PROMPT lists the memory tools three numbered rules already tell the model
to call, and rule 10 no longer sends the compliant path to an unsorted board:
a worst/biggest ask passes a real top-N, which is what the beat-3c pill's
"biggest one at the top" promises.
Tests: the BAD_CODE refusal path had none; the withheld codes are asserted
absent from refusal RESULTS, not just the prompt, and from every pill.
Every render in the exec skin's `tools.tsx` branched on `typeof result ===
"string"`, which says a tool call SETTLED and nothing about how. So a
cancelled countersign, an interrupt the run aborted under
("Error: Human-in-the-loop interaction aborted"), and any error-prefixed
relay all drew the same green receipt as a real publish — a board pack
reported as published that never was, identically on every replay.
Adds one shared classifier (`classifyToolSettle`) plus the single place
that draws its result (`SettleReceipt`), and routes all seven renders
through them:
- pending (still streaming) renders nothing, so HITL cards still fall
through to their own card;
- a cancel, an abort or an empty `respond()` renders as a NEUTRAL
`StatusNote` naming what did not happen, never as a receipt;
- an error-prefixed or known-failure-prefix settle renders negative
(fixes `pinBlockToDashboard` and `navigateTo`, which read
"Error: …" as a pin/navigation that happened);
- the gate refusal keeps its existing rendering, with the breach list
now built from a GUARDED shape — `breaches: [null]` used to throw
`Cannot read properties of null` inside the transcript render, taking
the chat column down mid-demo;
- `awaitDemonstration` no longer asserts "Recorded the demonstration."
when the recorder's step count is unreadable or zero, and the teach
chain's offer/save renders no longer report a choice ("Left it for
now", "already answered") that an abort made for nobody;
- `NarrativeFiledReceipt` (found in the full-file audit) shares the same
bug and now shares the classifier too.
The per-tool vocabularies (a card's own cancel sentence, a handler's own
failure prefix) live in `EXEC_SETTLES`, declared beside the code that
produces them rather than sniffed for with a regex — the countersign
Cancel button now settles with the same `PUBLISH_CANCELLED` constant the
classifier recognises.
Red-green: `src/skins/exec/tool-settle.test.tsx` (27 cases) went 14-red
against the old per-render checks — abort sentinel and cancel rendering
positive, the malformed refusal body throwing, the unreadable step count
claiming a recording — and green after.
Compare seeded memory count against the knowable expected count and 502
instead of ok:true when it falls short (an all-failed seed used to report
success). Redact apiUrl/memoryError with @/lib/redact-secrets, which can
otherwise carry the bearer key or backend address into the response body.
forgetAllMemories now throws a ForgetMemoriesError carrying the partial
forgot count instead of discarding it (9-of-10 deleted used to report 0).
Narrow SeedMemory.scope to "user" only, since forget-memories never deletes
project-scoped rows. In the client, resetDemo best-effort refreshes before
throwing on a non-OK reset so the UI reflects the store reset that DID
happen, and the layout alert explains a memory-only failure instead of a
generic "Reset failed".
skippedProjectScoped stays a per-bucket max rather than a sum: project scope
is global to the backend instance (documented in forget-memories.ts and
user-id.ts, and verified against the running stack), so every bucket's list
call re-sees the same rows and summing would overstate the count N-fold —
the same reasoning already encoded in keel's and commerce's dev/reset.
Seven findings from the exec-skin test review, each one a test that could
not fail against the implementation it was pointed at. Mutation-verified:
every assertion added or changed here was run against a deliberately broken
implementation and observed to fail.
- metrics-explorer: the top-N fixture was listed in descending |variance|,
so `slice(0, 5)` returned the expected five in the expected order with the
`.sort()` deleted. Shuffled, and the |variance| sequence itself is now
asserted (distinct values, strict descending).
- catalog/renderers: the VarianceBar period test asserted a row COUNT, which
the fixed DEPARTMENTS walk bounds to 4 whether or not the renderer narrows
by period — it now asserts the rendered FIGURES come from the latest of two
periods. Adds MissingTile's block-error surface, Delta's non-finite guard
(Infinity, NaN, and the forecast path), and ExceptionList's audience
filtering, period window and def requirement.
- nav-target: the `period` lever had no assertion at all; it now round-trips
into the query string and is asserted omitted when unset.
- sandbox-functions: the DTO-allowlist `toEqual`s passed for a handler that
returned raw domain rows, because the fixture had nothing to drop. The
fixture rows now carry a field the projection must strip. Adds the
`department: "all"` path, and widens the handlers' `department` annotations
to `Department | "all"` — the value the zod enum already admits and the
schema's own description tells the model to pass.
- budget-memo-pdf: the printed `variancePct` was unasserted.
- agent-tools: pins `publish_board_pack` as exported under that name AND
absent from execAgent()'s registered roster.
- skin: the chatHeaderActions test never invoked `onClick`; it now invokes it
and asserts `attachMemoByHand` ran.
Metrics Explorer: show an out-of-vocabulary `top` (e.g. an agent's `?top=7`)
as a labelled select option instead of rendering blank; restate the
VALIDATED department (dropping an unrecognised one) rather than the raw
query string on every lever push; treat an empty/whitespace `period` as
absent rather than a filter that excludes every row, in both
filterMetricRows and the page's own tint/option logic; preserve query
params outside the four levers on writeback instead of dropping them.
CEO dashboard: color every exception-feed card with the alert treatment
regardless of variance sign — every card is already a breach by
construction, so an over-plan breach no longer renders green while the
Metrics Explorer colors the identical number red.
ExecLedgerProvider used to wave a failed first fetch through with the EMPTY
snapshot (children mounted over it, indistinguishable from a real empty
demo) and refresh() swallowed every post-load error silently, so mutations
awaiting it reported success while the screen went stale. Now a failed first
load renders a loud role=alert panel with a retry instead of children, and a
failed refresh after a good load surfaces a dismissible role=alert banner
("saved, but the view may be stale: ...") while keeping the last good
snapshot on screen.
Also: block-data.tsx's no-provider addBlock fallback now rejects instead of
resolving silently, so AddToDashboard's existing error path fires instead of
showing a false "Pinned check-mark" outside the provider. And corrected the
PublishPackResult doc comment: pack?: never does not make status === 200
alone narrow the union (status: number overlaps 200) — the "pack" in result
check is the real discriminator; pack?: never only makes that check
type-safe.
buildKindComponent forwarded metricId unguarded for metricTile/trendLine/
varianceBar, so a spec missing it silently reached the client as
metricId: undefined on a REQUIRED catalog prop. The agent's execute
guard catches this on the agent's own path, but buildBlockOps is also
called from the ledger GET route rebuilding ops for every pinned block,
and store.createDraftBlock stored specs verbatim with no validation at
all.
Add assertValidBlockSpec (build-block-ops.ts), shared by buildBlockOps
and createDraftBlock, throwing METRIC_ID_REQUIRED for a metric-bound
kind missing metricId and UNKNOWN_BLOCK_KIND for a kind outside
BlockKind — so an invalid spec can never produce ops or be stored, even
via a caller other than the agent tool. Also add an exhaustive default
to buildKindComponent's switch (never-check) as a second, independent
guard. agent.ts now imports METRIC_BOUND_KINDS from build-block-ops.ts
instead of duplicating it.
CLAUDE.md's intro roster line omitted exec, the contract table quantified
Providers/CanvasSurface/sandboxFunctions/RuntimeProviders universals that no
longer hold since bookstore/exec joined the roster, the a2ui-canvas
"currently unexercised" claim was inverted by exec's omission, and the
demo-beat matrix's separator row was malformed under the exec column.
README's beat-first/raised-afterwards grouping and its per-skin subsections
likewise didn't account for exec. Reworded the contract-table cells to
derive from their own cited greps instead of asserting counts, and extended
skin-roster-docs.test.ts's EXHAUSTIVE_CUE with "registered roster" so the
intro line's roster enumeration is drift-guarded going forward.
The beat-2 no-restricted-syntax block covered logistics, airline, and
keel .tsx files but omitted src/skins/exec/**/*.tsx, so only
exec/tools.tsx and exec/agent.ts got statusKeyedTerminalRender (via
the beat-6 block) while every other exec file — pages, renderers,
components — would lint clean on a status-keyed terminal render.
exec currently has zero ToolCallStatus references, so the widening is
free today.
Add the exec glob to the beat-2 block and pin the resolved selector
set for a representative non-agent-facing exec file
(src/skins/exec/catalog/renderers.tsx) in skins-config.test.ts,
mirroring the existing logistics/pages/control-tower.tsx row.
Six adjudicated CR findings on the Vantage (exec) skin's agent, all reachable
from `src/skins/exec/agent.ts`.
- render_metric_block's metricId requirement moves out of the zod schema and
into `execute`. A parameter-validation failure surfaces as an AI-SDK
`tool-error` stream part, and @copilotkit/runtime's event translation has no
arm for one, so no TOOL_CALL_RESULT was ever emitted: the transcript's chip
spun InProgress forever. Returned as an `{ error, message }` RESULT the model
reads it and retries, the same way `file_variance_narrative` handles a bad
code.
- execAgent now sets `maxSteps: 12`. The runtime only passes `stopWhen` when
`maxSteps` is given (`dist/agent/index.mjs:479`) and the AI SDK defaults to
`stepCountIs(1)`, so the run ended on the first backend tool result — no
prose, and no multi-step arc could chain. Verified safe with this skin's
four `useHumanInTheLoop` cards: those are frontend tools with no server-side
`execute`, and the SDK's loop stops on an unanswered client tool call
regardless of `stopWhen`. The `maxSteps: 1` caveat in the runtime's types
applies only to `defineTool({ interrupt: true })`, which exec does not use.
- New EXEC_PROMPT rule 4: render blocks one per turn. Two parallel
render_metric_block calls collide on one a2ui surface upstream and the second
replaces the first. Rules 4-12 renumbered to 5-13, cross-references updated.
- Rule 10 (was 9) now states navigateTo's sentinel contract ("any"/0/false)
instead of telling the model to leave levers unset, which the
required-with-sentinel schema does not allow.
- Rule 6 (was 5) distinguishes inventing a code from being given one, and
admits the two beats that depend on it: a code the operator stated, one a
saved procedure names, or one an attached document determines unambiguously
(quoting the evidence). Beat 3a's pill is reworded to ask for the code rather
than imply the agent has one; no literal code appears in it.
- file_variance_narrative writes to the store from the backend, so nothing
re-read the ledger afterwards and the Board Packs list stayed stale.
`ExecTools` registers an exact tool-call renderer for it that refreshes once
per completed call id and leaves a receipt — the mechanism banking already
uses for its remotely-executed report tool.
The Vantage ledger's block mutators lied in three ways that all pointed the
same direction — a failed write looking like a successful one.
`removeBlock` DESTROYED the block instead of returning it to `drafts`. The
chat transcript keeps the block's "Add to dashboard" control mounted after an
unpin, and the agent keeps holding the same `blockId`, so both went on to
revive a dead id and get 404 "no draft block" — the exact confusion the
store's own doc comment says the drafts/dashboard split prevents. Unpin now
puts the block back in `drafts`, so a re-pin to either dashboard just works.
`addBlockToDashboard` answered NOT_FOUND ("no draft — render it again") for a
block already pinned to the OTHER dashboard. That advice is wrong and the
agent obeys it, producing a duplicate block. It now throws ALREADY_PINNED
naming the holding dashboard, which the POST route maps to 409. Single-home
stays; multi-homing is out of scope.
`removeBlock`/`moveBlock` silently no-oped on an unknown blockId and the
DELETE/PATCH routes answered 200 with the untouched block list — a failed
unpin or move was indistinguishable from a successful one, and inconsistent
with the POST on the same resource, which already 404'd. Both now throw the
NOT_FOUND-prefixed shape and the routes translate it, via one shared
`storeErrorResponse` table so a code cannot be handled by some verbs and not
others. A move that would fall off either END is deliberately still a silent
success: the block exists and the order already is what was asked for.
Ripples:
- `ledger-context` surfaces the server's `message` on remove/move, not just a
bare status, through one `throwWithBodyMessage` helper `addBlock` now shares.
`dashboard-grid` already renders those loudly (verified).
- Each code's remedy is written ONCE, beside its throw, because every relay
above forwards the message verbatim. `pinBlockToDashboard`'s failure arm no
longer appends a blanket "Render the block first", which was actively
harmful for ALREADY_PINNED.
- `AddToDashboard` drops its local sticky `pinned` flag and reads `isPinned`
only. The flag never cleared, so after an unpin the control sat on
"Pinned ✓" over a block on no dashboard, with no way back.
Tests, all red first: store round-trips remove→re-pin (draft and seeded
blocks), ALREADY_PINNED vs NOT_FOUND with each message's own remedy, throw on
unknown remove/move, boundary move still silent; route DELETE/PATCH 404 on an
unknown or wrong-dashboard blockId, POST 409 ALREADY_PINNED, and the
unpin→re-pin round trip over HTTP. The AddToDashboard renderer test's fake
`BlockData` was rewritten to derive `isPinned` from the set `addBlock` writes
— the old frozen `isPinned: () => false` was a state the app cannot be in, and
testing against it is what let the sticky flag survive.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shell's renderActivityMessages array shadows CopilotKit's built-in
a2ui-surface renderer, and that built-in renderer's ReactSurfaceHost is the
only thing that mounts an A2UIProvider. CopilotKitProvider's a2ui.catalog prop
mounts none of its own, so the inline block card's useA2UIActions() threw
unconditionally and, with no error boundary above it, took the page down on the
first block: activity.
InlineBlockSurface now mounts its own A2UIProvider with useSkin().catalog (the
same object the layout hands the provider, reached through the shell's skin
contract rather than an import from src/skins/), matching how every skin canvas
surface and the exec dashboard grid already own their provider.
Both SurfaceMessageProcessor copies also latched the op-list hash BEFORE
processing, so a rejected op list was skipped as a duplicate forever; the latch
now advances only after a successful processMessages, and the provider's
useA2UIError() is rendered as a loud alert instead of an empty card.
opex/headcountCost rendered two granularities that could contradict each
other on stage: `department: "all"` was its own PRNG draw instead of the
sum of the four department rows, so the Metrics Explorer could show, e.g.,
opex "all" actual down vs plan while the four departments summed up vs
plan for the same metric and period.
`all` is now derived as the sum of the department plan/actual/forecast
rows. The opex/distribution breach override is re-summed into `all`
afterward so the two stay in agreement, and stays a non-breach by
construction (distribution is 30% of opex, so the worst-case recomputed
`all` variance is bounded well under opex's threshold). The seeded breach
set is unchanged: {opex/distribution, dsoDays/all, burnRate/all}.
Adds a store.test.ts invariant that all.actual/plan/forecast equal the
summed department rows for every period, plus a test hardening the exact
breach set.
budget-memo/route.ts computed the memo dateline by adding milliseconds to a
local-midnight Date, which silently loses a calendar day when the window
crosses a DST fallback (e.g. a 2026-10 close prints 4 November instead of 5
in America/New_York). Switched to calendar-day arithmetic.
The memo's prose is written only for an overrun; isBreach trips on variance
magnitude, so a future under-plan breach would print a nonsensical negative
overrun and invert the VAR-TIMING driver cue. Added a guard that 404s an
under-plan breach exactly like a missing one, with a warning documenting why.
Also aligned the memo's author name ("Priya Anand") to the seed's initiative
owner ("Priya Nair" in seed.ts), whose note already ties that initiative to
this same opex overrun — two near-identical names read as an error on stage.
The narratives route's 400 body echoed zod's enum error, which spells out
the whole withheld variance-code catalogue the moment a bad code is POSTed
— defeating beat 6's "codes live only on the filing form" design. It also
left `period` and `body` unvalidated, so a mistyped period could file a
narrative that clears no breach, and an empty body could flip `explained`
on nothing.
Validate `code` with a refine (neutral, code-free message) instead of a
z.enum, strip any `options` field from the 400 body as defense in depth,
and add period/non-empty-body checks to both the REST route and the
file_variance_narrative tool (which calls store.fileNarrative directly,
bypassing the route entirely).
vi.stubEnv the Intelligence vars and PRESENTER_RESET_ENABLED explicitly in
every case so the suite can't hit a real Intelligence backend or wrongly
200 the FORBIDDEN case when those are set in the ambient shell
(pnpm test:unit could otherwise wipe a live memory store). Also fixes the
stale "route.ts doesn't exist yet" header comment, makes non-JSON response
bodies fail readably instead of throwing an opaque SyntaxError, and adds a
fetch-isolation test for the memory half (mocked global.fetch, asserts the
per-bucket forget/seed fan-out) while explicitly leaving the known
ok:true-with-seeded:0 honesty gap in route.ts to the separate fix.
useLatestCanvasSurface classified ANY a2ui-surface activity as a report, so
every exec inline block flipped CanvasRegion into a full-region report frame.
Exec ships no CanvasSurface, so that frame rendered nothing and buried the
CEO/CFO dashboard behind a bare "Back" control. Skip block:-prefixed surfaces
when deriving the canvas surface; the canvas contract is reports + OGUI only.
Integration-review round: a third seeded breach (burnRate) keeps the CFO
teach gate armed after beats 3a/3d explain opex, with a seam-guard test
pinning the order-independence; render_metric_block now returns its
blockId and a pinBlockToDashboard frontend tool makes beat 5's seeded
procedure executable; publish_board_pack is exported but no longer
registered (the countersign card owns the publish path); OGUI guidance
added to the prompt; countersign card retries on a bad PIN; pin 404s
diagnosably; e2e locator hardened.