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