readVerificationStatus() decided a phase's verification was `stale` (a *-SUMMARY.md newer than the *-VERIFICATION.md) by comparing filesystem mtimes. mtimes are assigned at checkout time and are not preserved by `git clone` / `cp -R`, and any unrelated `touch` / reformat / editor-save re-stales a valid report — so a committed phase declaring `status: passed` could silently read `stale` on a fresh clone purely from checkout order, falsely rewriting a ROADMAP row and blocking milestone close (#2022 gate). Each file's effective "last changed" time is now its git commit time when the file is committed AND clean, and its mtime otherwise (uncommitted or working-tree-dirty). Both are real wall-clock change times, so a summary committed after — or edited after — the verification reads stale, while a clean fresh clone stays passed. Git commit time is content-tied and clone- stable; mtime is retained only where it is the true last-changed signal. Implementation: - Two bounded git calls per phase (never one-per-file): `git log --first-parent --format=%ct --name-only` for commit times, and `git diff --name-only HEAD` to drop dirty files. readVerificationStatus runs per-phase in the init/roadmap listing loops, so per-file spawning would fan out to P×(S+1) git processes ("Unbounded Subprocesses"). - `--first-parent` so merge commits report their file lists (plain `--name-only` omits merge diffs and would under-date merge-landed content). - The dirty-check fails SAFE: if `git diff` is inconclusive (errors / exits non-zero) the commit times are discarded so every file falls back to mtime, never trusting a possibly-stale commit time (no false "not stale"). - Paths matched back by `/`-bounded suffix (root vs nested `plans/` can't collide) and passed after `--` (dash-named files can't be read as flags). - A phase with no summaries skips git entirely; the scan short-circuits on the first stale summary. A `phaseCleanCommitTimesMs` seam keeps the unit tests hermetic (no git spawn); the resolver's two-call error handling is unit-tested via an injected execGit; two real-git integration tests lock the end-to-end path, the committed-then-edited (dirty) regression, and the `--` argv guard.
34 KiB
34 KiB