Files
msd-core/msd-core/references/stale-reverification.md
Jakub Zych aa03fc7fa9 fix(verification): keep passed uat results across verifier re-runs
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
2026-10-11 01:44:30 +02:00

3.8 KiB
Raw Blame History

Stale Re-verification

The ONE procedure for refreshing a stale VERIFICATION.md when every plan in the phase already has a SUMMARY. Loaded by execute-phase (steps/stale-reverification.md), verify-work (its completion gate) and progress (Route V.stale), so the three cannot drift apart. Nothing here executes a plan, and nothing here discards a recorded UAT result. msd_run is 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).