BugCI/CD

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…

Rootlock SRE Engine 6 min read
Diagnostic brief

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.

Severity Not rated
Confidence High
Frequency Unknown
Impact See analysis

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:

  1. Resume branch diverges from fresh-authoring branch in core/scripts/stages/planning.ts.
   existingPlan = extractPlan(detail.comments)
   promptPlanText = planText  // derived from the GitHub comment

The fresh-authoring path instead assigns:

   promptPlanText = proposal.md   // read from worktree
   specContext    = readSpecDeltas // read from worktree

The 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.

  1. 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

  • extractPlan was 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-changeId validation 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 fixing changeId validation 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.md content, 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.

  1. 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.

  1. Re-read proposal.md and 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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 d

Verify 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 -40

Run build and CI after patching planning.ts:

cd core
node scripts/build.mjs
npm run ci

Configuration 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:

  1. Confirm prompt construction. Add a debug log or inspect the constructed prompt payload to confirm promptPlanText contains content from proposal.md, not solely from the GitHub comment.
  1. Confirm reviewer outcome. The reviewer should return APPROVED (or the equivalent passing status) without re-requesting changes that are present in the worktree.
  1. 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.
  1. Regression test passes. The new unit test injecting proposal.md + comment mocks must pass under npm 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.ts module on a test that covers the resume-with-worktree-change path. Enforce that both proposal.md and readSpecDeltas are exercised on resume when a singular change is present.
  • Alert on NEEDS_REVISION loop. Instrument the pipeline to fire an alert when the same changeId receives three or more consecutive NEEDS_REVISION results. 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 that promptPlanText and specContext are sourced identically to the fresh-authoring path when a singular OpenSpec change is present.
Developer FirstBuilt for engineers solving real problems
Evidence DrivenTechnical claims tied to available evidence
Automation ReadyStructured for CLI, APIs, and workflows
Privacy FocusedNo unnecessary data collection in this article UI
STAY AHEAD OF ISSUES

Get new root-cause analyses in your inbox

Engineering-focused updates. No fake subscriber counts. Unsubscribe anytime.