Plan-Review Resume Feeds Stale GitHub Comment Instead of Current Worktree OpenSpec Artifacts
When the planning pipeline resumes a plan-review stage for an OpenSpec repository, planning.ts reconstructs the reviewer prompt exclusively from the last posted ## Implementation Plan GitHub comment via extractPlan, ignoring…
At a Glance
When the planning pipeline resumes a plan-review stage for an OpenSpec repository, planning.ts reconstructs the reviewer prompt exclusively from the last posted ## Implementation Plan GitHub comment via extractPlan, ignoring the current proposal.md and spec deltas present on the worktree HEAD.
Summary
When the planning pipeline resumes a plan-review stage for an OpenSpec repository, planning.ts reconstructs the reviewer prompt exclusively from the last posted ## Implementation Plan GitHub comment via extractPlan, ignoring the current proposal.md and spec deltas present on the worktree HEAD. This causes the Codex/Grok reviewer to return NEEDS_REVISION demanding changes that were already committed to the worktree, creating an unbreakable review loop. Fresh authoring paths read the correct live artifacts; only the resume branch is affected.
Root-Cause Analysis
Confirmed Evidence
The resume code path executes:
- Resume branch diverges from fresh-authoring branch in
core/scripts/stages/planning.ts.
existingPlan = extractPlan(detail.comments)
promptPlanText = planText // derived from the GitHub commentThe fresh-authoring path instead assigns:
promptPlanText = proposal.md // read from worktree
specContext = readSpecDeltas // read from worktreeThe resume path never calls readSpecDeltas and never reads proposal.md.
Operators committed contract pins to openspec/changes/train-events-evidence-integrity/ under commits 136c05aa, 6f108b46, and e8f2578e, then re-entered plan-review. Post-commit review comments continued to demand coverage transitions, EEXIST-only retry, sole onRunReady append, and merge-proof payload — all of which were demonstrably present in the worktree change at the time. The only artifact the reviewer could have been reading that lacked those changes is the pre-commit GitHub comment.
- Incident reproduction on 2026-09-03 (#1301).
Confirmed Root Cause
The resume branch in planning.ts binds promptPlanText and specContext to the GitHub comment snapshot rather than re-reading the authoritative worktree artifacts. Because the reviewer model receives only the stale comment, it cannot detect that the worktree has already satisfied its earlier requirements.
Reasonable Inferences
extractPlanwas likely introduced to allow resume without a full re-read, which is appropriate for non-OpenSpec repos or repos with no active singular change. The problem is that the code applies this fallback unconditionally, even when a live OpenSpec change exists.- The empty-
changeIdvalidation gap (referenced as a related but distinct issue) may compound the problem by allowing resume to proceed even when the change context is ambiguous, but fixingchangeIdvalidation alone would not resolve the stale-artifact binding.
Alternative Causes Ruled Out
- Reviewer model hallucination is ruled out: the same pins were requested across multiple independent resume attempts and disappeared when operators force-fed the current
proposal.mdcontent, confirming the issue is in prompt construction, not model behavior. - Git worktree corruption is ruled out: the committed files were verified present and correct on HEAD.
Resolution Steps
Before constructing the reviewer prompt during resume, query the worktree for a singular active change directory under openspec/changes/. If exactly one change directory is found, treat it as the authoritative source.
- Detect a singular active OpenSpec change on resume.
Mirror the fresh-authoring path: read <change-dir>/proposal.md and invoke readSpecDeltas(<change-dir>). Assign the results to promptPlanText and specContext respectively, overriding any value derived from extractPlan.
- Re-read
proposal.mdand spec deltas from the worktree when a singular change exists.
The comment text from extractPlan may be included in the prompt as priorReviewHistory or a similarly scoped context field so the reviewer understands the iteration history without treating it as the current proposal.
- Retain the GitHub comment as supplementary history, not as the plan body.
If no singular change exists (zero or multiple change directories), the resume path may fall back to the comment text and must emit a named warning (e.g., WARN: no singular OpenSpec change found; falling back to comment-based plan text) rather than silently using the stale comment.
- Define the fallback explicitly.
Inject a mock worktree containing a proposal.md that satisfies a prior NEEDS_REVISION requirement, along with a mock GitHub comment that does not. Assert that the constructed promptPlanText contains the worktree content and that the test reviewer does not return NEEDS_REVISION for the already-satisfied requirement. No live GitHub calls; mock both readSpecDeltas and the comment fetch.
- Add a regression test.
After editing core/scripts/stages/planning.ts (and any shared prompt-construction utilities), run the project build and full CI suite before merging.
- Build and CI validation.
CLI Commands
Identify the active OpenSpec change directory from the worktree root:
# List candidate change directories; exactly one is required for safe resume
find openspec/changes -mindepth 1 -maxdepth 1 -type dVerify the proposal file exists on HEAD for the identified change:
# Replace <CHANGE_ID> with the directory name found above
git show HEAD:openspec/changes/<CHANGE_ID>/proposal.md | head -40Run build and CI after patching planning.ts:
cd core
node scripts/build.mjs
npm run ciConfiguration Snippets
Conceptual change to the resume branch of planning.ts (TypeScript pseudo-patch; adapt to the actual helper signatures in the codebase):
// BEFORE (resume branch — stale path)
const existingPlan = extractPlan(detail.comments);
const promptPlanText = existingPlan.planText;
// specContext is never set on this branch
// AFTER (resume branch — live worktree path)
const existingPlan = extractPlan(detail.comments); // kept for history
const singularChange = await findSingularOpenSpecChange(worktreeRoot);
let promptPlanText: string;
let specContext: string;
if (singularChange) {
// Bind reviewer to current worktree artifacts
promptPlanText = await fs.readFile(
path.join(singularChange, "proposal.md"),
"utf8"
);
specContext = await readSpecDeltas(singularChange);
} else {
// Explicit fallback with observable warning
logger.warn(
"no singular OpenSpec change found on worktree; " +
"falling back to comment-based plan text for resume"
);
promptPlanText = existingPlan.planText;
specContext = "";
}
// Optionally surface comment history to the reviewer
const priorReviewHistory = existingPlan.planText;Verification
After deploying the fix, trigger a plan-review resume against a change whose proposal.md already satisfies the most recent NEEDS_REVISION requirements:
- Confirm prompt construction. Add a debug log or inspect the constructed prompt payload to confirm
promptPlanTextcontains content fromproposal.md, not solely from the GitHub comment.
- Confirm reviewer outcome. The reviewer should return
APPROVED(or the equivalent passing status) without re-requesting changes that are present in the worktree.
- Confirm fallback path. Remove or rename the change directory in a test environment (or inject zero results from
findSingularOpenSpecChange) and verify the named warning appears in the log and the comment-based fallback is used.
- Regression test passes. The new unit test injecting
proposal.md+ comment mocks must pass undernpm run ci.
Rollback indicator: If, after the fix, any resume invocation produces NEEDS_REVISION for a requirement verifiably present in proposal.md on HEAD, the prompt-construction change must be reverted and the binding logic re-examined.
Prevention
- Prompt-source traceability. Emit a structured log line on every reviewer invocation that records the source of
promptPlanText(worktree:<path>vs.comment:<comment-id>). This makes silent fallback to stale comment content immediately observable in run logs.
- Resume precondition assertion. Before constructing the review prompt, assert the singular-change invariant explicitly. Fail fast with a named error (
RESUME_AMBIGUOUS_CHANGE) rather than silently degrading to the comment.
- CI regression test requirement. Gate the
planning.tsmodule on a test that covers the resume-with-worktree-change path. Enforce that bothproposal.mdandreadSpecDeltasare exercised on resume when a singular change is present.
- Alert on
NEEDS_REVISIONloop. Instrument the pipeline to fire an alert when the samechangeIdreceives three or more consecutiveNEEDS_REVISIONresults. This detects future prompt-source mismatches before operators must intervene manually.
- Code review checklist item. For any future change to the resume branch of
planning.ts, explicitly require a reviewer to verify thatpromptPlanTextandspecContextare sourced identically to the fresh-authoring path when a singular OpenSpec change is present.