638 Commits

Author SHA1 Message Date
Guido Vizoso c56cb0fb34 docs(showcase): correct two comments that outlived the code they describe
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.
2026-09-08 15:39:30 -03:00
Guido Vizoso 9bf0eb3f51 fix(showcase): relay the store's refusal message through both publish paths
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.
2026-09-08 15:39:29 -03:00
Guido Vizoso 9d6a42f00f test(showcase): match the exec contrast render-scan to the split card markup 2026-09-08 15:39:29 -03:00
Guido Vizoso ce600f6078 fix(showcase): detect rejected A2UI op lists via the store version counter
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.
2026-09-08 15:39:29 -03:00
Guido Vizoso be03693699 test(showcase): cover exec route success, guard and leak arms 2026-09-08 15:39:29 -03:00
Guido Vizoso 03ee6f4a3d fix(showcase): surface exec chrome failures and guard the two-registry invariant
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.
2026-09-08 15:39:28 -03:00
Guido Vizoso 41ac2ec175 docs(showcase): correct exec-skin falsities in the reskinnable-demo docs and derive the roster fixtures 2026-09-08 15:39:28 -03:00
Guido Vizoso 7c20a8d7d0 fix(showcase): keep the skin layout's CopilotKit provider props stable
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.
2026-09-08 15:39:28 -03:00
Guido Vizoso 242869fb94 fix(showcase): never let an unrenderable a2ui surface hijack the shared canvas
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.
2026-09-08 15:39:28 -03:00
Guido Vizoso 680b78b9c5 fix(showcase): stop the budget memo datelining itself in the future
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.
2026-09-08 15:39:27 -03:00
Guido Vizoso 33763eac6c fix(showcase): stop exec dev-reset calling a half-configured backend an OSS reset 2026-09-08 15:39:27 -03:00
Guido Vizoso 9d5321d675 fix(showcase): verify exec's memory sweep instead of assuming one page and a live row 2026-09-08 15:39:27 -03:00
Guido Vizoso fe109c78e9 fix(showcase): bound exec seed POSTs and make the seeded procedure stepwise 2026-09-08 15:39:27 -03:00
Guido Vizoso ef7c2dee42 fix(showcase): key exec's operator identity off a Map so prototype names cannot resolve 2026-09-08 15:39:26 -03:00
Guido Vizoso dfdbffd0b1 fix(showcase): bring exec's token pairs up to the contrast bar its siblings guard
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.
2026-09-08 15:39:26 -03:00
Guido Vizoso f2ea34f5ba fix(showcase): honour exec metric levers only when they are usable
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).
2026-09-08 15:39:26 -03:00
Guido Vizoso 09264d30e7 fix(showcase): make the CEO exception strip agree with the block beside it
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.
2026-09-08 15:39:26 -03:00
Guido Vizoso c12ffea1ae fix(showcase): colour exec ExceptionList rows by breach, and say what is missing
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.
2026-09-08 15:39:26 -03:00
Guido Vizoso e15d271c70 fix(showcase): tag the exec seed's exec-grade breaches for both audiences
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).
2026-09-08 15:39:25 -03:00
Guido Vizoso ffa04c26cd fix(showcase): make the exec block-spec guard reject every spec the renderer would silently absorb
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.
2026-09-08 15:39:25 -03:00
Guido Vizoso 2211c33f7e fix(reskinnable-demo): close the exec store's vacuous publish gate
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.
2026-09-08 15:39:25 -03:00
Guido Vizoso 86c9e589dd fix(showcase): surface exec ledger mutation failures honestly
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.
2026-09-08 15:39:25 -03:00
Guido Vizoso 92ba755a33 fix(showcase): make exec tool settles read as what actually happened
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.
2026-09-08 15:39:24 -03:00
Guido Vizoso 4dc0670ae8 fix(showcase): make the narratives route's withheld-code guarantee structural
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.
2026-09-08 15:39:24 -03:00
Guido Vizoso 05fa1739b7 fix(showcase): stop the exec tool boundary accepting calls it silently drops
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.
2026-09-08 15:39:24 -03:00
Guido Vizoso 66e132415f chore(showcase): oxfmt normalization across the CR fix batch 2026-09-08 15:39:24 -03:00
Guido Vizoso a3464fffe0 fix(showcase): classify exec tool settles instead of assuming success
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.
2026-09-08 15:39:24 -03:00
Guido Vizoso a47056dff0 fix(showcase): make exec dev-reset honest about partial memory failures
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.
2026-09-08 15:39:23 -03:00
Guido Vizoso 90eb49fd9f test(showcase): give the exec skin's vacuous tests teeth
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.
2026-09-08 15:39:23 -03:00
Guido Vizoso 037ac2e42d fix(showcase): exec skin lever hygiene — top display, department restatement, empty period, param preservation, breach coloring
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.
2026-09-08 15:39:23 -03:00
Guido Vizoso d057da8dee fix(showcase): surface exec ledger load/refresh failures instead of hiding them
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.
2026-09-08 15:39:23 -03:00
Guido Vizoso c2b2af0415 fix(showcase): guard buildBlockOps and createDraftBlock against invalid specs
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.
2026-09-08 15:39:22 -03:00
Guido Vizoso 44cf5fa608 fix(showcase): correct skin-count and per-skin-set doc falsities left by exec
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.
2026-09-08 15:39:22 -03:00
Guido Vizoso 2ae25e89e7 fix(showcase): cover exec skin in the beat-2 status-keyed-render glob
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.
2026-09-08 15:39:22 -03:00
Guido Vizoso 877014c533 fix(showcase): unblock the exec agent's runtime — hang, one-step runs, prompt contradictions
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.
2026-09-08 15:39:22 -03:00
Guido Vizoso 20fc39d46c fix(showcase): make exec block pin/unpin lifecycle honest
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>
2026-09-08 15:39:22 -03:00
Guido Vizoso cd434f47c0 reskinnable-demo: give the inline block card its own A2UIProvider
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.
2026-09-08 15:39:21 -03:00
Guido Vizoso f03ca57fbf fix(showcase): derive byDepartment 'all' rows from department sums, not an independent draw
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.
2026-09-08 15:39:21 -03:00
Guido Vizoso fc41a9c3e3 fix(showcase): DST-safe memo date, under-plan guard, author-name consistency
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.
2026-09-08 15:39:21 -03:00
Guido Vizoso 87b2b85e05 fix(exec-skin): stop the narrative route/tool from leaking codes or filing junk
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).
2026-09-08 15:39:21 -03:00
Guido Vizoso f872271427 fix(showcase): make exec dev-reset route tests hermetic against host env
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.
2026-09-08 15:39:20 -03:00
Guido Vizoso 5b615f11b3 fix(showcase): keep inline block surfaces out of the reskinnable canvas
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.
2026-09-08 15:39:20 -03:00
Guido Vizoso 311dd09efb exec skin: arm beat 6 against pill order, give the agent a pin path
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.
2026-09-08 15:39:20 -03:00
Guido Vizoso ef8f2b39a6 exec skin: reskin-skill review, docs roster, e2e for the block pipeline 2026-09-08 15:39:20 -03:00
Guido Vizoso af1489d381 exec skin: register Vantage, the eighth skin 2026-09-08 15:39:20 -03:00
Guido Vizoso c9362be845 exec skin: frontend tools, countersign HITL, beat-ordered pills 2026-09-08 15:39:19 -03:00
Guido Vizoso 39015ced42 exec skin: dashboard, explorer and board-pack pages 2026-09-08 15:39:19 -03:00
Guido Vizoso 000405ae32 exec skin: ledger context, identity, boardroom theme, layout chrome 2026-09-08 15:39:19 -03:00
Guido Vizoso 0b9ba32fd9 exec skin: seeded memories, user scoping, presenter reset memory half 2026-09-08 15:39:19 -03:00
Guido Vizoso 40905e5bc4 exec skin: in-process agent with block, narrative and gated publish tools 2026-09-08 15:39:18 -03:00