/** * agent-notes — the collapsed section at the end of a PR body that holds every line gstack's tools read * (R76: the front of a body is for people, the end is for machines). The section is the marker line, then * `
Agent notes`, a blank line, the content, a blank line and `
`; there is one, * at the very end of the body (a Capy badge a bot appends may follow it). It carries the ship receipt and its runs * block, the Approvals table and Plan line, issue and Refs lines, ruled notes and the exception lines /ship writes, so * the readable part above it opens with plain sentences. * * The marker counts only on a line of its own outside a code fence, so a body that quotes it in an example never * opens a section; the last such line wins. `` closes the section only on its own line outside a fence, so a * receipt block inside the notes cannot end it early. A body with no marker has no section (`notes: null`): readers * then read the whole body, which keeps PRs posted before this section existed working. */ export const AGENT_NOTES_MARKER = ''; export const AGENT_NOTES_OPEN = '
Agent notes'; const CLOSE = '
'; const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/; const fenceCloses = (fence: string, line: string) => new RegExp(`^ {0,3}${fence[0] === '`' ? '`' : '~'}{${fence.length},}\\s*$`).test(line); interface Parsed { readable: string; notes: string | null; after: string } /** Line indexes of `wanted` lines outside code fences, from `from` on. */ function linesOutsideFences(lines: string[], from: number, wanted: string): number[] { const hits: number[] = []; let fence: string | null = null; for (let i = from; i < lines.length; i++) { const line = lines[i]!.replace(/\r?\n$/, ''); if (fence !== null) { if (fenceCloses(fence, line)) fence = null; continue; } const open = FENCE_OPEN.exec(line); if (open) { fence = open[1]!; continue; } if (line.trim() === wanted) hits.push(i); } return hits; } /** * The section's lines in a body split after each newline: the marker line, the line after the wrapper's opener, and * the closing `` line (`lines.length` when it never closes); null when the body has no section. */ export function agentNotesLines(lines: string[]): { marker: number; start: number; close: number } | null { const marker = linesOutsideFences(lines, 0, AGENT_NOTES_MARKER).at(-1); if (marker === undefined) return null; let start = marker + 1; while (start < lines.length && lines[start]!.trim() === '') start++; if (start < lines.length && /^]*>\s*(?:[^<]*<\/summary>)?$/i.test(lines[start]!.trim())) { start++; if (start < lines.length && /^[^<]*<\/summary>$/i.test(lines[start]!.trim())) start++; } return { marker, start, close: wrapperClose(lines, start) }; } /** * The `` line that closes the wrapper: the first one outside fences with every `
` opened inside the * notes closed, or, when a `
` in the notes never closes, the first `
` line outside fences. */ function wrapperClose(lines: string[], from: number): number { let depth = 0; let first = -1; let fence: string | null = null; for (let i = from; i < lines.length; i++) { const line = lines[i]!.replace(/\r?\n$/, ''); if (fence !== null) { if (fenceCloses(fence, line)) fence = null; continue; } const open = FENCE_OPEN.exec(line); if (open) { fence = open[1]!; continue; } if (line.trim() === CLOSE && depth === 0) return i; if (line.trim() === CLOSE && first < 0) first = i; const prose = line.replace(/`[^`]*`/g, ''); depth = Math.max(0, depth + (prose.match(//gi)?.length ?? 0)); } return first >= 0 ? first : lines.length; } /** The body around its section: the text above the marker, the content inside the wrapper, and any text after `
`. */ function parse(body: string): Parsed { const lines = body.split(/(?<=\n)/); const at = agentNotesLines(lines); if (!at) return { readable: body, notes: null, after: '' }; const { marker, start, close: end } = at; return { readable: lines.slice(0, marker).join(''), notes: lines.slice(start, end).join('').replace(/^(?:[ \t]*\r?\n)+/, '').replace(/\s+$/, ''), after: lines.slice(end + 1).join(''), }; } /** The text above the Agent notes marker, and the section's content (`null` when the body has no section). */ export function splitAgentNotes(body: string): { readable: string; notes: string | null } { const { readable, notes } = parse(body); return { readable, notes }; } /** What machine readers parse: the section's content when the body has one, else the whole body (a legacy body). */ export function agentNotesOrBody(body: string): string { return parse(body).notes ?? body; } /** The section for `content`: the marker, the collapsed wrapper, and the content between blank lines. */ export function renderAgentNotes(content: string): string { return `${AGENT_NOTES_MARKER}\n${AGENT_NOTES_OPEN}\n\n${content.trim()}\n\n${CLOSE}\n`; } /** `body` with its section's content replaced by `content` (the section appended when the body has none); text after `` stays after it. */ export function withAgentNotes(body: string, content: string): string { const { readable, after } = parse(body); const lead = readable.replace(/\s+$/, ''); const tail = after.replace(/^\s+/, ''); return (lead ? `${lead}\n\n` : '') + renderAgentNotes(content) + (tail ? `\n${tail}` : ''); }