Skip to content

Dirty working-tree pre-flight gate: halt the cycle when repoRoot has uncommitted residue at branch/commit-pr entry #283

Description

@randomm

Context

Transcript-verified root cause of the worst defect cluster shipped to date (nessie #602 → PR #611, 2026-07-27). The repoRoot working tree was dirty with residue from a PRIOR #602 attempt (a different hook implementation, a test file gutted from 575→64 lines, a broken StrategyView edit) when the cycle started. Nothing checked. The cascade:

  1. This cycle's developer transcript is clean — the shipped defects were prior-run residue, not authored this cycle (verified: ms3j1eq7-3mjzm7-developer.json touched only in-scope files, 382 tests passed).
  2. The mechanized commit-pr (PR19) failed with "patch does not apply / already exists in index" — stale residue conflicted — forcing the LLM ops fallback.
  3. The fallback ran git add -A on the dirty tree, sweeping the gutted test file + broken out-of-scope edit into the commit. Green CI, merged, live data-loss regression (nessie PR #613 fixed it by hand).

Historical note: the old PM-driven flow had ABORT-on-dirty-tree ops doctrine (parseAbort exists for exactly the "Working tree is not clean" reply). The compiled driver has no deterministic equivalent — doctrine got lost in the mechanization.

This work must ship as its own separate PR, independent of any other open issue.

What to build

A. Pre-flight check in runBranch (extension/src/work-driver.ts, before the ops dispatch): git status --porcelain at ctx.repoRoot via the existing verifyExecFn ?? execp seam. Non-empty output → emit cap-hit preflight-dirty-tree → handoff. The cap's explainCap branch must list the leftover files (first ~20, from the porcelain output, stored in a new optional pipelineState.preflightDirtyFiles) and give verbatim recovery commands (git stash -u / git checkout -- <files> / inspect-first guidance), noting that residue from a prior cycle is the most likely source.

B. Re-assert at commit-pr entry: in mechanizedCommitPr, before consolidation, verify the repoRoot porcelain contains ONLY paths attributable to this cycle (paths under the declared workstream paths, or new files the developers created — heuristic: if any modified file matches a workstream's declared outOfScope list, bail to fallback is NOT enough; emit the same preflight-dirty-tree cap since the LLM fallback would sweep it with git add -A). Keep it simple: any modified file matching an outOfScope entry → cap, don't consolidate.

C. Schema: additive — preflight-dirty-tree cap literal in the workflow-state union; optional preflightDirtyFiles?: string[] on PipelineState.

Env: PI_ENSEMBLE_PREFLIGHT=0 escape hatch (default ON).

Acceptance criteria

  • A cycle started with a dirty repoRoot (injected verifyExecFn porcelain output) halts at branch with cap preflight-dirty-tree BEFORE any ops dispatch fires (assert dispatchFn not called for the branch step), end-to-end through runWorkDriver.
  • explainCap("preflight-dirty-tree", …) names the files and the recovery commands.
  • Clean tree → zero behaviour change (regression tests pass untouched).
  • mechanizedCommitPr with a porcelain entry matching a workstream outOfScope path → cap, no consolidation, no LLM fallback sweep.
  • PI_ENSEMBLE_PREFLIGHT=0 restores today's behaviour.
  • tsc + biome + all offline smokes green (suite runs under PI_ENSEMBLE_FORBID_LIVE_SPAWN=1).
  • Docs: troubleshooting section ("preflight-dirty-tree — what it means, how to recover"); AGENTS.md §7 sentence.

Out of scope

  • Auto-stashing (operator decides; the halt is the feature).
  • Worktree-cleanliness checks beyond repoRoot + the declared worktrees map.
  • Any LLM involvement.

Test plan

Offline: injected verifyExecFn porcelain fixtures (dirty at branch, dirty-out-of-scope at commit-pr, clean) in test-work-driver.ts following the PR18 integration-test pattern. Live (nessie): deliberately leave an uncommitted file in nessie's repoRoot, run /work N, confirm the pre-flight handoff names it.


Addendum (2026-07-27) — interaction with the parallel-work series (#287#290)

Scope reduced: the parallel-work series (PR-A) removes the residue cause (development at repoRoot). This preflight is NOT superseded — it narrows to the commit-pr integration-mutex entry (guarding operator edits / crash residue at the integration point). The branch-step preflight is deleted along with the ops branch dispatch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions