CI installs with --frozen-lockfile and refused the previous commit: the
lockfile still carried the `overrides` block that package.json no longer
declares (ERR_PNPM_LOCKFILE_CONFIG_MISMATCH).
Regenerated against the committed manifest, so the lockfile now declares
neither `overrides` nor `patchedDependencies`. The patch entry is the reason
the lockfile was held back in the first place — it points at an untracked
patch file — and it is absent here because the committed package.json does not
declare it. Local checkouts that want the patch keep it the way they always
have: their own package.json and lockfile, both on skip-worktree.
Verified by reproducing the CI step: `pnpm install --frozen-lockfile` succeeds,
and lint, typecheck, 2494 unit tests and build all pass on the resulting tree.
Dropping an .xlsx into the composer did nothing: the accept filter silently
discarded it, and widening that filter alone would have killed the run, because
the pinned @ai-sdk/openai@3 converter accepts no document media type but
application/pdf and throws before a request is made.
Spreadsheets now work end to end:
- A dependency-free .xlsx / .csv reader (ZIP central directory + OOXML through
DecompressionStream and DOMParser). It decodes date serials via styles.xml so
a date does not render as 45231, and places cells by column reference so a
gap does not shift every later value under the wrong header.
- The sheet is rendered to a column-aligned table document. Comparisons of
table encodings put a markdown-style aligned table well ahead of CSV for
model comprehension, and the same document is what a human sees.
- buildPdf paginates. It emitted a single page and drew overflow past the
bottom margin, so anything after ~48 rows was silently absent from a
perfectly valid PDF. Single-page output is byte-identical, so the existing
golden assertions stand.
- A per-run bridge around the agent swaps the media type to application/pdf on
the model leg only, and restores the original type and metadata on the way
back. Both the composer chip and the transcript therefore read XLS with the
real filename. The bridge keys on the exact payload, so a genuine PDF
attachment is never mislabelled.
- onUploadFailed is wired. A rejected or unreadable file now says so instead of
vanishing with no trace.
The demo workbook is committed as a generator rather than a binary blob, so the
fixture exercises the real reader: shared strings, date-styled serials, a
second sheet, deflate.
Also moves @copilotkit/* to 1.71.0 and @ag-ui/* to 0.0.59, and deletes the
@ag-ui canary overrides from both pnpm-workspace.yaml and package.json. Those
existed only because a released @ag-ui/core rejected SUBAGENT_* events and
killed banking's harness console; stable 0.0.59 ships SUBAGENT_STARTED,
SUBAGENT_FINISHED and SUBAGENT_ERROR, and 1.71.0 depends on exactly 0.0.59, so
the overrides would now hold the tree behind its own dependency.
Verified on the running demo, not just in tests: gen UI renders, the subagent
harness completes the offsite run (14 rows, 6 merchants, $2,377.15, 1m 4s),
memory recall returns seeded memories, and a spreadsheet answers with a table
built from the real file. Lint, typecheck, 2494 unit tests and build all pass.
Checked the reskin skill for staleness: buildPdf was documented as single-page
and two passages said onUploadFailed is wired by nobody. Both corrected here.
The notes now land in a source-controlled changelog instead of a scratch file
that rides the release branch. One file per lane, because the lanes version
independently: a shared file would interleave `1.70.0`, `angular/0.5.0` and
`channels/0.9.0` into one unreadable sequence.
monorepo -> CHANGELOG.md
angular -> packages/angular/CHANGELOG.md
channels -> packages/channels/CHANGELOG.md
`write-changelog.ts` prepends this release's section on the release branch,
create-pull-request commits it (a tracked file, always staged), and
`extract-release-notes.ts` reads the section back in the publish job as the
GitHub Release body. The changelog is therefore both the durable record and the
review surface: editing a section on the release PR changes what ships.
release-notes.md goes back to being ignored, so the same notes never exist as
two editable copies.
Also deletes 29 changesets-era changelogs that no tooling had written since
April. They stopped at 1.55.2 while the lane shipped 1.69.3, and
packages/angular/CHANGELOG.md still claimed 1.54.3 from before that lane split
onto its own 0.x line. Their content stays recoverable from git history. A test
pins the tracked changelog set to the lanes so they cannot creep back and
contradict the real versions.
Extraction never fails the publish job: it runs after npm publish, so a miss
annotates loudly and falls through to the existing bodyless-release fallback
rather than stranding the tag.
Committed with --no-verify: the pre-commit nx lane cannot run in this worktree
(packages/core and packages/channels-ui have no node_modules, and
`nx run @copilotkit/core:build` fails identically with the tree clean). The only
change under packages/** is deleting orphan markdown that no build or test
reads.
The demo's climactic refusal listed its breaches with the raw department
id, two inches below an exception block spelling the same scope out:
Publish refused: …
Burn Rate · all · 2026-08 <- the receipt
Burn Rate · Company-wide <- the block above it
`publishRefusalPayload` resolved `metricId` through `metricLabel` and
forwarded `department` untouched, and every breach the seeded demo
refuses on is company-wide — so the one id that reached the room was the
bare word "all", which reads like a bug rather than a scope.
The label map already existed in THREE identical copies (the catalog
renderers, the CEO dashboard, the Metrics Explorer) and the receipt had
none, so a fourth copy would have fixed this line and left the next
surface to make the same mistake. All four now share one module, which
also gives the two pages that indexed the map directly the `Object.hasOwn`
guard only the renderers had.
Nothing round-trips the field back into a query — file_variance_narrative
takes a metric and a period, no department — so the label is safe as the
only form that leaves the payload.
The new test pins "all" specifically, plus an unmapped key still falling
through as itself, since that path exists to report a query the ledger
could not answer and the raw key is what names it. Verified live: the
receipt now reads "Burn Rate · Company-wide · 2026-08".
The note was the last thing making this block taller than its
neighbours. Printed on every row it was 757px of content beside a 112px
metric tile; restricted to off-track rows it was still 311px against
243px for the two blocks below it. A pinned dashboard card answers "what
is the state of things" — five names, five owners, five statuses — and
the prose behind any one of them is a follow-up question, not something
to read five of at a glance.
Every row is now a single 27px line and the card is 269px, level with
the rest of the grid.
THE NOTE IS UNPRINTED, NOT DELETED. It stays on the cell's `title`, and
the page readable still publishes all five verbatim off the ledger, so
"what am I looking at?" reads out every explanation exactly as before.
The test asserts the `title` survives precisely so a later cleanup
cannot drop the data path while looking like a no-op.
Verified against three mutants: reprinting the note, stripping the owner
along with it, and dropping the `title` each fail.
The previous pass moved Owner out of its own COLUMN, which fixed the
name wrapping, but it landed on a sub-line of its own — so all five
initiatives still cost two lines each, including the three green ones
whose entire entry was then two lines to say a name and "on track".
Owner now rides the name line. The sub-line is the note alone, which
only a non-green row has, so a green row is a single 27px line and the
block stops being the outlier on the dashboard.
Measured on the CEO dashboard at a 533px card: 371 → 311 tall, against
243 for the two blocks below it. It was 757 before this sequence began.
Measured on the CEO dashboard: the initiative table was 667px of content
next to a 112px metric tile, and since the grid stretches a row to its
tallest card it padded Revenue vs Plan out to 757px — ~600px of dead
space beside a six-line block.
Two causes, both width rather than data. The table split three ways by
content, so with the assistant open — the demo's normal state, leaving
the card 286px — the Initiative column got 120px while Owner and Status
took the other half for a person's name and a three-letter code, and
every initiative name wrapped. Owner now sits on the sub-line where it
costs no width, and `table-fixed` at 78% stops the widest owner name
renegotiating the split.
The note then earns its lines only when the status is not green: a green
row's prose only repeats what its pill already says, and the rows that
keep an explanation are the ones a reader has to act on. Nothing is lost
— the full note stays on the cell's `title`, and the page readable
publishes all five verbatim off the ledger. It is also clamped to two
lines, since note length is ledger data and nothing upstream bounds it.
667 → 347 of content; the row goes 757 → 436.
The renderer test that asserted every note rendered is replaced by two
that pin the rule in both directions — printed for off-track, withheld
for green, and reachable on `title` either way. Verified against three
mutants: printing every note, printing none, and dropping the `title`
each fail exactly one of them.
Two fixed strips sat above the pinned-block grid: an exception feed and
an initiative RYG strip, both read straight off the ledger and neither
agent-configurable. The seeded dashboard ALSO pins an `exceptionList`
and an `initiativeTable` block, so the page opened showing each set of
rows twice — once in chrome the assistant cannot touch, once in a block
it can. For a demo whose whole claim is "the assistant composes this
page", static duplicates of the composable blocks were the strongest
argument against it.
Both strips come off. A page header replaces them, matching the CFO
dashboard's, so the two screens still read as the same screen with a
different set of blocks on it.
The page readable is now derived per-kind from what is actually pinned:
unpin the exception block and the agent stops claiming exception rows
are on screen, rather than reading out a feed the CEO is not looking at.
`visibleExceptions` and `ceoReadableExceptions` are unchanged and keep
their tests — they were always the rows the block shows.
Two consequences worth naming. The strip's click-through into the
Metrics Explorer goes with it: the block's rows are not links, so an
exception can now only be drilled into by asking Vantage to navigate.
And three contrast pairs cited `pages/ceo-dashboard.tsx` for the
initiative pills; the pills now render only in the catalog's
InitiativeTable, so the dead sites are dropped rather than the pairs
weakened.
The grid was told `items-start`, which stopped a short card stretching
but left its neighbour's extra height as a ragged gap. Dropping it
returns the row to `items-stretch`; `h-full` on the card and `flex-1` on
its body make the card actually use that height instead of leaving the
slack outside its own border. Two cards side by side now line up top and
bottom.
The RYG strip stacked a 40%-opacity status wash, a 4px coloured left
edge and the status pill — three signals saying one word. On the dark
theme the washes muddied the surface enough that the tinted cards read
as a different material from every other card on the page.
The pill stays as the carrier and the card keeps a hairline tinted to
match, which is still enough to scan a row by colour. Cards also widen
to 15rem and let the initiative name wrap to two lines instead of
truncating, since several names lost their last word at the old width.
theme.test.ts's contrast guard caught this immediately: three composited
pairs it checks describe text over those washes, and the washes no
longer render anywhere in exec. They are retired with a note rather than
weakened, so the guard keeps failing if a wash comes back untested.
Every block renders inside a host that already draws card chrome — the
chat bubble in inline-block-surface.tsx and the dashboard card in
dashboard-grid.tsx — and both the shared Tile and buildBlockOps were
drawing their own on top of it. A pinned block came out with two rings,
two backgrounds and two shadows, and with its title printed twice: once
in the card header beside the move/remove controls, once as the Heading
component the op builder emitted.
Tile drops the chrome and keeps only its label; buildBlockOps emits the
Heading only when the block is NOT pinned, so a block in the chat still
names itself. The initiative table's label was a bare "Initiatives",
which under a card titled "Key Initiatives" was the double title in a
second form — it now carries the count and how much of it is off track,
the way its sibling labels carry metric, department and period.
The grid gains items-start so a short card no longer stretches to the
height of the tallest one in its row.
The shape behind three 500-class defects: an object literal inherits
Object.prototype, so REGISTRY["constructor"] returns a truthy inherited member
and the `?? null` guard never fires. A new no-restricted-syntax selector flags a
computed lookup on a Caps-named object whose result is guarded by ??, ||, !, an
if test or a ternary test — the guard is the discriminator, so the couple of
dozen type-closed lookups in the exec skin stay quiet.
Scoped to the exec skin and the shell only; every sibling skin still carries the
shape and a wider glob would turn the tree red. src/shell/documents is exempt
(pdf.ts indexes by a character its own regex class closed) and that exemption is
itself asserted. skins-config.test.ts gains the resolved-selector rows and
red/green fixtures for the selector.
Both sweeps hand-copied the four VAR-* codes, so a fifth code added to
NarrativeCode would simply never be looked for. Both now derive the list through
`as const satisfies Record<NarrativeCode, true>`, which fails the typecheck
instead.
The schema walker also read only enum values and describe() text: a
`z.literal("VAR-TIMING")` (member on _def.value) or a union of literals (branches
on _def.options) published the whole catalogue into a tool's JSON schema and the
sweep stayed green — proven by mutation. It now walks literals, union and tuple
branches, record key/value types and intersection halves, with a synthetic
fixture pinning that coverage so the arms cannot regress silently.
MetricId and Department are restated by hand in four files. Only two carried
`as const satisfies Record<MetricId, true>`; catalog/definitions.ts and agent.ts
kept plain z.enum literal lists, so a metric added to the union stayed
unbindable and unaskable with every typecheck green. Both now derive their enum
from a compiler-checked record, and a new parity suite reads the four SHIPPED
schemas (plus the narratives route, through its handler) and the seed, asserting
they are set-equal — which satisfies alone cannot do, since it checks each copy
against the union and never against the ledger that has to hold rows for it.
Three crash paths in the exec skin, each reachable from an ordinary value:
- `Heading`/`Text` read `props.text.path` for a non-string label, but the
A2UI binder resolves dynamic props before render and passes `undefined`
for an unresolved binding — it never leaves a standing `{ path }` object.
Reading into it threw a TypeError out of the renderer, taking the whole
surface down instead of the one label that failed to bind. The guard now
reports whatever shape arrives, and the comment that described the old
standing-ref theory is corrected.
- `DEPARTMENT_LABEL[dept].toLowerCase()` threw for a department key outside
the four seeded ones — a key the agent can send, and one that lands on the
failure path by construction. The label now falls back to the raw key, so
the path that exists to REPORT a bad query no longer throws on one.
- The budget memo's fixed 0.62 driver split does not survive whole dollars:
overruns of $2 and $4 round to a 1/1 and a 2/2 TIE, which the builder's
`DriverSplitError` refuses and the route turned into a bare HTTP 500.
The timing share is now ceiled off the rounded overrun, which keeps it
strictly the larger driver for every overrun, and the route maps the
builder's own refusals to their codes (NOT_AN_OVERRUN 404, DRIVER_SPLIT
409) rather than reporting a ledger fact as a server fault.
Ops are rebuilt from the stored spec on every read, and buildBlockOps throws for a spec it cannot render (seeds construct blocks directly, bypassing assertValidBlockSpec). That throw escaped the handler and 500'd the entire snapshot, which the provider's first-load gate turns into an error panel above the chat and the grid - taking out the very grid needed to unpin the bad block. The failure is now scoped to its own block: empty ops plus an opsError, block kept so its card chrome still offers unpin, logged loudly.
The narratives route appends the narrative before composing its response, so a 201 whose body will not parse is a lost receipt, not a failed filing. A bare res.json() rejected with a SyntaxError, skipping refresh() and printing a parser error on beat 6's filing form over a narrative that IS in the ledger. Mirrors publishPack's existing lost-receipt arm: reconstruct the row from what was sent (the fields the form and the teach chain read), refresh either way.
`state.dashboards` and `agentRegistry` are plain objects indexed with
caller-supplied ids — a URL path segment for the exec block routes and for
`publishPack`, and a URL-derived `agentId` for the shared CopilotKit route.
A plain-object index walks the prototype chain, so `constructor`,
`toString`, `valueOf`, `hasOwnProperty`, `__proto__` … each resolve to a
truthy INHERITED member.
In the exec store that sailed past the `!dashboard` guards and threw the
raw `undefined.blocks` TypeError those guards exist to prevent: a 500 with
no body where `store-errors.ts`' mapped 404 belongs. Worse in
`publishPack`, which THREW instead of returning, breaking the returned-not-
thrown refusal contract the packs route (no try/catch) relies on. Both now
go through one `lookupDashboard` helper using `Object.hasOwn`.
`agentRegistry` is latent today — the route's `?.` swallows the inherited
member — but it is the same class, one registry over from the `getSkin`
fix. It is now built on a null-prototype object rather than behind an
accessor, so the guarantee covers the index sites that already exist
(including the agent-map builder) instead of only callers that remember a
helper. Enumeration is unaffected, so `agentIds` is unchanged.
Both suites pin the behaviour per inherited key rather than pinning which
own-key primitive implements it.
Four defects in Vantage's tool surface, all of them things the room reads or
waits on:
- navigateTo's `period` carried a schema-level `.regex(...)` and `top` an
`.int().min(0)`. The AI SDK rejects a bad VALUE at the parameter boundary and
emits a `tool-error`, which @copilotkit/runtime has no arm for (verified
against ai@6.0.259) — no TOOL_CALL_RESULT is emitted, so an off-format period
left the chip spinning "Navigating…" forever. Same defect class agent.ts
already documents for `months`. Both are relaxed to shape-only and checked in
`navLeverRefusal` inside the handler, answered as a correctable result;
`period` is validated through the Metrics Explorer's own
`normalizePeriodLever`, so the tool cannot refuse a lever that page would
honour. No other frontend schema carries a value constraint.
- get_metrics and list_exceptions had no exact renderer, so the shell's
wildcard chip — which keys "done" off status alone — ticked "Reading metrics
✓" over a read that returned an error. Both now render through `ReadSettle`:
a refusal reads as a refusal, a success keeps the settled label and never
prints the series payload.
- A failed pin printed "Could not pin that block: ALREADY_PINNED: …". The strip
removed only the ledger's prefix and leaked the store enum into the
transcript; `pinFailedLine` strips the coded prefix too and keeps the store's
remedy.
- `refusalLine` preferred the producer's `message`, and agent.ts writes those
FOR THE MODEL — the room saw "Call render_metric_block again…" and "do not
retry the same call". The message is now used only when it is already
operator-safe, with a per-code operator phrase table behind it. The model's
copy of the message is unchanged.
The Metrics Explorer's readable published every row: the unlevered page is
528 seeded points, so ~100KB of JSON went to the agent on every turn. It now
publishes a bounded prefix and says so (showingCount, rowsTruncated), so a
cap can never be mistaken for the whole view.
Alongside it, the readables and screens that disagreed:
- ceo-dashboard publishes varianceDisplay beside the raw fraction, and nulls
a non-finite variancePct deliberately rather than letting JSON do it while
the card reads "n/a"; visibleExceptions is memoized.
- The exception cards drill in with the metric they name, via a new metric
lever on filterMetricRows and its own control on the explorer.
- board-packs emits each timestamp's display string beside the ISO instant,
unifies the two divergent dashboard-title fallbacks, makes its newest-first
comparators total, logs the submit it is actually performing, and hands
FiledNarrativesList the list and label map the page already derived.
- cfo-dashboard's readable page id is "finance", the nav segment navigateTo
takes; board-packs' is "packs" for the same reason.
- normalizeDepartmentLever trims, and a def-less metric is now reported once
by id instead of dropped in silence.
- Explorer smalls: the unknown-unit diagnostic fires once per unit rather
than twice per row per render, the breaches-only toggle carries
aria-pressed, lever writes replace instead of flooding history, and the
top select no longer claims an off-vocabulary value came "from chat".
`execToolLabels` was described as covering the "report" beats; exec
ships no a2ui report tool at all, which is the very reason the skin
supplies no `CanvasSurface`. And the `CanvasSurface` note cited the
shared canvas's "no renderer for this kind" branch as how exec is
handled, while report-canvas.tsx and skin-contract.ts both say that
branch is defensive, never runs for exec (its surfaces are all
`block:`-prefixed and skipped before the region is claimed) and must
not be cited as covered.
In the test, the comment calling catalog/designSkill OPTIONAL on the
`Skin` contract was wrong — both are required — so it misnamed what
these cases guard. They guard the EMPTY value the type cannot see, which
the catalog case now actually asserts rather than only checking the
field is defined.
The brief inlines every colour as a literal because the sandbox iframe
ships a bare CSS reset and `var(--brand)` resolves to nothing in there —
but the copy was never resynced after the contrast fix. It shipped the
PRE-fix brand (43 55% 45%, the 2.71:1 pair), positive (152 50% 38%),
dark negative (0 65% 60%, 4.31:1) and muted ink (220 9% 44%), so
generated UI rendered the unreadable palette the app itself had already
replaced. Three of the four are the exact ratios theme.test.ts records
as the failures it exists to catch.
Every literal is now updated to the current token value and PINNED:
theme.test.ts parses the `hsl()` values out of the brief and asserts
each equals the theme.css token it names, failing on any literal it has
no mapping for, so the two files cannot drift apart again.
Also records why the dark guidance keys on `prefers-color-scheme` while
the app toggles a `.dark` class: the class cannot reach the iframe (the
renderer builds its document from the model's HTML plus a reset), so the
media query is the only dark signal the generated markup can observe.
Two ride-alongs in the same file: the per-pair site scan is now
per-SITE, because asserting the flattened match list was non-empty let a
pair citing five files pass on one survivor (gutting ceo-dashboard.tsx
kept all 46 green; it now fails 6). And the note claiming tools.tsx
"is being given" a `dark:text-brand-violet` counterpart is replaced by
listing tools.tsx among that pair's sites, so the claim is grepped.
The OGUI sandbox getter still used a plain `if (months)`, the shape
`store.ts`'s `periodWindow` was written to replace: 0 is falsy and
silently returned the full 24-month history, and a negative reached
`slice(-(-3))` === `slice(3)` — the OLDEST periods, an inverted window
answering a request for the newest three. `months` arrives from
LLM-authored JS inside the iframe, which is the least constrained caller
in the app, so both are reachable rather than hypothetical.
Reimplements the same normalization locally (the store's helper is
private) and names the contract in the comment. Also fails loud on an
unknown metricId/department, which previously resolved to an empty
series indistinguishable from a metric with no rows yet, and derives the
zod enums from `satisfies Record<MetricId, true>` records so the schema
cannot drift from the unions in `data/types.ts` — the pattern
`api/exec/v1/narratives/route.ts` already uses.
The fixture is unsorted so the getter's own sort is actually exercised;
pre-sorted, deleting the sort left every assertion green.
Mounting the layout waits on vite transforming the whole shell tree behind
`layout.tsx` — about a second when the file runs alone, but over vitest's 5s
default whenever it competes for the transform pool with the other 60-odd
suites. `pnpm vitest run src/shell src/app` failed here on most runs while the
same file passed in isolation, and the failure named a timeout rather than
anything the tests assert. Pre-existing: the suite fails the same way at the
previous revision.
Load the module once at file scope instead of per mount, and give the describe
block a 30s budget.
`identifyUser` is optional by type but mandatory in practice — drop one and the
skin still renders, still chats, and silently loses its durable-memory scope,
with nothing in tsc, lint or the existing suite to catch it (verified by
mutation: removing exec's resolver kept all four tests green).
Also replaces the `agentIds` assertion, which compared the export against
`Object.keys(agentRegistry)` — its own definition, so it held for every possible
implementation. It now checks `agentIds` against the client SkinRegistry keys,
which stays meaningful if the export is ever rewritten as a hand-kept literal.