import type { AskUserQuestionFingerprint } from './claude-pty-runner'; import type { NativePlanQuestionCall } from './plan-count-transcript'; // A header may repeat its own native decision ID. A foreign ordinal cannot // rename a substantive question into navigation. function navigationHeader(q: NativePlanQuestionCall['questions'][number]): string { const header=/^(D[1-9]\d*)\s+(.+)$/.exec(q.header.trim()); if(!header)return q.header.trim(); return new RegExp(`^${header[1]}\\s*[—–:-]`).test(q.question) ? header[2]! : ''; } /** Count a completed review-navigation choice separately; never choose pending input. */ export function isEngCompletionHandoff(fp: AskUserQuestionFingerprint, reviewedPlan: string, priorCalls: readonly NativePlanQuestionCall[] = []): boolean { const call = fp.nativeCall; if (!call?.sessionId || !call.toolUseId || call.answered !== true || call.failed !== false || fp.signature !== `${call.sessionId}:${call.toolUseId}` || call.questions.length !== 1 || !Array.isArray(call.unansweredQuestionIndices) || call.unansweredQuestionIndices.length || (fp.nativeQuestionIndex !== undefined && fp.nativeQuestionIndex !== 0) || Object.keys(call.answers ?? {}).length !== 1 || !Number.isFinite(Date.parse(call.answeredAt ?? ''))) return false; if (isPublishedTaskPauseNavigation(fp, reviewedPlan, priorCalls) || isCurrentLedgerNavigation(fp, reviewedPlan, priorCalls) || isApprovedInvestigationRecap(fp, reviewedPlan, priorCalls) || isPublishedReadyNavigation(fp, reviewedPlan, priorCalls) || isApprovedMaintenanceRecap(fp, reviewedPlan, priorCalls) || isPublishedPrerequisiteHandoff(fp, reviewedPlan)) return true; const q = call.questions[0]!; if (q.multiSelect || navigationHeader(q) !== 'Next steps' || q.options.length !== 2 || fp.options.length !== 2 || !fp.options.every((o, i) => o.index === i + 1 && o.label === q.options[i]!.label) || !q.options.some(o => o.label === call.answers?.[q.question]) || / s.trim()).filter(Boolean); if (lines.length !== 4 || !/^Next steps: Eng Review is CLEAR\. This is a backend auth refactor with no UI scope, so \/plan-design-review does not apply\. No CEO review exists, but the plan changes no product direction\. What next\?$/.test(lines[0]!) || !/^Recommendation: [1-9]\d*[A-Z] because the plan is implementation-ready and a CEO review would add little to a pure infrastructure refactor\.$/.test(lines[1]!) || !/^Note: options differ in kind, not coverage [—–-] no completeness score\.$/.test(lines[2]!) || !/^Net: start building now versus one more optional review pass on scope\.$/.test(lines[3]!)) return false; const label = (s: string) => s.trim().replace(/^[1-9]\d*[A-Z]\)\s*/, '').replace(/\s*\(recommended\)$/, ''); const ready = q.options.find(o => /^Ready to implement [—–-] run \/ship when done$/.test(label(o.label))); const ceo = q.options.find(o => label(o.label) === 'Run /plan-ceo-review first'); if (!ready || !ceo) return false; const task = /^Exit plan mode with the reviewed plan; implement T([1-9]\d*)[–-]T([1-9]\d*) \(record T([1-9]\d*) regression fixtures first\)\. ✅ All required reviews complete and logged\. ✅ Tasks JSONL and QA test plan are already written for \/autoplan and \/qa\. ❌ No second-opinion pass since codex reviews are disabled\.$/.exec(ready.description ?? ''); // Bind implementation references to the published reviewed task catalog. // A new task or a newly proposed regression step is still substantive work. if (!task || Number(task[1]) !== 1 || Number(task[2]) < Number(task[3]) || Number(task[3]) < Number(task[1])) return false; const tasks = [...reviewedPlan.matchAll(/^- \[ \] \*\*T([1-9]\d*)\b[^\n]+$/gm)]; const ids = tasks.map(t => Number(t[1])).sort((a, b) => a - b); if (ids.length !== Number(task[2]) || ids.some((id, i) => id !== i + 1)) return false; const regression = tasks.find(t => t[1] === task[3])?.[0] ?? ''; if (!/ [—–] Record regression(?: characterization)? fixtures before\b/i.test(regression)) return false; return ceo.description === 'Optional scope and strategy pass before implementing. ✅ Catches product-level questions the eng review does not ask. ✅ Adds a CEO row to the dashboard. ❌ Little product surface here; likely confirms the current scope.'; } /** A completed report can retain critical implementation gaps (ISSUES OPEN). * Its current decision states, actual earlier answers and published task order * must still agree. This route supplies navigation credit only. */ function isCurrentLedgerNavigation(fp: AskUserQuestionFingerprint, plan: string, prior: readonly NativePlanQuestionCall[]): boolean { const call = fp.nativeCall!, q = call.questions[0]!; const compact = (s: string) => s.replace(/\s+/g, ' ').trim(); const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const label = (s: string) => compact(s.replace(/\*\*/g, '')).replace(/^(?:[1-9]\d*)?[A-Z]\s*[).:—–-]\s*/, '').replace(/\s*\((?:recommended|optional)\)$/i, ''); const currentText = (s: string) => s.replace(/(?:^|\n)(?:Earlier|Previous|Historical|Example|Quoted)\b[^\n]*:\s*(?:"[^"\n]*"|“[^”\n]*”)\s*$/gmi, ''); if (q.multiSelect || !/^Next(?: steps?)?$/i.test(navigationHeader(q)) || q.options.length !== 2 || fp.options.length !== 2 || !fp.options.every((o, i) => o.index === i + 1 && o.label === q.options[i]!.label)) return false; const ready = q.options.find(o => /^Ready to implement(?: [—–-] run \/ship when done)?$/i.test(label(o.label))); const ceo = q.options.find(o => /^Run \/plan-ceo-review(?: first)?$/i.test(label(o.label))); if (!ready || !ceo || call.answers?.[q.question] !== ready.label) return false; const context = currentText([q.question, ...q.options.map(o => `${o.label}\n${o.description ?? ''}`)].join('\n')); const positive = context.replace(/"[^"\n]*"|“[^”\n]*”|'[^'\n]*'|‘[^’\n]*’/g, ''); const status = context.replace(/["“”'‘’]/g, ''); const badStatus = /\b(?:review|decisions?(?: [A-Z][1-9]\d*)?|readiness|state|accepted scope)\s*(?:(?:is|are|remains?|has been)\s+|:\s*)?(?:now |still )?(?:incomplete|unfinished|pending|unanswered|unresolved|reopened|withdrawn|superseded|cancelled|canceled|rejected|revoked|denied|unapproved|(?:not|no longer) (?:complete|completed|done|finished|approved|answered|settled))\b|\b(?:not all|not every) decisions?\b|\b[1-9]\d* unresolved decisions?\b/i; if (badStatus.test(status) || /`{3}|~{3}|(?:^|\n)\s*>/.test(context) || !/\breview (?:is |has been )?(?:complete[d]?|done|finished)\b/i.test(positive) || !/\bwhat next\b|\bnext steps?\b/i.test(positive) || !/\b(?:every decision is|all decisions are) (?:answered|settled)\b/i.test(positive) || /\breview\b[^.!?;\n]{0,60}\b(?:will|would|may|might|could|should) (?:be )?(?:complete|done|finished)\b|\breview\b[^.!?;\n]{0,60}\b(?:complete|done|finished)\b[^.!?;\n]{0,60}\b(?:if|when|once|unless|provided|assuming|after)\b|\b(?:if|when|once|unless|provided|assuming)\b[^.!?;\n]{0,60}\breview\b[^.!?;\n]{0,60}\b(?:complete|done|finished)\b/i.test(status)) return false; // Fences, quoted lines and explicitly historical sections cannot establish a // current owner. Keep the heading hierarchy so an archived parent is inert. const lines: string[] = [], headings: { depth: number; inactive: boolean }[] = []; let fence: string | undefined; for (const line of plan.split(/\r?\n/)) { const mark = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line); if (mark) { if (!fence) fence = mark[1]; else if (mark[1]![0] === fence[0] && mark[1]!.length >= fence.length && !mark[2]!.trim()) fence = undefined; continue; } if (fence || /^(?: {4}|\t| {0,3}>)/.test(line)) continue; const h = /^(#{1,6}) (.+)$/.exec(line); if (h) { while (headings.at(-1) && headings.at(-1)!.depth >= h[1]!.length) headings.pop(); headings.push({ depth: h[1]!.length, inactive: /\b(?:history|historical|archived?|withdrawn|superseded|example|quoted|template)\b/i.test(h[2]!) }); } if (!headings.some(h => h.inactive)) lines.push(line); } if (fence) return false; const current = currentText(lines.join('\n')); const section = (name: string) => { const starts = lines.flatMap((line, i) => new RegExp(`^#{2,3} ${escape(name)}$`, 'i').test(line) ? [i] : []); if (starts.length !== 1) return undefined; const start = starts[0]!, depth = /^#+/.exec(lines[start]!)![0].length; const end = lines.findIndex((line, i) => i > start && new RegExp(`^#{1,${depth}} `).test(line)); return lines.slice(start + 1, end < 0 ? undefined : end).join('\n'); }; const titles = [...current.matchAll(/^# (?:Plan: )?(.+?)(?: \(reviewed\))?$/gm)]; const targets = [...current.matchAll(/^Reviewed target: `([^`\n]+\.md)` \("([^"]+)"\)[^\n]*, branch `([^`\n]+)`[^\n]*$/gm)]; if (titles.length !== 1 || targets.length !== 1 || compact(titles[0]![1]!) !== compact(targets[0]![2]!.replace(/^Plan: /i, ''))) return false; const source = targets[0]![1]!, branch = targets[0]![3]!; const metadata = q.question.split('\n').filter(s => /^Project\/branch\/task:/.test(s)); if (metadata.length !== 1 || !new RegExp(`^Project/branch/task: ${escape(branch)}; /plan-eng-review of ${escape(source)} (?:finished|completed|done),`).test(metadata[0]!)) return false; const ledger = section('Decision ledger'), tasks = section('Implementation Tasks'), lanes = section('Worktree parallelization strategy'), report = section('GSTACK REVIEW REPORT'); if (!ledger || !tasks || !lanes || !report || badStatus.test(currentText(report).replace(/["“”'‘’]/g, '')) || report.trim().split('\n').at(-1) !== 'NO UNRESOLVED DECISIONS') return false; const reportRows = report.split('\n').filter(s => /^\| Eng Review \|/.test(s)); const verdicts = report.split('\n').filter(s => /^(?:- )?\*\*VERDICT:\*\*/.test(s)); const gaps = reportRows.length === 1 ? /\| (CLEAR|ISSUES OPEN) \|[^\n]*\b(\d+) critical gaps\b/.exec(reportRows[0]!) : null; if (!gaps || verdicts.length !== 1 || !new RegExp(`\\bEng Review (?:is )?${gaps[1]}\\b`, 'i').test(verdicts[0]!) || !/\b0 unresolved decisions\b/.test(verdicts[0]!) || (gaps[1] === 'CLEAR') !== (+gaps[2]! === 0) || !new RegExp(`\\b${gaps[2]} critical gaps\\b`).test(metadata[0]!)) return false; const identities = prior.map(c => `${c.sessionId}:${c.toolUseId}`); if (!prior.length || new Set(identities).size !== prior.length || prior.some(c => c.sessionId !== call.sessionId || !c.toolUseId || c.toolUseId === call.toolUseId || c.answered !== true || c.failed !== false || !Number.isFinite(Date.parse(c.answeredAt ?? '')) || Date.parse(c.answeredAt!) >= Date.parse(call.answeredAt!) || !Array.isArray(c.unansweredQuestionIndices) || c.unansweredQuestionIndices.length || c.questions.length !== 1 || Object.keys(c.answers ?? {}).length !== 1 || c.questions[0]!.multiSelect || new Set(c.questions[0]!.options.map(o => o.label)).size !== c.questions[0]!.options.length || !c.questions[0]!.options.some(o => o.label === c.answers?.[c.questions[0]!.question]))) return false; const approvals = prior.map(c => ({ call: c, q: c.questions[0]!, id: /^(D[1-9]\d*)\s*[—–:-]/.exec(c.questions[0]!.question)?.[1], selected: label(c.answers![c.questions[0]!.question]!) })); const finalId = /^(D[1-9]\d*)\s*[—–:-]/.exec(q.question)?.[1]; if (!finalId || approvals.some(a => !a.id || a.id === finalId) || new Set(approvals.map(a => a.id)).size !== approvals.length) return false; const rows = ledger.split(/\n(?=### )/).filter(s => /^### /.test(s.trim())); const rowIds = rows.map(s => /^### ([A-Z][1-9]\d*):/.exec(s.trim())?.[1]); const states = [...ledger.matchAll(/^State: (.+)$/gm)]; const owned = new Map(); if (!rows.length || rowIds.some(id => !id) || new Set(rowIds).size !== rows.length || states.length !== rows.length || states.some(s => s[1] !== 'approved')) return false; for (const row of rows) { const field = (name: string) => { const matches = [...row.matchAll(new RegExp(`^${name}: (.+)$`, 'gm'))]; return matches.length === 1 ? matches[0]![1]! : undefined; }; const answer = field('Actual answer'), scope = field('Accepted scope'); const question = [...row.matchAll(/^Question (D[1-9]\d*): (.+)$/gm)]; if (field('State') !== 'approved' || !answer || !scope || question.length !== 1 || badStatus.test(currentText(row).replace(/["“”'‘’]/g, ''))) return false; const id = question[0]![1]!, a = approvals.find(a => a.id === id); if (!a || owned.has(id) || !answer.endsWith(`(${id})`) || label(answer.slice(0, -id.length - 3)) !== a.selected) return false; const meta = a.q.question.split('\n').filter(s => /^Project\/branch\/task:/.test(s)); if (meta.length !== 1 || !new RegExp(`^Project/branch/task: ${escape(branch)}; [^;\n]*\\bon ${escape(source)}(?=[ ,;])`).test(meta[0]!) || /\b(?:compare|comparison|historical|archived?|withdrawn|superseded)\b/i.test(meta[0]!.split(`on ${source}`)[0]!) || [...meta[0]!.split(`on ${source}`)[0]!.matchAll(/[\w./-]+\.md\b/g)].some(m => m[0] !== 'TODOS.md')) return false; const owner = /^### ([A-Z][1-9]\d*):/.exec(row)![1]!; const changed = new RegExp(`\\b(?:${owner}|${id})\\b(?: (?:decision|scope|state|approval))?\\s*(?::|is|was|has been|remains)?\\s*(?:now |still )?(?:pending|withdrawn|superseded|reopened|cancelled|canceled|rejected|revoked|denied|unapproved|not approved|no longer approved)\\b`, 'i'); if (changed.test(currentText(row).replace(/["“”'‘’]/g, '')) || prior.some(c => Date.parse(c.answeredAt!) > Date.parse(a.call.answeredAt!) && changed.test(currentText(c.questions[0]!.question).replace(/["“”'‘’]/g, '')))) return false; owned.set(id, row); } const readiness = ledger.split('\n').filter(s => /^Approval readiness:/.test(s)); const readyRefs = readiness.length === 1 ? [...readiness[0]!.matchAll(/\b([A-Z][1-9]\d*) \((D[1-9]\d*)\)/g)] : []; if (readiness.length !== 1 || !/^Approval readiness: PASS\b/.test(readiness[0]!) || readyRefs.length !== rows.length || new Set(readyRefs.map(r => r[2])).size !== rows.length || readyRefs.some(r => !owned.get(r[2]!)?.startsWith(`### ${r[1]}:`))) return false; const entries = [...tasks.matchAll(/^- \[ \] \*\*(T[1-9]\d*)\b[^\n]+/gm)]; const ids = entries.map(e => e[1]!); if (!ids.length || new Set(ids).size !== ids.length) return false; for (const ref of context.matchAll(/\bT([1-9]\d*)(?:\s*(?:[–-]|through|to)\s*T([1-9]\d*))?\b/g)) { const first = +ref[1]!, last = +(ref[2] ?? ref[1])!; if (last < first || last - first >= ids.length) return false; for (let n = first; n <= last; n++) if (!ids.includes(`T${n}`)) return false; } const taskBody = (id: string) => { const e = entries.find(e => e[1] === id); return e ? tasks.slice(e.index!, entries.find(v => v.index! > e.index!)?.index ?? tasks.length) : ''; }; if (/\bT[1-9]\d*\b[^.!?\n]*\b(?:withdrawn|cancelled|canceled|rejected|not approved|pending approval)\b/i.test(currentText(tasks).replace(/["“”'‘’]/g, ''))) return false; const count = /\bimplement (?:the )?(\d+) tasks\b/i.exec(positive); if (!count || +count[1]! !== ids.length) return false; const laneIds = [...lanes.matchAll(/\bLane ([A-Z]):/g)].map(m => m[1]!); const execution = lanes.split('\n').filter(s => /^Execution(?: order)?:/.test(s)); const menuOrder = [...context.matchAll(/\blane order \(([^)]+)\)/gi)]; const groups = (text: string) => text.split(/,?\s*then\s+|\s*→\s*/i).map(s => s.trim().replace(/\s+parallel$|\s+sequential(?:ly)?$/i, '').split(/\s*\+\s*|\s+and\s+/).sort()); const publishedGroups = execution.length === 1 ? [...execution[0]!.matchAll(/\b(?:launch|then) ([A-Z](?:\s*(?:\+|and)\s*[A-Z])*)(?: in parallel(?: worktrees)?| sequentially)?(?=[.,]|$)/gi)].map(m => groups(m[1]!)[0]!) : []; if (!laneIds.length || new Set(laneIds).size !== laneIds.length || !publishedGroups.length || menuOrder.length !== 1 || JSON.stringify(groups(menuOrder[0]![1]!)) !== JSON.stringify(publishedGroups) || JSON.stringify(publishedGroups.flat().sort()) !== JSON.stringify([...laneIds].sort())) return false; // Strip only already-owned implementation/navigation recaps before applying // the established action veto. Approval references are read from this packet. let actions = context.replace(/\bimplement (?:the )?\d+ tasks in the lane order given\b/gi, '') .replace(/\bimplement T[1-9]\d*[-–]T[1-9]\d* in lane order \([^)]+\)/gi, ''); const maintenance = /(?:First two edits after exit|Post-exit|After exiting): ((?:append|add) (?:gstack )?routing rules to CLAUDE\.md) \((D[1-9]\d*)\) and ((?:create|write) TODOS\.md) \((D[1-9]\d*)\)\./i.exec(actions); if (maintenance) { const routing = approvals.find(a => a.id === maintenance[2]), todo = approvals.find(a => a.id === maintenance[4]); const sameTask = entries.some(e => { const b = taskBody(e[1]!); return /\bCLAUDE\.md\b/.test(b) && /\bTODOS\.md\b/.test(b) && [maintenance[2], maintenance[4]].every(id => new RegExp(`\\b${id}\\b`).test(b)); }); if (!routing || !todo || routing.q.header !== 'Routing' || !/^(?:Add|Append) (?:gstack )?routing rules(?: to CLAUDE\.md)?$/i.test(routing.selected) || !/\bCLAUDE\.md\b/.test(routing.q.question) || !/^TODO(?: [1-9]\d*)?$/.test(todo.q.header) || todo.selected !== 'Add to TODOS.md' || !owned.has(todo.id!) || !sameTask || !new RegExp(`^Project/branch/task: ${escape(branch)}(?: branch|;)`).test(routing.q.question.split('\n')[1] ?? '')) return false; for (const a of [routing, todo]) { const withdrawn = new RegExp(`\\b${a.id}\\b(?: (?:decision|scope|state|approval))?\\s*(?::|is|was|has been|remains)?\\s*(?:now |still )?(?:pending|withdrawn|superseded|reopened|cancelled|canceled|rejected|revoked|denied|unapproved|not approved|no longer approved)\\b`, 'i'); if (withdrawn.test(current.replace(/["“”'‘’]/g, '')) || prior.some(c => Date.parse(c.answeredAt!) >= Date.parse(a.call.answeredAt!) && withdrawn.test(currentText(c.questions[0]!.question).replace(/["“”'‘’]/g, '')))) return false; } actions = actions.replace(maintenance[0], ''); } actions = actions.replace(/\bAdds? a scope\/strategy pass(?: and a (?:written )?([a-z]+(?: [a-z]+)+) the plan currently lacks \((T[1-9]\d*)\))?(?=[.!?]|$)/gi, (whole, noun, id) => !noun || new RegExp(`\\b${escape(noun)}\\b`, 'i').test(taskBody(id).split('\n')[0] ?? '') ? '' : whole); const action = /(?:^|[.!?;]\s+|\n|[✅❌]\s*|["“'‘]\s*|\b(?:and|but|also|first|then|now|next|while|before (?:implementation|building|review))\s+)(?:please\s+)?(?:adds?|adding|append(?:s|ing)?|remov(?:e|es|ing)|delet(?:e|es|ing)|cut(?:s|ting)?|drop(?:s|ping)?|replac(?:e|es|ing)|rewrit(?:e|es|ing)|chang(?:e|es|ing)|alter(?:s|ing)?|modif(?:y|ies|ying)|enabl(?:e|es|ing)|disabl(?:e|es|ing)|implement(?:s|ing)?|install(?:s|ing)?|introduc(?:e|es|ing)|build(?:s|ing)?|writ(?:e|es|ing)|record(?:s|ing)?|captur(?:e|es|ing)|creat(?:e|es|ing)|switch(?:es|ing)?|migrat(?:e|es|ing)|externaliz(?:e|es|ing)|refactor(?:s|ing)?|expand(?:s|ing)?|reduc(?:e|es|ing)|deploy(?:s|ing)?|approv(?:e|es|ing))\b/i; return !action.test(actions) && !/\brun\s+(?!\/(?:ship|plan-ceo-review)\b)|\b(?:new|additional|extra) (?:work|task|requirement|dependency|feature)\b|\b(?:must|shall|should|needs? to|required to)\s+[a-z]/i.test(actions); } /** A ready menu may recap one previously approved investigation. The completed * native answer, current ledger and published task jointly own that exception; * it never supplies seed credit or replaces the runner's report/exit checks. */ function isApprovedInvestigationRecap(fp: AskUserQuestionFingerprint, plan: string, prior: readonly NativePlanQuestionCall[]): boolean { const call = fp.nativeCall!, q = call.questions[0]!; const compact = (s: string) => s.replace(/\s+/g, ' ').trim(); const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const label = (s: string) => compact(s).replace(/^(?:[1-9]\d*)?[A-Z][).:]\s*/, '').replace(/\s*\((?:recommended|optional)\)$/i, ''); if (q.multiSelect || !/^Next(?: steps?)?$/i.test(navigationHeader(q)) || q.options.length < 2 || q.options.length > 3 || fp.options.length !== q.options.length || !fp.options.every((o, i) => o.index === i + 1 && o.label === q.options[i]!.label) || new Set(q.options.map(o => label(o.label))).size !== q.options.length) return false; const ready = q.options.find(o => /^Ready to implement(?: [—–-] run \/ship when done)?$/i.test(label(o.label))); const ceo = q.options.find(o => /^(?:CEO review \(\/plan-ceo-review\)|Run \/plan-ceo-review(?: first)?)$/i.test(label(o.label))); const design = q.options.find(o => /^Design review \(\/plan-design-review\)$/i.test(label(o.label))); if (!ready || !ceo || q.options.length === 3 && !design || call.answers?.[q.question] !== ready.label) return false; const context = [q.question, ...q.options.map(o => `${o.label}\n${o.description ?? ''}`)].join('\n'); const body = compact(q.question.replace(/"[^"\n]*"|“[^”\n]*”/g, '')); const issue = /\b(?:follow-up|investigation)\s*\((R[1-9]\d*)\)/i.exec(body)?.[1]; const task = /\b(?:follow-up|investigation) task\s*\((T[1-9]\d*)\)\s*,?\s*(?:is )?not a blocker\b/i.exec(body)?.[1]; if (!issue || !task || !/^D[1-9]\d*\s*[—–:-]\s*Next steps? after this eng(?:ineering)? review\?/i.test(body) || !/\b(?:eng(?:ineering)? review|review) is (?:done|complete[d]?|finished)\b/i.test(body) || !/\b(?:every|all) P1 (?:fix(?:es)?|remed(?:y|ies)) (?:is|are) approved and specified\b/i.test(body) || !/\bremaining choice is whether another review pass\b|\bonly (?:remaining )?choice is (?:the )?next (?:review|workflow)\b/i.test(body) || !/\b(?:one|1) (?:\w+ )?(?:follow-up|investigation)\b/i.test(body) || design && !/\bno UI\b[\s\S]*\bdesign review does not apply\b/i.test(body) || /(?:^|\n)\s*>|`{3}|~{3}|\b(?:Example|Historical|Quoted)\s*:/i.test(context)) return false; // Descriptions can explain existing work. New commands, including commands // hidden in quoted/conjoined prose, are not navigation on either option. const action = /(?:^|[.!?;]\s+|\n|[✅❌]\s*|["“'‘]\s*|\b(?:and|but|also|first|then|now|next|while|before (?:implementation|building|review))\s+)(?:please\s+)?(?:add(?:ing)?|remov(?:e|ing)|delet(?:e|ing)|cut(?:ting)?|drop(?:ping)?|replac(?:e|ing)|rewrit(?:e|ing)|chang(?:e|ing)|alter(?:ing)?|modif(?:y|ying)|enabl(?:e|ing)|disabl(?:e|ing)|implement(?:ing)?|install(?:ing)?|introduc(?:e|ing)|build(?:ing)?|writ(?:e|ing)|record(?:ing)?|captur(?:e|ing)|creat(?:e|ing)|switch(?:ing)?|migrat(?:e|ing)|externaliz(?:e|ing)|refactor(?:ing)?|expand(?:ing)?|reduc(?:e|ing)|deploy(?:ing)?|approv(?:e|ing))\b/i; const statusText = (s: string) => s.replace(/(?:^|\n)(?:Earlier|Previous|Historical|Example|Quoted)\b[^\n]*:\s*(?:"[^"\n]*"|“[^”\n]*”)\s*$/gmi, '').replace(/["“”'‘’]/g, ''); const eng = String.raw`(?:(?:the |this )?(?:eng(?:ineering)? review|eng gate)|this review|the review|the verdict|all required reviews)`; const complete = String.raw`(?:clear(?:ed)?|complete[d]?|done|finished)`; const invalidCompletion = (s: string) => { const v = statusText(s); return new RegExp(String.raw`\b${eng}\b[^.!?;\n]{0,100}\b(?:incomplete|unfinished|pending|withdrawn|superseded|cancelled|canceled|reopened|rejected|not (?:done|complete|completed|clear|cleared|finished))\b`, 'i').test(v) || new RegExp(String.raw`\b${eng}\s+(?:will|would|may|might|can|could|should)\s+(?:be |become )?${complete}\b`, 'i').test(v) || new RegExp(String.raw`\b${eng}\b[^.!?;\n]{0,100}\b${complete}\b[^.!?;\n]{0,100}\b(?:if|when|once|unless|provided|assuming|after)\b`, 'i').test(v) || new RegExp(String.raw`\b(?:if|when|once|unless|provided|assuming)\b[^.!?;\n]{0,100}\b${eng}\b[^.!?;\n]{0,60}\b${complete}\b`, 'i').test(v) || /\bnot (?:all|every) P1 (?:fix(?:es)?|remed(?:y|ies))\b|\bP1 (?:fix(?:es)?|remed(?:y|ies))\b[^.!?;\n]{0,80}\b(?:not approved|unapproved|pending|reopened|withdrawn|superseded|cancelled|canceled|rejected)\b/i.test(v); }; const changedInvestigation = (s: string, owner = `${issue}|${task}|(?:the |this )?(?:investigation|follow-up|open item|pending item)`, genericApproval = true) => { const v = statusText(s).replace(/\bnot a blocker\b|\bNo (?:[\w-]+ )*implementation approved\./gi, ''); return new RegExp(String.raw`\b(?:${owner})\b(?: (?:task|decision|requirement|status|implementation))?\s*(?::|is|was|has been|remains)?\s*(?:now |still )?(?:a blocker|blocking|withdrawn|cancelled|canceled|rejected|reopened|superseded|not (?:required|approved)|no longer (?:optional|approved|nonblocking)|required before (?:implementation|building|work)|implementation (?:is )?(?:now )?approved)\b`, 'i').test(v) || genericApproval && /\bimplementation (?:is |now |is now )?approved\b/i.test(v); }; const allowedReask = new RegExp(`\\b${issue}(?: (?:stays|remains) (?:open|unresolved) and)? must be re-asked after ${task}\\b`, 'gi'); const actions = context.replace(allowedReask, ''); if (invalidCompletion(context) || changedInvestigation(context) || action.test(actions) || /\brun\s+(?!\/(?:ship|plan-ceo-review|plan-design-review)\b)/i.test(actions) || /\bimplementation (?:is |now |is now )?approved\b/i.test(actions) || /\b(?:new|additional|extra) (?:work|task|requirement|dependency|feature)\b|\b(?:must|shall|should|needs? to|required to)\s+[a-z]/i.test(actions) || /\b(?:review|P1 (?:fixes|remedies))\b[^.!?;\n]*\b(?:not done|not complete|not approved|pending approval|reopened|withdrawn)\b/i.test(context)) return false; const published: string[] = []; let fence: string | undefined; for (const line of plan.split(/\r?\n/)) { const mark = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line); if (mark) { if (!fence) fence = mark[1]; else if (mark[1]![0] === fence[0] && mark[1]!.length >= fence.length && !mark[2]!.trim()) fence = undefined; continue; } if (!fence && !/^(?: {0,3}>| {4}|\t)/.test(line)) published.push(line); } if (fence) return false; const current = published.filter(line => !/^(?:Earlier|Previous|Historical|Example|Quoted)\b[^\n]*:\s*(?:"[^"\n]*"|“[^”\n]*”)\s*$/i.test(line)).join('\n'); const section = (heading: RegExp): string | undefined => { const hits = published.flatMap((line, i) => heading.test(line) ? [i] : []); if (hits.length !== 1) return undefined; const start = hits[0]!, before = published.slice(0, start).filter(s => s.trim()).at(-1) ?? ''; if (/[::]$|\b(?:example|sample|hypothetical|template|quoted)\b/i.test(before)) return undefined; const end = published.findIndex((line, i) => i > start && /^#{1,2} /.test(line)); return published.slice(start + 1, end < 0 ? undefined : end).join('\n'); }; const titles = [...current.matchAll(/^# Plan: (.+?)(?: \(reviewed\))?$/gm)]; const targets = [...current.matchAll(/^Reviewed target: `([^`]+\.md)` on `([^`]+)`.*$/gm)]; const sourceLines = q.question.split('\n').filter(line => /^Project\/branch\/task:/.test(line)); if (titles.length !== 1 || targets.length !== 1 || sourceLines.length !== 1) return false; const title = titles[0]![1]!, source = targets[0]![1]!, branch = targets[0]![2]!; if (!new RegExp(`^Project/branch/task: [\\w.-]+ on ${escape(branch)}, ${escape(title)}(?: plan)?[.;]`).test(sourceLines[0]!)) return false; const ledger = section(/^## Decision ledger$/i), tasks = section(/^## Implementation Tasks$/), lanes = section(/^## Worktree parallelization strategy$/i), report = section(/^## GSTACK REVIEW REPORT$/); if (!ledger || !tasks || !lanes || !report) return false; const rows = ledger.split(/\n(?=### )/).filter(s => /^### /.test(s.trim())); const owned = rows.filter(s => new RegExp(`^### ${issue}: `).test(s.trim())); if (owned.length !== 1 || [...ledger.matchAll(/^State: pending\b/gmi)].length !== 1) return false; const row = owned[0]!; const field = (name: string) => { const found = [...row.matchAll(new RegExp(`^${name}: (.+)$`, 'gm'))]; return found.length === 1 ? found[0]![1]! : undefined; }; const finding = field('Finding'), state = field('State'), answer = field('Actual answer'), scope = field('Accepted scope'); if (!finding || !new RegExp(`(?:^|[ ,])${escape(source)}:\\d+\\b`).test(finding) || [...finding.matchAll(/[\w./-]+\.md:\d+/g)].some(m => !m[0].startsWith(source + ':')) || !/^pending \(Investigate\)$/i.test(state ?? '') || !answer || !scope || !/^bounded investigation only:/i.test(scope) || !/\bNo (?:[\w-]+ )*implementation approved\./i.test(scope) || !new RegExp(`\\b${issue} remains unresolved until re-asked\\b`, 'i').test(scope)) return false; const scopeRemainder = scope.replace(/\bNo (?:[\w-]+ )*implementation approved\./gi, ''); if (changedInvestigation(row) || action.test(scopeRemainder) || /\bimplementation (?:is |now |is now )?approved\b|\brun\s+/i.test(scopeRemainder)) return false; const identities = prior.map(c => `${c.sessionId}:${c.toolUseId}`); if (!prior.length || new Set(identities).size !== prior.length || prior.some(c => c.sessionId !== call.sessionId || !c.toolUseId || c.toolUseId === call.toolUseId || c.answered !== true || c.failed !== false || !Number.isFinite(Date.parse(c.answeredAt ?? '')) || Date.parse(c.answeredAt!) >= Date.parse(call.answeredAt!) || !Array.isArray(c.unansweredQuestionIndices) || c.unansweredQuestionIndices.length || !c.questions.length || Object.keys(c.answers ?? {}).length !== c.questions.length || new Set(c.questions.map(v => v.question)).size !== c.questions.length || c.questions.some(v => v.multiSelect || v.options.length < 2 || v.options.length > 4 || new Set(v.options.map(o => o.label)).size !== v.options.length || !v.options.some(o => o.label === c.answers?.[v.question])))) return false; const approvals = prior.flatMap(c => c.questions.map(v => ({call:c, question:v}))).filter(({question:v}) => new RegExp(`^D[1-9]\\d*\\s*[—–:-]\\s*${issue}:`).test(v.question)); if (approvals.length !== 1) return false; const approved = approvals[0]!, priorQuestion = approved.question, selected = approved.call.answers![priorQuestion.question]!; const decision = /^(D[1-9]\d*)\s*[—–:-]/.exec(priorQuestion.question)![1]!; if (prior.some(c => Date.parse(c.answeredAt!) > Date.parse(approved.call.answeredAt!) && c.questions.some(v => { const text = statusText(v.question); return new RegExp(`\\b${issue}\\b`).test(text) && changedInvestigation(text, issue, false); }))) return false; const priorTitle = priorQuestion.question.split('\n')[0]!.replace(new RegExp(`^${decision}\\s*[—–:-]\\s*${issue}:\\s*`), ''); const priorSource = priorQuestion.question.split('\n').filter(line => /^Project\/branch\/task:/.test(line)); const savedQuestions = [...row.matchAll(/^Question (D[1-9]\d*): "([^"\n]+)" Options: .+$/gm)]; if (!/^Investigate before choosing$/i.test(label(selected)) || priorSource.length !== 1 || !new RegExp('^Project/branch/task: `' + escape(branch) + '`, ' + escape(source) + ' ' + escape(title) + ';').test(priorSource[0]!) || savedQuestions.length !== 1 || savedQuestions[0]![1] !== decision || savedQuestions[0]![2] !== priorTitle || label(answer.replace(new RegExp(` \\(${decision}\\)$`), '')) !== label(selected) || !answer.endsWith(`(${decision})`)) return false; const approvedDescription = priorQuestion.options.find(o => o.label === selected)?.description ?? ''; if (!/\bre-asked\b/i.test(approvedDescription) || action.test(approvedDescription) || /\brun\s+|\bimplementation (?:is |now |is now )?approved\b/i.test(approvedDescription)) return false; const entries = [...tasks.matchAll(/^- \[ \] \*\*(T[1-9]\d*)\b[^\n]+$/gm)]; const ids = entries.map(m => m[1]!); const target = entries.filter(m => m[1] === task); const count = /\b([1-9]\d*) tasks\b/i.exec(ready.description ?? ''); const laneCount = /\b([1-9]\d*) (?:parallel )?lanes\b/i.exec(ready.description ?? ''); const laneIds = [...lanes.matchAll(/\bLane ([A-Z]):/g)].map(m => m[1]); if (!entries.length || new Set(ids).size !== ids.length || target.length !== 1 || !count || +count[1]! !== ids.length || !laneCount || +laneCount[1]! !== laneIds.length || new Set(laneIds).size !== laneIds.length) return false; for (const ref of context.matchAll(/\b(T[1-9]\d*)(?:\s*[-–]\s*(T[1-9]\d*))?\b/g)) { const first = +ref[1]!.slice(1), last = +(ref[2] ?? ref[1]!).slice(1); if (last < first || last - first >= ids.length) return false; for (let id = first; id <= last; id++) if (!ids.includes(`T${id}`)) return false; } const start = target[0]!.index!, end = entries.find(m => m.index! > start)?.index ?? tasks.length; const taskLines = tasks.slice(start, end).trim().split('\n').filter(s => s.trim() && !/^_/.test(s)); const taskBody = taskLines.join('\n'); const taskField = (name: string) => { const found = taskLines.filter(line => line.startsWith(` - ${name}: `)); return found.length === 1 ? found[0] : undefined; }; const surfaced = taskField('Surfaced by'), files = taskField('Files'), verify = taskField('Verify'); if (taskLines.length !== 4 || !/ — plan — (?:Enumerate|List|Inventory|Document)\b/i.test(taskLines[0]!) || !surfaced || !new RegExp(`^ - Surfaced by: .*\\b${issue} / ${decision}\\b`).test(surfaced) || !files || !/^ - Files: this plan, /i.test(files) || !verify || !new RegExp(`^ - Verify: .+; ${issue} re-asked$`, 'i').test(verify) || action.test(taskBody) || /\b(?:implement|build|deploy|delete|add|approve)\b/i.test(taskBody)) return false; const table = /\bin (?:the|this) plan's "([^"\n]+)" table\./i.exec(scope)?.[1]; if (!table || !files.includes('"' + table + '" table') || !new RegExp(`\\b${issue}\\b`).test(taskLines[0]!) || !/\b(?:enumerate|list|inventory|document)\b/i.test(taskLines[0]!)) return false; const status = new RegExp(`\\b(?:${issue}|${decision}|${task})\\b(?: (?:task|decision|requirement|status))?\\s*(?::|is|was|has been|remains)?\\s*(?:now |still )?(?:withdrawn|cancelled|canceled|rejected|reopened|superseded|not (?:required|approved)|implementation approved)\\b`, 'i'); if (status.test(current.replace(/["“”]/g, '')) || changedInvestigation(current, `${issue}|${decision}|${task}`, false) || invalidCompletion(report)) return false; const unresolved = report.split(/\*\*UNRESOLVED DECISIONS:\*\*/); if (unresolved.length !== 2 || !/\bEng Review ISSUES OPEN \(1 unresolved decision, 0 critical gaps\)/.test(report) || !/\b(?:All|Every) P1 (?:remedies|fixes) (?:are|is) approved and specified\b/i.test(report) || !/\bnot a blocker\b/i.test(report)) return false; const remaining = unresolved[1]!.trim().split('\n').filter(s => s.trim()); return remaining.length === 1 && new RegExp(`^- ${issue} / ${decision} [—–-] .+: Investigate; re-ask .+\\(${task}\\)$`, 'i').test(remaining[0]!); } function hasCompleteEarlierNativeAnswers(call: NativePlanQuestionCall, prior: readonly NativePlanQuestionCall[]): boolean { const identities = prior.map(c => `${c.sessionId}:${c.toolUseId}`); return !!prior.length && new Set(identities).size === prior.length && !prior.some(c => c.sessionId !== call.sessionId || c.toolUseId === call.toolUseId || !c.toolUseId || c.answered !== true || c.failed !== false || !Number.isFinite(Date.parse(c.answeredAt ?? '')) || Date.parse(c.answeredAt!) >= Date.parse(call.answeredAt!) || !Array.isArray(c.unansweredQuestionIndices) || c.unansweredQuestionIndices.length || !c.questions.length || c.questions.length > 4 || Object.keys(c.answers ?? {}).length !== c.questions.length || new Set(c.questions.map(q => q.question)).size !== c.questions.length || c.questions.some(q => q.multiSelect || q.options.length < 2 || q.options.length > 4 || new Set(q.options.map(o => o.label)).size !== q.options.length || !q.options.some(o => o.label === c.answers?.[q.question]))); } function introducesSourceContext(line: string): boolean { return /[::]$|\b(?:example|sample|hypothetical|template|quoted)\b/i.test(line); } /** Navigation may repeat already-approved post-review bookkeeping, but cannot * authorize it afresh or hide new implementation work behind a completion label. */ function isApprovedMaintenanceRecap(fp: AskUserQuestionFingerprint, plan: string, prior: readonly NativePlanQuestionCall[]): boolean { if (isRecordedMaintenanceRecap(fp, plan, prior)) return true; const call = fp.nativeCall!, q = call.questions[0]!; const label = (s: string) => s.replace(/^[1-9]\d*[A-Z]\)\s*/, '').replace(/\s*\(recommended\)$/, '').trim(); const current = (s: string) => !/^(?:\s*>|\s*`{3,}|\s*~{3,})|(?:^|\n)\s*(?:source|example|historical|quoted)\b|["“”]|\b(?:withdrawn|cancelled|canceled|rejected|superseded|no longer current|not approved|no longer approved|pending approval|if approved|once approved|assuming approval|provided approval)\b/i.test(s); if (q.multiSelect || !/^Next steps?$/i.test(q.header) || q.options.length !== 2 || fp.options.length !== 2 || !fp.options.every((o, i) => o.index === i + 1 && o.label === q.options[i]!.label) || !/^D[1-9]\d*\s*[—–:-]\s*Next steps?[.:]/i.test(q.question) || !/\bEng(?:ineering)? review is (?:clear(?:ed)?|complete[d]?)\b/i.test(q.question) || !/\bno UI scope\b/i.test(q.question) || !/\bCEO review is optional\b/i.test(q.question) || /\b(?:add|implement|rewrite|build|require|if|unless|assuming|provided|pending)\b/i.test(q.question) || !current([q.question, ...q.options.map(o => `${o.label}\n${o.description ?? ''}`)].join('\n'))) return false; const ready = q.options.find(o => /^Ready to implement(?: [—–-] run \/ship when done)?$/i.test(label(o.label))); const ceo = q.options.find(o => /^Run \/plan-ceo-review(?: first)?$/i.test(label(o.label))); if (!ready || !ceo || call.answers?.[q.question] !== ready.label) return false; const recap = /^Exit plan mode with the reviewed plan\. (?:Post-exit|After exiting): (.+)\.$/i.exec(ready.description ?? ''); const actions = recap?.[1]?.split(/\s+and\s+|;\s*/); if (!actions || actions.length !== 2) return false; const routing = actions.filter(a => /^(?:append|add) (?:the )?(?:gstack )?routing rules to CLAUDE\.md$/i.test(a)); const todos = actions.map(a => /^(?:create|write) TODOS\.md with (?:the )?(one|two|three|four|five|six|seven|eight|nine|[1-9]) accepted items?$/i.exec(a)).filter(Boolean); if (routing.length !== 1 || todos.length !== 1) return false; const count = Number(todos[0]![1]) || ['one','two','three','four','five','six','seven','eight','nine'].indexOf(todos[0]![1]!.toLowerCase()) + 1; if (!hasCompleteEarlierNativeAnswers(call, prior)) return false; const approved = prior.flatMap(c => c.questions.map(q => ({ q, selected: label(c.answers![q.question]!) }))); const routingCalls = approved.filter(({q}) => q.header === 'Routing'); if (routingCalls.length !== 1 || routingCalls.filter(({q, selected}) => current(q.question) && /\bskill routing rules\b/.test(q.question) && /CLAUDE\.md/.test(q.question) && /^(?:Add|Append) (?:gstack )?routing rules to CLAUDE\.md$/.test(selected)).length !== 1) return false; const accepted = approved.filter(({q, selected}) => /^TODO [1-9]\d*$/.test(q.header) && selected === 'Add to TODOS.md' && current(q.question) && /\bCaptured in the plan's TODOS section now\b/.test(q.options.find(o => label(o.label) === selected)?.description ?? '')); if (accepted.length !== count || approved.filter(({q}) => /^TODO [1-9]\d*$/.test(q.header)).length !== count || new Set(accepted.map(a => a.q.header)).size !== count) return false; // Only current, unfenced TODO headings in the published report bind the recap. let fence: string | undefined; const lines = plan.split(/\r?\n/).filter(line => { const mark = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line); if (mark) { if (!fence) fence = mark[1]; else if (mark[1]![0] === fence[0] && mark[1]!.length >= fence.length && !mark[2]!.trim()) fence = undefined; return false; } return !fence && !/^(?: {0,3}>| {4}|\t)/.test(line); }); if (fence) return false; const starts = lines.flatMap((line, i) => /^## TODOS?(?:\.md)?\b/i.test(line) ? [i] : []); if (starts.length !== 1 || !current(lines[starts[0]!]!) || /:\s*$/.test(lines.slice(0, starts[0]).filter(s => s.trim()).at(-1) ?? '')) return false; const tail = lines.slice(starts[0]! + 1), end = tail.findIndex(line => /^## /.test(line)); const blocks = (end < 0 ? tail : tail.slice(0, end)).join('\n').trim().split(/\n(?=### )/); return blocks.length === count && accepted.every(({q}) => { const subjects = q.question.split('\n')[0]!.match(/\b(?:[a-z]+|[A-Z][a-z]+)(?:[A-Z][A-Za-z0-9]*)+\b/g) ?? []; return subjects.length === 1 && blocks.filter(b => /^### /.test(b) && current(b) && new RegExp(`\\b${subjects[0]}\\b`).test(b.split('\n')[0]!)).length === 1; }); } /** Reference-bearing navigation can repeat approved post-exit maintenance and * the published first task. Bind each D/T reference to the current report and * actual earlier native answers; this never supplies report or exit evidence. */ function isRecordedMaintenanceRecap(fp: AskUserQuestionFingerprint, plan: string, prior: readonly NativePlanQuestionCall[]): boolean { const call = fp.nativeCall!, q = call.questions[0]!; const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const label = (s: string) => s.trim().replace(/^(?:[1-9]\d*)?[A-Z][).:]\s*/, '') .replace(/\s*\((?:recommended|optional|soft(?:: optional)?)\)$/i, ''); if (q.multiSelect || !/^Next(?: steps?)?$/i.test(navigationHeader(q)) || q.options.length !== 2 || fp.options.length !== 2 || !fp.options.every((o, i) => o.index === i + 1 && o.label === q.options[i]!.label) || Date.parse(call.answeredAt!) > Date.now()) return false; const ready = q.options.find(o => /^Ready to implement(?: [—–-] run \/ship when done)?$/i.test(label(o.label))); const ceo = q.options.find(o => /^Run \/plan-ceo-review(?: first)?$/i.test(label(o.label))); if (!ready || !ceo || call.answers?.[q.question] !== ready.label || !/^Optional (?:strategy|scope)/i.test(ceo.description ?? '')) return false; const recap = /^Exit plan mode[.;] (?:start|begin) with (T[1-9]\d*) (?:fixtures|characterization tests), then (?:write|create) CLAUDE\.md routing rules \((D[1-9]\d*)\) and TODOS\.md \((D[1-9]\d*(?:\/D[1-9]\d*)*)\)\.$/i.exec(ready.description ?? ''); if (!recap) return false; const context = [q.question, ...q.options.map(o => `${o.label}\n${o.description ?? ''}`)].join('\n'); const positive = q.question.replace(/"[^"\n]*"|“[^”\n]*”/g, ''); const eng = '(?:(?:the |this )?eng(?:ineering)? review|the review|this review)'; const completed = '(?:clear(?:ed)?|complete[d]?|done|finished)'; const invalid = new RegExp(`\\b${eng}\\b[^.!?;\\n]{0,100}\\b(?:not|never|incomplete|unfinished|pending|withdrawn|superseded|cancelled|canceled|reopened)\\b|\\b${eng}\\s+(?:will|would|may|might|could|should) (?:be )?${completed}\\b|\\b${eng}\\b[^.!?;\\n]{0,100}\\b${completed}\\b[^.!?;\\n]{0,80}\\b(?:if|when|once|unless|provided|assuming|after)\\b|\\b(?:if|when|once|unless|provided|assuming)\\b[^.!?;\\n]{0,80}\\b${eng}\\b`, 'i'); if (!/\b(?:where next|next steps?)\b/i.test(positive) || !new RegExp(`\\b${eng} (?:is |has been )?${completed}\\b`, 'i').test(positive) || !/\b(?:every finding has an approved fix|all decisions (?:are )?(?:answered|settled))\b/i.test(positive) || !/\b(?:remaining|only) choice is whether\b/i.test(positive) || invalid.test(context) || /(?:^|\n)\s*>|`{3}|~{3}|\b(?:example|quoted|historical)\s*:/i.test(context) || /\b(?:not every finding|not all decisions|unanswered|unresolved|unapproved|pending approval)\b/i.test(context)) return false; // Only the already-bound selected recap may contain implementation commands. const actions = context.replace(ready.description!, ''); // A new obligation is substantive whether phrased as a command, a need, // a dependency or a declared requirement. Only the bound recap is exempt. const obligation = /\b(?:must|shall|should|requires?|needs?)\s+\S|\b(?:depends on|required to)\s+\S|\b(?:is|are|becomes?|remains?)\s+(?:now |still )?(?:required|mandatory|a prerequisite)\b/i; if (/(?:^|[.!?;]\s+|\n|[✅❌]\s*|["“'‘]\s*|\b(?:and|but|also|first|then|now|next|while)\s+)(?:please\s+)?(?:add|remove|delete|replace|rewrite|change|alter|modify|enable|disable|implement|install|introduce|build|write|record|capture|create|switch|migrate|refactor|expand|reduce|deploy|approve)\b/i.test(actions) || /\brun\s+(?!\/(?:ship|plan-ceo-review)\b)|\b(?:new|additional|extra) (?:work|implementation|scope|task|requirement|dependency|feature|datastore|database|cache|test|prerequisite)\b/i.test(actions) || obligation.test(actions)) return false; const published: string[] = [], hierarchy: { depth: number; inactive: boolean }[] = []; let fence: string | undefined, preceding = ''; for (const line of plan.split(/\r?\n/)) { const mark = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line); if (mark) { if (!fence) fence = mark[1]; else if (mark[1]![0] === fence[0] && mark[1]!.length >= fence.length && !mark[2]!.trim()) fence = undefined; continue; } if (fence || /^(?: {4}|\t| {0,3}>)/.test(line)) continue; const h = /^(#{1,6}) (.+)$/.exec(line); if (h) { while (hierarchy.at(-1) && hierarchy.at(-1)!.depth >= h[1]!.length) hierarchy.pop(); hierarchy.push({depth:h[1]!.length,inactive:introducesSourceContext(preceding) || /\b(?:history|historical|archived?|example|quoted|template|withdrawn|superseded)\b/i.test(h[2]!)}); } // Keep the preceding visible line even in an inactive section, so source // context follows its heading's descendants and ends at the next sibling. if (line.trim()) preceding = line; if (!hierarchy.some(h => h.inactive)) published.push(line); } if (fence) return false; const current = published.join('\n'), titles = [...current.matchAll(/^# Plan: (.+) \(reviewed\)$/gm)]; const owners = [...current.matchAll(/^(?: