A phase whose human UAT passed could loop forever between execute-phase and verify-work. A gap-closure plan, or a later phase editing a covered file, made the report stale; verify-work only recorded a passed UAT against human_needed; the re-run verifier could not see the UAT file and re-emitted human_needed; and execute-phase's human_needed branch then rewrote *-UAT.md with every row back to [pending]. - verification.uat-evidence: recorded UAT rows plus the covered implementation files changed since the UAT. The verifier (Step 8b) treats a row that passed on unchanged code as verified and re-lists only affected rows, each with a retest_reason. - verification.seed-uat: merges the report's human items into *-UAT.md. Existing rows are kept byte-for-byte; a pass is reset only with a recorded reason. - verification.canonicalize-uat: the single human_needed -> passed flip, gated on the uat-only row predicate; refuses a stale report. - verification.status reports stale_reason; init.progress lists every executed-but-stale phase in reverify_phases. - verify-work and progress re-run the verifier themselves for a stale report instead of routing to execute-phase; execute-phase records a gap-closure checkpoint's UAT answers before dispatching the verifier. Fail-closed properties are unchanged: a malformed fingerprint and a real change to covered code still read stale. Emitted-Drift-Ack-Growth: execute-phase.md — verify_phase_goal gains the gap-closure uat-record pointer and the merge-not-overwrite human_needed branch; the uat template it replaced moved into verification.seed-uat Emitted-Drift-Ack-Growth: msd-verifier.md — new step 8b pointer to the lazily loaded verifier-uat-evidence reference plus the human_verified frontmatter key Emitted-Drift-Ack-Growth: progress.md — route v.stale now re-verifies every stale executed phase in place instead of naming one command
3.8 KiB
Stale Re-verification
The ONE procedure for refreshing a
staleVERIFICATION.md when every plan in the phase already has a SUMMARY. Loaded byexecute-phase(steps/stale-reverification.md),verify-work(its completion gate) andprogress(Route V.stale), so the three cannot drift apart. Nothing here executes a plan, and nothing here discards a recorded UAT result.msd_runis the launcher shim of the loading workflow.
verification.status reports stale with a stale_reason:
stale_reason |
What happened |
|---|---|
uncovered_artifacts |
The phase gained a PLAN/SUMMARY the report never declared — every gap-closure cycle does this. |
covered_changed |
A covered file changed after the verifier ran — possibly edited by a LATER phase. |
fingerprint_malformed / summary_newer |
The fingerprint is unusable, or a legacy report predates a SUMMARY. |
Every reason has the same remedy: the verifier looks again. What must NOT happen is the human being asked to repeat a UAT the change did not touch.
1. Re-run the verifier
Dispatch msd-verifier with the same brief execute-phase's verify_phase_goal step uses. A
workflow that did not load execute-phase's init resolves the inputs itself:
PHASE_INIT=$(msd_run query init.execute-phase "{phase}") # phase_dir, phase_req_ids, requirements_path, verifier_model
PHASE_GOAL=$(msd_run query roadmap.get-phase "{phase}" --pick goal)
VERIFIER_SKILLS=$(msd_run query agent-skills msd-verifier)
Agent(
description="Re-verify phase {phase_number} (stale report)",
prompt="Verify phase {phase_number} goal achievement.
Phase directory: {phase_dir}
Phase goal: {PHASE_GOAL}
Phase requirement IDs: {phase_req_ids}
Check must_haves against actual codebase. The previous VERIFICATION.md is stale — regenerate it.
A recorded human UAT is evidence: apply Step 8b before deciding the status.
<required_reading>
- {phase_dir}/*-PLAN.md
- {phase_dir}/*-SUMMARY.md
- {phase_dir}/*-UAT.md (if present)
- {requirements_path}
</required_reading>
${VERIFIER_SKILLS}",
subagent_type="msd-verifier",
model="{verifier_model}"
)
The verifier reads the phase's *-UAT.md itself (verification.uat-evidence): a human item
whose UAT row passed on unchanged code comes back verified, and only a row whose tested code
changed comes back for re-test.
Re-verifying several phases (progress's batch): dispatch one verifier per phase, in parallel, and run steps 2–3 for each as it returns.
2. Merge human items into the UAT file — never overwrite it
SEED=$(msd_run query verification.seed-uat "$PHASE_DIR")
Run this only when the fresh status is human_needed. It keeps every existing row and
result, appends unseen items as [pending], and resets a passing row only when the
verifier recorded a retest_reason (written into the row). When .changed is true,
commit .uat_file:
msd_run query commit "test({phase_num}): merge human verification items into UAT" --files "$(printf '%s' "$SEED" | jq -r '.uat_file')"
3. Route on the fresh status
STATUS=$(msd_run query verification.status "$PHASE_DIR" --pick status)
| Fresh status | Then |
|---|---|
passed |
Done — the phase is verified again. No human was asked anything. |
human_needed, SEED.pending is 0 |
Every UAT row may already pass — record it: msd_run query verification.canonicalize-uat "$PHASE_DIR". canonicalized: true → passed, no test re-run. Otherwise present its blockers and route to /msd:verify-work {phase}. |
human_needed, SEED.pending > 0 |
Only the pending rows need a human — /msd:verify-work {phase} resumes at the first one. |
gaps_found |
A real regression: /msd:plan-phase {phase} --gaps. |
stale again |
Stop and present the report — never loop on it (#4623 covers what the digest hashes). |