fence-attrs.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 PMmoment-fence.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 PMMomentFenceNode.tsxfeat(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.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 PMScorecardFenceNode.tsxfix(coaching): keep the dry-run CLI a thin client, and use absolute imports The no-heavy-imports guard caught coaching-dryrun importing services/*. convention-paths drags in documents/index — the DB handle, drizzle and env — which is exactly the tsx/ESM boot weight the CLI-over-HTTP migration removed. repSlugFromEmail was a pure three-line helper that merely happened to live in that heavy module, and nothing inside it used the function. Split it into its own module so the CLI shares the real implementation instead of reimplementing it and drifting. seed-content imports nothing at all, so both it and rep-slug are allow-listed as pure, with the reasoning recorded next to the exceptions. Also switches the coaching fence node views to absolute imports, per the frontend convention. Includes markdown-prose-classes.ts from concurrent work in the tree. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 25, 2026, 6:10 PM