Research: move-task / approval-gate ergonomics (#3469)

No [NEEDS CLARIFICATION] markers and no new/changed dependencies — this is a brownfield change to existing CLI/library code. Research consolidates the design decisions grounded by the alignment + scope grounding squads and the post-spec adversarial review.

Decision 1 — Single-source gating classification

(extend discover_issue_references / the IssueReference model in tasks/issue_reference_discovery.py + tasks/issue_matrix.py), consumed read-only by all three sites that compute "referenced-but-missing": the approval blocker (tasks_parsing_validation.py), merge_gates._evaluate_issue_matrix_completeness_gate, and status/doctor.py.

matrix_issues) forked across three sites. Narrowing in one place without the others would let a reference be non-gating at approved` but re-block at merge or report dirty in doctor (a parallel-authority defect). One classifier closes the defect class by construction (DIRECTIVE_043).

divergent (NFR-006 violation). (b) A second scanner — rejected: violates single-canonical-authority.

  • Decision: Attach a gating classification to each discovered reference in ONE shared function
  • Rationale: Grounding + review found the gating computation (`referenced_issues -
  • Alternatives considered: (a) narrow only the approval blocker — rejected: leaves merge/doctor

Decision 2 — Default-gating fail-safe

non-gating on a positive, explicit signal (leading PR /pull token → PR/commit ref; context markers Follow-up:, baseline-red, see #, parent, epic on the citation line → context-only; cross-repo URL → already excluded). Ambiguity stays gating.

fail-open — a real implementation target misclassified as context-only would silently escape the completeness control on a governance artifact.

inverts the safe direction and would require re-marking every real target.

  • Decision: A discovered reference is gating by default; classification only demotes to
  • Rationale: The current gate is fail-closed (every ref gates). Narrowing must not flip it
  • Alternatives considered: default-non-gating with an explicit "gating" marker — rejected:

Decision 3 — Aggregate-over-occurrences classification

implementation-target occurrence forces gating (impl-target wins).

occurrence's line only. If a context citation appears before the impl-target citation, a first-occurrence classifier would silently downgrade the target — the exact edge case the spec forbids (FR-015, edge case).

  • Decision: Classify each issue number as an aggregate over ALL its occurrences; any
  • Rationale: detect_issue_references today dedupes by issue number keeping the first
  • Alternatives considered: first-occurrence classification — rejected (silent downgrade).

Decision 4 — not-applicable verdict: additive + terminal + non-gating

approved AND terminal — it passes the done/merge completeness gate unchanged (unlike in-mission, which is accepted at approved but rejected at done).

values validating unchanged (no migration, NFR-001). Terminal semantics avoid a not-applicable row re-blocking at merge.

(DM-01M302SBM26BGHEX36GC8YFJME): not-applicable is the truthful catch-all. Non-terminal (in-mission-style) — rejected: would re-block a context row at merge.

  • Decision: Append not-applicable to IssueMatrixVerdict (StrEnum). It is non-gating at
  • Rationale: Verified additive-safe — IssueMatrixVerdict(value) parsing leaves the four legacy
  • Alternatives considered: context-only / reference names — rejected by operator decision

Decision 5 — Lever SSOT (classification vs verdict)

existing row is RESOLVED. When they disagree: an operator may record any verdict on any row; a not-applicable verdict makes a row non-gating regardless of classification; an implementation-target classification requires a row even if none was scaffolded.

authority for "must a row exist?"; the verdict is the authority for "is the existing row resolved?".

  • Decision: classification decides whether a row is REQUIRED (gating); verdict decides whether an
  • Rationale: Removes the ambiguity between the two levers (FR-013). The classifier is the

Decision 6 — ADR supersedes stale WP09 not_applicable intent

Gate-4 intent documented in merge_gates.py, so two definitions do not diverge.

was never implemented; the ADR must absorb it, not create a second authority.

  • Decision: The ADR (FR-010) cites and supersedes the never-implemented WP09 not_applicable
  • Rationale: Review found a docstring claiming a prior owner for a not_applicable verdict that

Decision 7 — Inert checkboxes are docs-only (#2816)

mark-status and stating checkboxes are not the source of truth.

would regress it (C-001).

  • Decision: Do not re-read tasks.md checkboxes for subtask completion; add guidance pointing at
  • Rationale: Subtask completion is event-sourced by design (#2816, IC-10). Re-reading checkboxes

Supply-Chain Security (Planning)

Not applicable — this mission adds/upgrades/removes no dependencies. No registry, lifecycle-script, or freshness decision is introduced. Recorded per the planning supply-chain section: silence here is a true N/A, not an unexamined default.

Adversarial Evidence Dispositions (post-spec squad)

Per contracts/adversarial-evidence-contract.md, every contested finding's disposition is recorded. The post-spec adversarial review raised 3 BLOCKERs + 3 MAJORs + 2 MINORs; all accepted and folded into the spec before planning:

FindingSeverityDispositionSpec landing
No operational discriminator for gating vs context-onlyBLOCKERacceptedFR-001 + FR-011
Narrowing inverts gate fail-safe directionBLOCKERacceptedFR-011 + SC-007 + US1.2
Parallel authority: gating decided at 4 sitesBLOCKERacceptedFR-012 + NFR-006 + US1.7
Two levers not disambiguated (SSOT)MAJORacceptedFR-013
not-applicable lifecycle at done/merge unspecifiedMAJORacceptedFR-014 + edge case
First-occurrence dedupe downgrades dual-cited targetMAJORacceptedFR-015 + US1.6
Undisclosed prior WP09 not_applicable claimMINORacceptedFR-010
FR-005 alias reconciliation asymmetryMINORacceptedFR-005 + US3.2 reworded

No contested finding was dropped.