boardfix(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 PMtablefeat: the transcript names what it did, and the table it made is reachable A chat that spent twenty calls building a LinkedIn tracker recorded almost none of it: four different people got connection requests under four identical "Executing linkedin-write" lines, the table itself never became a context item, and re-entering the chat reopened the deal instead of the table. Traced end to end; this is the set of causes. The transcript: - Tool labels resolve from the ARGUMENTS, not just the name. Every tool is an action family now, so the name alone collapses a dozen verbs into one row. Rows expand to who / where / whether a message went with it. - A select column's declared option order IS its sort ladder; the tool description now says so, and the transcript shows the ladder. LinkedIn caps: - A send's provider-id lookup gets its own budget (connect/dm/inmail_lookup), mirroring the send it serves. Charged to profile_view, a day of browsing refused invitations that had 80/week of connect headroom left. - Connection requests go out with NO note unless the user asked for one — enforced at execute time, not just described. Getting back to what the agent made: - The table tool files a context chip on create/import, as write-document has always done; delete pulls it back off. - A thread remembers what it was displaying (chat_threads.context), so re-entry restores the table rather than re-deriving the deal. - A fan-out opens a conversation-scoped table inside its deal's Files tab. - 'table' is a valid write type on the document family — the tool was telling the agent to pass a value its own schema rejected, which is what drove a delete-and-rebuild that orphaned the open document. The grid: - Title, then description, then the commands — the order the questions arrive in; a URL or address in a cell is a real link; a deleted table says so instead of rendering on from the local Y.Doc cache. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 20, 2026, 11:43 PMagentFrontmatter.test.tsfeat(agents): four namespaces, a default file, and an agent doc that opens as the agent An agent's folder now reads as four namespaces — playbook, config/, memory/ and everything else as output — by CLASSIFICATION rather than migration, so not one document row moves. The thing that actually writes these paths is prompt text, some of it stored in seeded agent bodies, and `overview` is addressed by exact-path equality while `archives/` is matched by a metadata-stamping regex; re-pointing the helpers would have been a silent-failure change. This generalises the split the coaching agent already ships. `default_file` frontmatter says which file the Files tab opens. Relative to the agent's folder so it survives duplicate and publish, resolved once on the server so no client re-derives the default, absent meaning `overview` so nothing changes on deploy. Set from the agent's menu, a file row, `agent.create` or the CLI. The frontmatter patcher learned to REMOVE a key, which is what makes "back to the default" expressible at all. A document whose type is `agent` renders as the agent. Five surfaces rendered one as prose and only one mounted the structured header, so the raw YAML showed with no header at all on the rest — and the hider was looking for a horizontal-rule run when the server emits a `frontmatter` sentinel code block. Both are fixed: the render is keyed on documentType, and the settings are a collapsed form above the editor carrying every key, with unknown ones kept in their own block rather than dropped. Brain documents share one measure and one left edge: the reading column drops to 80ch with no horizontal padding, its back gutter is resized to match, and a stale `ml-4` on the playbook crumb is gone. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 2, 2026, 11:51 PMcoaching-fence-attrs.test.tsfix(coaching): stop the callout round-trip and the warm-up gate losing real data Review findings on this branch. Four of them lose or suppress real content: - Serializing a callout with plainText dropped every inline mark inside it, so bold, links and widgets vanished on each save of the markdown mirror. Use renderInline, and collapse breaks to a space since the marker syntax is single-line on both sides. - Converting a blockquote to a callout lifted its first paragraph and discarded every sibling block. A callout is inline-only, so anything larger now stays a blockquote rather than losing content. - isWarmupSeedThread fired on ANY message carrying a seed code. Warm-up mail gets merged into live deal threads, and one stray message would then suppress the agent on a real deal on every subsequent sync, silently and forever. Require every non-draft message to carry a code. - The moment card's deal crumb pointed at /conversations/:id, which stopped being a route; it opens at /agent?conversationId= via a router Link. And the smaller ones: decodeURIComponent throws on a malformed attribute and would take down the editor from inside parseHTML, the recording link is agent-authored so it needs a scheme check, --days reached Postgres as 'NaN days'::interval, a code inside a table cell was missed, and the radar dropped a series whose header matched the score column. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 25, 2026, 3:55 PMcoaching-fence-render.test.tsfix: rubric matches Peter's Aug 19 list exactly; skip non-disco/demo calls Two corrections after reviewing the generated output against the designed state. 1. The rubric had drifted. Peter's Aug 19 recap names five discovery rows, FOUR demo rows and three end-of-call items. This drops M5 (right demo type), which was never in that list, and cuts the checklist from eight invented items back to Peter's three: objections surfaced, decision-maker name asked for, group demo or ROI meeting asked for. M1's wording is now his: "this can just be if they are saying 'other people i speak with'". 2. Unscored calls are skipped entirely — no document, no line, no mention. The previous behaviour listed them, which produced near-empty stub documents for closing and pricing calls and made the whole suite look like summaries. This supersedes Peter's Aug 23 "I'd like to see all the calls" request; the classification stays auditable via save-execution-summary. Also adds coaching-fence-render.test.ts, which pins that a ```scorecard fence actually becomes a scorecardBlock rather than being claimed by StarterKit's generic code-block rule — registering an extension is not the same as it firing, and a grey code block looks exactly like "the scorecard didn't render". Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 25, 2026, 7:38 AMcoaching-yjs-survival.test.tstest: confirm the markdown baseline (design: coaching-agent phase 1) The coaching-agent design rests on one claim: an agent can write a plain GFM pipe table and a plain GFM task list into a Cedar Doc and both render, with no new node types. Phase 1 pins that before anything is built on it. - markdown-baseline.test.tsx — schema registration, real markdown parse to table/tableRow/tableHeader/tableCell and to taskItem with `checked` preserved, and a markdown round-trip. - coaching-yjs-survival.test.ts — the table, the checklist's checked state and the day heading all survive a prosemirrorJSONToYDoc/yDocToProsemirrorJSON round trip, so a rep's checklist does not silently reset on sync. Both drive the same extension set markdown-editor.tsx registers, so dropping table or task-list support there fails these. Also corrects the design doc: apps/mail runs jest, not vitest. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 24, 2026, 10:54 PMdocument.test.tsxfix(agents): thread targetUserId into the instructions doc editor + doc-events SSE Karim (Adapt Insurance, org admin) reported Jacob's agent instructions not showing when viewing his agent — the admin-view targetUserId prop reached every trpc.agent.* call but never the shared <Document> Y.js editor that AgentInstructionsSection hands the actual instructions content to. Document had no targetUserId concept at all, so its documents.getDoc fetch ran under the admin's own session; the server correctly rejected it as a cross-user read (row.userId !== userId on a user/-scoped doc), and the failure was swallowed into a console.error with no onProviderError handler wired up — the editor just rendered a permanently empty Y.Doc. - Document/DocumentProps gains targetUserId, forwarded to both the initial getDoc seed and refreshFromServer. - AgentInstructionsSection forwards its own targetUserId prop down into <Document> instead of dropping it at the door. - /api/doc-events (the live-update SSE channel) had the same shape of gap: a user/-scoped doc's live stream only allowed the doc's own owner or Cedar staff, so even a fixed initial read would 403 on remote updates. Routed through assertActorAllowed — the same authority documents.getDoc already uses — instead of a flat isCedarStaff check. files.applyUpdate (the write path) needed no change: it already derives the document's scope from the row itself via assertAuthorizedScope, not from a client-supplied id, so org-admin writes were already authorized correctly. Co-Authored-By: Claude Sonnet 5 <<email>> Claude-Session: https://claude.ai/code/session_01SfYKPYHXdMgrPLuKhXvWedSep 11, 2026, 12:29 PMDocumentProperties.test.tsxfeat(mail): one properties rail, and a tree that reads as a tree **The properties rail is now literally the task ticket's.** It was a lookalike — a `grid-cols-[5.5rem_1fr]` with a fixed label column, its own caption constant, its own row height — which is why it read as a different UI for the same job. The task rail is the one built against Linear and the one that reads right, so it was extracted rather than averaged: `components/ui/property-rows.tsx` owns the row geometry, the hover chrome, the caption type and the section rhythm, and both surfaces compose it. `TaskTicketProperties` keeps only what is a fact about a task. One deliberate difference, and it is not cosmetic: a document's keys are ARBITRARY. `stance` and `seniority` are both selects, so `propertyIcon` gives them the same glyph, and a task rail's icon-only row would leave a column of identical dots beside three bare values. So the field's name rides in the row, muted, ahead of the value — same height, same type, same hover. It does not get a column of its own, which was the thing that made the old rail read as a form. **Siblings stopped disagreeing.** Floating anchors were picked by comparing card CENTRES, and two people on one row of a chart have different centres whenever their cards differ in height. When their shared boss's centre fell between the two, one child connected upward and the other downward, and a line looped around a card for no reason a reader could see. A rank is a shared TOP edge — exactly equal for siblings however tall either card grows — so tops are what is compared now. **The axis relation draws orthogonally.** `getSmoothStepPath` with `offset` at half the layout's own rank gap, so every child of one parent turns on the same y and they share one horizontal run: the screenshot's shape, and what makes a tree read as a tree rather than as a fan of curves that happen to converge. Every other relation stays a bezier, so an `influences` overlay can never be mistaken for the reporting line it crosses. The `hierarchy` flag comes off the same predicate that chose `layoutEdges`, so what ranks the chart and what draws as its skeleton cannot be two different sets. **The legend moved to the bottom.** It is read once on arrival and then never again, which is the whole lifecycle a legend should have — and that is not a reason to put it across the top of the drawing it explains. Co-Authored-By: Claude Opus 5 <<email>>Sep 20, 2026, 5:50 PMmarkdown-baseline.test.tsxtest: confirm the markdown baseline (design: coaching-agent phase 1) The coaching-agent design rests on one claim: an agent can write a plain GFM pipe table and a plain GFM task list into a Cedar Doc and both render, with no new node types. Phase 1 pins that before anything is built on it. - markdown-baseline.test.tsx — schema registration, real markdown parse to table/tableRow/tableHeader/tableCell and to taskItem with `checked` preserved, and a markdown round-trip. - coaching-yjs-survival.test.ts — the table, the checklist's checked state and the day heading all survive a prosemirrorJSONToYDoc/yDocToProsemirrorJSON round trip, so a rep's checklist does not silently reset on sync. Both drive the same extension set markdown-editor.tsx registers, so dropping table or task-list support there fails these. Also corrects the design doc: apps/mail runs jest, not vitest. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 24, 2026, 10:54 PMmoment-fence.test.tsfeat(coaching): moment header — clickable deal, Fathom button, named criterion - Header is now conversation > event > date, with the deal name clickable when the fence carries a `conversation:` id. - An "Open Fathom link" button sits right-justified on the header row, from crm_meeting_events.recording_url. Real Fathom share links are wired into the Xander suite. - The criterion moves to its own row and carries its readable name — "D2" alone means nothing to the rep reading it. A bare id still parses as an id (caught by an existing test when the split regex first required a name). - Positive coaching notes must now say what the move PRODUCED, not just that it was good: a good moment is only instructive if the rep can see the payoff. - The weekly overview separates every section with a rule so the scorecards, checklist and pattern sections do not run together. All four rules are in the playbook prompt, not just the generated docs. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 25, 2026, 8:03 AMrecording-fence-render.test.tsfeat(documents): play the call inside the document, at the moment being discussed A coaching note the rep cannot check against the tape is an assertion, and until now checking it meant leaving the document for a provider page that opened at 0:00. A ```moment card now embeds the call positioned at `at:`, every timestamp in its transcript is a button that seeks the player to that line, and a new ```recording fence embeds a whole call anywhere in a document. Playback resolves from `externalId`, never a stored URL. That is not a preference: Circleback's `recording_url` is a signed link that expired 24h after ingest, so every Circleback moment's "mandatory recording link" was already a 400, and Fathom's is a web page rather than media. The id is stable, and the server mints from it per view. Cedar's own <video> rather than a provider embed, because only an element we own can be seeked — verified against the live providers rather than assumed. Fathom's /embed/<token> IS frameable (no X-Frame-Options, no frame-ancestors, unlike /share), but its component declares only {call, autoplay, shareUrl, displayMode} and nine probed parameter names changed nothing, so it cannot be positioned; it is kept as the last rung of the fallback ladder. Fathom's share page does deep-link — ?timestamp=125 comes back as currentTime 125.0 — so links out carry the moment. Gong's iframe takes exact from/to but demands the viewer be signed into Gong, in an iframe, not in Safari. Gong gains a media path it never had: /v2/calls/extensive with exposedFields.media, and the tenant's real call URL read off metaData.url instead of the hardcoded us-4796 subdomain. Its 403 returns `permission_required`, deliberately not `unavailable` — the recording exists and Cedar may not fetch it, which is a problem with a named owner and a one-minute fix, so the player names the api:calls:read:media-url scope and where a Gong admin grants it. The connection form now asks for it up front. This lives in gong-recording.ts, not gong.ts, because gong.ts reaches crm/meeting-events -> meetings/index.ts -> back to gong.ts, and importing the minter from there crashes on load with a TDZ error. Nothing mints until the reader presses play: Fathom allows 30 download requests per 60s per key and renders a cold recording in ~34s, so a document with ten moments that minted eagerly would spend a third of that budget before anyone pressed anything. Not verified, and called out in the design doc: no live mint ran (this machine's ORG_CREDENTIALS_ENCRYPTION_KEY does not match those connection rows), the Gong path has never run against Gong since no org has a connection, and whether a seek lands on the right words is still unproven — Fathom's call started_at and recording.started_at differ by 62s on the one sample inspected. Also carries a pre-existing working-tree change to fathom.ts that could not be separated from the same file: transcript turns keep their timestamp, so the coaching rubric's discovery-length row is scoreable for webhook-ingested calls. Its test is included rather than left orphaned. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 29, 2026, 3:02 PMrecording-fence.test.tsfeat(documents): play the call inside the document, at the moment being discussed A coaching note the rep cannot check against the tape is an assertion, and until now checking it meant leaving the document for a provider page that opened at 0:00. A ```moment card now embeds the call positioned at `at:`, every timestamp in its transcript is a button that seeks the player to that line, and a new ```recording fence embeds a whole call anywhere in a document. Playback resolves from `externalId`, never a stored URL. That is not a preference: Circleback's `recording_url` is a signed link that expired 24h after ingest, so every Circleback moment's "mandatory recording link" was already a 400, and Fathom's is a web page rather than media. The id is stable, and the server mints from it per view. Cedar's own <video> rather than a provider embed, because only an element we own can be seeked — verified against the live providers rather than assumed. Fathom's /embed/<token> IS frameable (no X-Frame-Options, no frame-ancestors, unlike /share), but its component declares only {call, autoplay, shareUrl, displayMode} and nine probed parameter names changed nothing, so it cannot be positioned; it is kept as the last rung of the fallback ladder. Fathom's share page does deep-link — ?timestamp=125 comes back as currentTime 125.0 — so links out carry the moment. Gong's iframe takes exact from/to but demands the viewer be signed into Gong, in an iframe, not in Safari. Gong gains a media path it never had: /v2/calls/extensive with exposedFields.media, and the tenant's real call URL read off metaData.url instead of the hardcoded us-4796 subdomain. Its 403 returns `permission_required`, deliberately not `unavailable` — the recording exists and Cedar may not fetch it, which is a problem with a named owner and a one-minute fix, so the player names the api:calls:read:media-url scope and where a Gong admin grants it. The connection form now asks for it up front. This lives in gong-recording.ts, not gong.ts, because gong.ts reaches crm/meeting-events -> meetings/index.ts -> back to gong.ts, and importing the minter from there crashes on load with a TDZ error. Nothing mints until the reader presses play: Fathom allows 30 download requests per 60s per key and renders a cold recording in ~34s, so a document with ten moments that minted eagerly would spend a third of that budget before anyone pressed anything. Not verified, and called out in the design doc: no live mint ran (this machine's ORG_CREDENTIALS_ENCRYPTION_KEY does not match those connection rows), the Gong path has never run against Gong since no org has a connection, and whether a seek lands on the right words is still unproven — Fathom's call started_at and recording.started_at differ by 62s on the one sample inspected. Also carries a pre-existing working-tree change to fathom.ts that could not be separated from the same file: transcript turns keep their timestamp, so the coaching rubric's discovery-length row is scoreable for webhook-ingested calls. Its test is included rather than left orphaned. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 29, 2026, 3:02 PMrecording-timestamps.test.tsfeat(documents): play the call inside the document, at the moment being discussed A coaching note the rep cannot check against the tape is an assertion, and until now checking it meant leaving the document for a provider page that opened at 0:00. A ```moment card now embeds the call positioned at `at:`, every timestamp in its transcript is a button that seeks the player to that line, and a new ```recording fence embeds a whole call anywhere in a document. Playback resolves from `externalId`, never a stored URL. That is not a preference: Circleback's `recording_url` is a signed link that expired 24h after ingest, so every Circleback moment's "mandatory recording link" was already a 400, and Fathom's is a web page rather than media. The id is stable, and the server mints from it per view. Cedar's own <video> rather than a provider embed, because only an element we own can be seeked — verified against the live providers rather than assumed. Fathom's /embed/<token> IS frameable (no X-Frame-Options, no frame-ancestors, unlike /share), but its component declares only {call, autoplay, shareUrl, displayMode} and nine probed parameter names changed nothing, so it cannot be positioned; it is kept as the last rung of the fallback ladder. Fathom's share page does deep-link — ?timestamp=125 comes back as currentTime 125.0 — so links out carry the moment. Gong's iframe takes exact from/to but demands the viewer be signed into Gong, in an iframe, not in Safari. Gong gains a media path it never had: /v2/calls/extensive with exposedFields.media, and the tenant's real call URL read off metaData.url instead of the hardcoded us-4796 subdomain. Its 403 returns `permission_required`, deliberately not `unavailable` — the recording exists and Cedar may not fetch it, which is a problem with a named owner and a one-minute fix, so the player names the api:calls:read:media-url scope and where a Gong admin grants it. The connection form now asks for it up front. This lives in gong-recording.ts, not gong.ts, because gong.ts reaches crm/meeting-events -> meetings/index.ts -> back to gong.ts, and importing the minter from there crashes on load with a TDZ error. Nothing mints until the reader presses play: Fathom allows 30 download requests per 60s per key and renders a cold recording in ~34s, so a document with ten moments that minted eagerly would spend a third of that budget before anyone pressed anything. Not verified, and called out in the design doc: no live mint ran (this machine's ORG_CREDENTIALS_ENCRYPTION_KEY does not match those connection rows), the Gong path has never run against Gong since no org has a connection, and whether a seek lands on the right words is still unproven — Fathom's call started_at and recording.started_at differ by 62s on the one sample inspected. Also carries a pre-existing working-tree change to fathom.ts that could not be separated from the same file: transcript turns keep their timestamp, so the coaching rubric's discovery-length row is scoreable for webhook-ingested calls. Its test is included rather than left orphaned. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 29, 2026, 3:02 PMscorecard-fence.test.tsfix(coaching): decimal scale, stricter call classification, Meeting Coaching Log 1. Scores are 0.0-5.0 to one decimal, with a named anchor at every integer (5 exemplary / 4 solid / 3 partial / 2 token / 1 absent / 0 counter- productive / na no opportunity). Most real calls land between anchors, so most scores should carry a decimal — a file of flat 1s and 3s means the scorer reached for the anchor instead of judging the call. 2. Fixes the classification bug. The prompt said "reps routinely run both in one call" and "most calls get both", with no bar on what counts as a discovery portion — so a three-minute "remind me what you have" opener before a re-demo was labelled discovery+demo. Now: most calls are ONE or the other, a discovery portion needs 5+ minutes of exploratory questioning, and where there is none Discovery is `na` rather than a row of 1s. Scoring D1-D5 on a demo call hands the rep five 1s for a call type he was never running. Marmon Mok reclassifies from "Discovery + demo" to "Demo" on that rule. 3. The rundown becomes the Meeting Coaching Log: heading is conversation name — calendar event name, with date and the Fathom link beneath it, then a one-sentence summary rather than a "biggest miss" label (a sentence can carry what went well AND what did not; a label forces a complaint). A `---` rule after every meeting so entries stay distinct. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 25, 2026, 8:13 AMtaskListRowLayout.test.tsfix(coaching): restore the two discovery rows, and put checklist text beside its box Peter asked for the two discovery rows back. Both were dropped for reasons that still hold, so both come back carrying them rather than quietly reversed. D4 "Discovery ran long enough" keeps the caveat it was written with — every blind agent in the Aug 19 analysis called discovery length a non-signal, several of the longest discoveries are losses, and total call duration came out null on advance. The row is reported, never presented as a driver, and the test now asserts the warning is present rather than asserting the row is absent: an edit that keeps the row and deletes the caveat turns a reported number back into a claimed cause, which is what got it dropped the first time. D5 "Recapped and tied back before demoing" was folded into M2 when it went. With it back, M2 keeping that sentence would score one behaviour in two rows and drag a rep's average twice for a single miss, so M2 now points at D5 instead of absorbing it. A demo can open with a clean recap and still be a feature dump; the two failures need different coaching. Publishing a rubric edit also gets its own script. `copy-coaching-to-pirros.ts` reads as a rubric seeder and is not one — it calls `publishCoachingSuite`, which rewrites every per-meeting doc, log and overview from local `/tmp/cdocs*` snapshots. Running it to ship this change reverted ~3,000 words of newer coaching across two rep logs. `publish-coaching-playbook.ts` writes the three instruction documents and nothing else. Separately, in documents: a task item renders as `<li><label><input></label><div>`, and there was no layout rule for the li at all — only the input was ever styled. The label is inline and the div is block, so the text began on the line BELOW the checkbox, a full line rather than a nudge. The row is now a flex line. The 1px baseline nudge stays on the remarkGfm path, where the input really is an inline sibling and still needs it, and comes off the TipTap path, where the flex label centres the box and the nudge would double-count. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 29, 2026, 1:49 PM