Context
Found by the PR #86 final review (adversarially verified). Pre-existing on main since #79's file-granular excludes + upstream BMAD-METHOD#2564 (which saves intent-gap patches regardless of orchestrator version); #86 narrows it by excluding the latched patch (T4) but cannot close the from-scratch case.
The gap
verify.has_changes_since (verify.py:141-155) counts every untracked file and never subtracts the baseline_untracked snapshot (contrast attempt_dirty directly below it, which does). The saved intent-gap patch is untracked residue under implementation_artifacts, which _protected_relpaths shields from every reset.
So after ANY intent-gap halt, a from-scratch re-arm (restore_patch=None — the common "attempted reading was wrong" case) leaves the stale patch on disk, un-excluded (the orchestrator never learns its path: it only exists in escalation-detail free text). A re-driven session that produces zero code changes but flips the spec to the expected status passes the proof-of-work gate on the patch file's mere presence, and finalize_commit's add -A sweeps it into the story commit.
Why path exclusion can't fix it
verify_dev_exclude_relpaths(…, restore_patch=…) (the T4 fix) keys on the latch; on a from-scratch re-arm there is no latch and no reliable path. The only mechanical close is snapshot subtraction in the gate itself.
Fix shape
Subtract task.baseline_untracked from the untracked set in the proof-of-work gate — with a None fallback that keeps counting all untracked files (pre-snapshot runs must not weaken the gate, unlike attempt_dirty's ignore-all semantics). Both the re-arm snapshot (runs.py) and the dispatch-time re-capture (engine.py) run while the patch is on disk, so it lands in the snapshot and stops counting.
Context
Found by the PR #86 final review (adversarially verified). Pre-existing on
mainsince #79's file-granular excludes + upstream BMAD-METHOD#2564 (which saves intent-gap patches regardless of orchestrator version); #86 narrows it by excluding the latched patch (T4) but cannot close the from-scratch case.The gap
verify.has_changes_since(verify.py:141-155) counts every untracked file and never subtracts thebaseline_untrackedsnapshot (contrastattempt_dirtydirectly below it, which does). The saved intent-gap patch is untracked residue underimplementation_artifacts, which_protected_relpathsshields from every reset.So after ANY intent-gap halt, a from-scratch re-arm (
restore_patch=None— the common "attempted reading was wrong" case) leaves the stale patch on disk, un-excluded (the orchestrator never learns its path: it only exists in escalation-detail free text). A re-driven session that produces zero code changes but flips the spec to the expected status passes the proof-of-work gate on the patch file's mere presence, andfinalize_commit'sadd -Asweeps it into the story commit.Why path exclusion can't fix it
verify_dev_exclude_relpaths(…, restore_patch=…)(the T4 fix) keys on the latch; on a from-scratch re-arm there is no latch and no reliable path. The only mechanical close is snapshot subtraction in the gate itself.Fix shape
Subtract
task.baseline_untrackedfrom the untracked set in the proof-of-work gate — with a None fallback that keeps counting all untracked files (pre-snapshot runs must not weaken the gate, unlikeattempt_dirty's ignore-all semantics). Both the re-arm snapshot (runs.py) and the dispatch-time re-capture (engine.py) run while the patch is on disk, so it lands in the snapshot and stops counting.