boardChrome.test.tsfeat(board): glyphs instead of column colour, declared by whoever writes the schema Columns had a colour: the option's colour when the field declared one, else a hash of the value. It worked and it was wrong. A row of tinted headers competes with the card faces underneath, the hashed hues mean nothing — they are only "not the neighbour" — and a colour cannot say FINISHED, only DIFFERENT. A glyph can. A dashed circle reads as not-started, a filled dot as underway, a check as done, at a glance and without a legend. So the colour moved onto the glyph and the header lost its tint entirely. `column-tone.ts` is gone. A select field now carries `optionIcons` — one glyph name per value: { key=[redacted], options: ['backlog','done'], optionIcons: { backlog: 'circle-dashed', done: 'circle-check' } } THE BOARD DECLARES THEM; CEDAR ONLY DRAWS THEM. Which shape means finished is a judgement about the VALUES — `shipped`, `settled`, `closed_won` and `archived` are all somebody's done — and the board is the only thing that knows what its values mean. So an agent picks the glyphs when it creates the board, and the tool description now tells it to, with the name list. Both templates ship theirs. The NAMES are a closed, shared vocabulary the same way `TableColumnType` is: Cedar knows what `circle-check` looks like, never what `done` means. A registry of shapes is a rendering concern; a registry of statuses would be the board learning a noun again — which is the whole thing this refactor was about. An unrecognised name draws NOTHING, not a fallback glyph. A wrong-but-present shape is worse than none: the label alone still reads, but a card showing a check because the name was misspelled reads as finished. The same glyph appears on the card face, so a value looks like itself whether you read down a column or across a card. Scalars only — a multi-value cell would draw a row of glyphs competing with the text, and first-value-wins is a COLUMN rule, not a display one. Not reintroducing `wipLimit`: it was a per-lane number the code itself described as "surfaced in the UI, never enforced by a write". An unenforced limit is decoration, and a column is a value now, with nowhere to hang one. Verified live as jesse@: an agent-authored schema round-trips its glyphs including a deliberately misspelled name (kept verbatim, draws nothing) and value keys Cedar has never seen; both templates arrive with theirs. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 31, 2026, 8:37 PMBoardDocumentView.empty.test.tsxfeat(boards): a board born with columns, and a schema you edit as a ticket A new board arrived as a blank rectangle — the one thing it failed to teach was that it was a kanban. It is now created on a starter axis (`Column 1/2/3`), placeholder vocabulary that reads as "rename me" rather than Cedar deciding the board is a work ladder. `emptyBoardSchema` stays what a board READS as, so a failed parse still invents nothing. The 20rem schema dropdown becomes two controls, the same split as /tasks/kanban: - Schema, a modal shaped like an open ticket — title/description beside a rail of typed attributes, the geometry a person already knows from `CardDocumentView`. Identity writes to `documents` columns (`boards.setInfo`); shape writes to the Y.Doc, so an agent's edit merges rather than clobbers and shows live while the dialog is open. - Display, the `view` half alone — which attribute draws the columns, which show on the card face. Regrouping writes no card row, which is the whole reason it is a view. Renaming a column renames the value on every card in it (`boards.renameColumnValue`); renaming only the option drew a new empty column beside the old one, still full. Boards and cards also render in /brain now — a board's content is not its prosemirror fragment, so the prose branch drew an empty editor that wrote prose INTO the board. Both own their full surface, and the back gutter steps aside: at wide widths that invisible ~460px strip lay over the first kanban column, eating clicks and blocking drags. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 7, 2026, 3:28 PMBoardDocumentView.render.test.tsxfix(tests): types:test reaches zero in both packages The leftovers, and three of them were assertions or props that could not work. DEAD SHAPES. `model` is retired from `AgentSummary` (797 of 896 production docs said `sonnet` and the hardcoded per-tool constants were what ran), yet two fixtures still set it. `summary` is not an `AgentSharePanel` prop — it takes `{ agentId, className }`. `onAddTaskToGroup` is no longer on `AgendaTaskCallbacks`, and the mocks type is a mapped type OVER that interface, so the extra key was the one thing it could not carry. GROWN TYPES. `AgentSummary` gained `namespace` — where the agent's FOLDER lives, which is a different question from `scope`, where its document lives — plus `defaultFile` and `chatEnabled`. NARROWING, NOT CASTING. `closest()` answers `Element | null` and `within()` wants an `HTMLElement`: the non-null `!` covered the null and not the widening, so it is an `instanceof` check now and a card whose text escaped its container fails loudly. Same for `PAGE[0].createdAt` and the reference-lookup input. MOCKS FROM THE REAL THING. `applyUpdate` carries `ApplyUpdateClient`'s signature, so `mock.calls[1][0].update` — the bytes those cases exist to prove were re-sent — is a real read rather than an out-of-range one on an empty tuple. Likewise `logToolCall` (six arguments, five destructured), `createPlaybookExtensions` and `useSubagentCreation`, whose option types come off the hook rather than being exported for a test's benefit. Four `as unknown as` casts go with them. `feedScopeGate`'s `ALL` lost its `as const`: nothing read it in a type position, and the readonly tuple could not be the mutable `string[]` the resolved participation set arrives as. Also `shareDocumentTool.test.ts`, new this morning, through the same two helpers the rest of the server already uses — `toolOutput()` for the `ValidationError` arm and one named boundary for Mastra's request context. apps/mail 19 -> 0, apps/server 11 -> 0. Both `types` stay clean. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 9, 2026, 9:39 PMCardFieldList.test.tsxfeat(board): the board agent tool, the kanban, and the typed ticket (phases 3-4) Answers "can a board be used exactly like a table" with yes on all three surfaces: headless, agent, and UI. **The extraction that makes the rest honest.** Every board operation moved to `services/documents/board/board-ops.ts`. Three callers now wrap it — `admin.boards.*` (the CLI), a new session-scoped `boards.*` (the browser), and `boardTool` (the agent) — differing in exactly one thing: how they answer "who is asking". A kanban drag and `cedar-cli board card move --before` are the same function call, so the headless proof is evidence about what the UI does. **Phase 3 — the agent tool.** `boardTool` with all fourteen actions, a flat object with an `action` enum (a top-level union advertises `{properties: {}}` to the MCP SDK). `boards:` grant enforcement at the CALL SITE: a read-only grant refuses set_schema, an ungranted board is refused by name, and `list_boards`/cross-board `cards` are CLIPPED rather than errored. `grantsBoard` is deliberately not `grantsFile` — a prefix rule would make a grant on `organisation/boards` silently grant every board in the org. **Phase 4 — the UI.** A kanban (`BoardDocumentView`) drawn from one SQL query with dnd-kit drag ordering, and a ticket (`CardDocumentView`) in the same body+rail geometry `TaskTicketView` already uses. The rail renders every field the BOARD declares, typed — select pills, checkboxes, dates, URLs, `[[type: id]]` chips — by importing the table's chip renderer, option colours and type vocabulary rather than forking them. A field whose type Cedar has never heard of renders as text and says so. Two things the UI does that keep the schema open: it renders values whose keys the board does not declare under their own heading, and `BoardSchemaMenu` never serialises a fresh object from form state — every action reads the current schema out of the Y.Doc and merges. Those were the two remaining ways to silently close an open schema; the other two are on the server and were already closed. The tool description had to be cut from 2467 to 2044 chars — the tool-description-length guard catches exactly this, and text past 2048 is silently dropped for every caller. 1551 server tests and 217 frontend tests pass; both packages typecheck; oxlint clean. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 30, 2026, 9:33 PMopenCard.test.tsfeat(board): click into a card the way you click into a task A card had its own id, path and share link — the whole reason a ticket is a `documents` row rather than a Y item — and no way to navigate to one. It opened only by clicking a tile inside the kanban, which swapped in place: no URL, so it survived neither a reload nor a paste, and no other surface could open it. Modelled on `open-task.ts`, which exists for the same reason (a task is clicked from a sidebar, a kanban and a board, and all three must agree what "open this" means): - `open-card.ts` — the one definition. Writes `?card=<id>`, then sets the `file` display artifact. Param first, so a reload landing mid-open resolves to the ticket the artifact is about to show. - `useOpenCard()` — wires that to nuqs and the store. - `LayoutUrlSync` — `?card=` URL -> store, mirroring the `?task=` block, so a cold load or a pasted link resolves. Claims the artifact only when no other is named in the URL; its clear branch is guarded on the artifact still being a file. `kind: 'file'`, not a new ContextKind: a card IS a document and the panel already dispatches on documentType. A `card` kind would mean teaching every context-item consumer about a file wearing a hat. `BoardDocumentView` gains an optional `onOpenCard`. Absent it keeps the in-place swap — right when the board owns its whole surface (the Files tab), where there is no column to swap. The artifact panel supplies the URL-writing one. And the affordance that was missing either way: `getCard` now returns the parent board, so a ticket renders a breadcrumb back to it. Without that, a ticket opened from the artifact column was a dead end — the same thing TaskTicketView solves by naming the deal beneath the title. 502 frontend and 884 server tests pass; both packages typecheck; oxlint clean. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 30, 2026, 10:08 PM