Skip to content

Pipeline Design 232

ezigus edited this page Mar 30, 2026 · 3 revisions

I now have a complete picture. The fix is already implemented — the code and tests are in place. Let me write the ADR.

Design: bug: pipeline-tasks.md not cleared on resume — stale tasks injected into build loop

Context

When a pipeline resumes (or a new pipeline starts on a branch that previously ran a different issue), pipeline-tasks.md — the file injected into the build stage prompt as a task checklist — was not being cleared. This caused stale tasks from a prior issue to leak into the current build loop, confusing the build agent with irrelevant or completed work items.

The root file is .claude/pipeline-tasks.md, which contains an - Issue: #N metadata line in its ## Context section. Three code paths touch this file:

  1. initialize_state() (scripts/lib/pipeline-state.sh:452) — called on fresh pipeline start
  2. resume_state() (scripts/lib/pipeline-state.sh:536) — called on shipwright pipeline resume
  3. stage_build() (scripts/lib/pipeline-stages-build.sh:172) — reads the file and injects its contents into the build prompt

A shared helper extract_issue_from_tasks_file() (scripts/lib/helpers.sh:429) normalizes the issue number from the file's metadata header for comparison.

Decision

Defense-in-depth at all three touchpoints — each layer independently prevents stale task injection:

  1. initialize_state() (line 463): Unconditionally deletes $TASKS_FILE on every new pipeline start. No issue comparison needed — a fresh run always starts clean.

  2. resume_state() (lines 607–620): After restoring state from pipeline-state.md, validates the tasks file:

    • If the file exists but lacks a parseable - Issue: header → malformed → delete it.
    • If the file's issue number differs from the resumed pipeline's $GITHUB_ISSUE → stale → delete it.
    • If the issue matches → keep it (preserves legitimate progress).
  3. stage_build() (lines 172–188): As a final guard at injection time, repeats the same validation before appending tasks to the build prompt. Stale or malformed files are removed and a warning is logged, but the build continues without task context.

Validation logic uses extract_issue_from_tasks_file() which:

  • Returns exit code 1 if the file is missing, unreadable, or lacks an - Issue: line
  • Strips # prefix and whitespace for normalized comparison
  • Handles both - Issue: and Issue: formats, case-insensitive

Edge cases handled:

  • TASKS_FILE variable unset or empty → [[ -n "${TASKS_FILE:-}" ]] guards in initialize_state; [[ -s "${TASKS_FILE:-}" ]] guards in resume_state and stage_build
  • GITHUB_ISSUE unset (goal-only pipeline, no issue) → current_issue will be empty; comparison "$tasks_issue" != "$current_issue" evaluates true only when current_issue is non-empty, so tasks are kept when there's no issue to compare against
  • File present but empty (0 bytes) → -s test fails, file is ignored

Alternatives Considered

  1. Clear only in initialize_state() — Pros: single change point, simplest diff / Cons: doesn't protect resume path; a user could manually create a tasks file between runs that wouldn't be validated. Insufficient for the resume scenario which is the primary bug.

  2. Add issue metadata to pipeline-state.md and skip tasks file entirely — Pros: eliminates the stale-file class of bugs entirely / Cons: major refactor of the build stage prompt assembly; tasks file is also used by the loop system (sw-loop-test.sh:1185) and bookkeeping exclusion lists (helpers.sh:394). Too much blast radius for a bug fix.

  3. Timestamp-based staleness (clear if tasks file is older than pipeline start) — Pros: no issue parsing needed / Cons: fragile on resume (legitimate tasks from a paused run would be incorrectly cleared); doesn't handle the case where two different issues run on the same branch in quick succession.

Implementation Plan

  • Files to create: none
  • Files to modify:
    • scripts/lib/pipeline-state.shinitialize_state() (line 463): add rm -f "$TASKS_FILE" ; resume_state() (lines 607–620): add issue-match validation block
    • scripts/lib/pipeline-stages-build.shstage_build() (lines 172–188): add issue-match guard before injection
    • scripts/lib/helpers.shextract_issue_from_tasks_file() (lines 429–435): new shared extraction helper
    • scripts/sw-lib-pipeline-stages-test.sh — (lines 385–496+): tests for all three defense layers
  • Dependencies: none (uses existing grep, sed, xargs)
  • Risk areas:
    • extract_issue_from_tasks_file grep pattern must match both - Issue: #42 and Issue: 42 formats — covered by the regex ^-\{0,1\} *Issue:
    • Goal-only pipelines (no GITHUB_ISSUE) should not falsely clear a valid tasks file — handled by the [[ -n "$current_issue" ]] guard

Validation Criteria

  • initialize_state() deletes $TASKS_FILE unconditionally on new pipeline start
  • resume_state() deletes tasks file when issue number differs from resumed pipeline
  • resume_state() preserves tasks file when issue number matches
  • resume_state() deletes malformed tasks file (no - Issue: header)
  • stage_build() refuses to inject tasks from a mismatched issue and removes the file
  • extract_issue_from_tasks_file() handles #-prefixed and bare issue numbers
  • All existing tests pass (npm test)

Smoke Test Specification

Test Case Input Expected
Fresh start clears stale file Write pipeline-tasks.md with issue #99, call initialize_state File deleted
Resume with mismatched issue Tasks file for #99, state file for #42, call resume_state File deleted, warning logged
Resume with matching issue Tasks file for #42, state file for #42, call resume_state File preserved
Resume with malformed file Tasks file with no - Issue: line, call resume_state File deleted, warning logged
Build rejects stale injection Tasks file for #99, GITHUB_ISSUE=#42, run build enrichment Tasks not injected, file deleted

These are exercised by the test cases at scripts/sw-lib-pipeline-stages-test.sh:385–530.

Health Check Results

  • Liveness: N/A — CLI tool, not a service
  • Readiness: N/A
  • Functional: The five test scenarios above directly validate the bug fix against realistic data (task files with issue metadata, state files with frontmatter)

Regression Detection

  • Full npm test suite must pass — no new error patterns
  • The pipeline-tasks.md file is in _GIT_BOOKKEEPING_FILES (helpers.sh:394), ensuring it is excluded from auto-commits and diff checks, so the cleanup operations don't trigger false "uncommitted changes" signals

Issue Closure Decision

PASS — All three defense layers are implemented and tested. The fix is minimal (no new files, no new dependencies) and follows the existing pattern of issue-based metadata validation already used elsewhere in the pipeline.

Clone this wiki locally