S2 - Preserve hard halts
Goal
- Prove the “(b)” half of the plan mission: every failure that is NOT safely retriable stays a loud, bannered terminal halt, and that halt tells the truth about which repos reached origin vs which are stranded committed-but-local.
- Named failure classes: message/manifest invalidity, mod-resolution invalidity, git/index + lock/provenance ambiguity, non-FF, push rejection, auth-required.
- Falsifier (b): refuse-push -> bannered halt naming pushed + still-local SHAs, NEVER a dirty-commit success and NEVER a bare payload.
- Classification of these classes is ALREADY correct in code — S2 does NOT re-implement it; S2 adds a matrix TEST plus the one omitted halt-payload wiring (falsifier (b)).
Implementation Sketch
Classification is already correct — do NOT re-implement
BLOCKER_HALT_REASONS(rules/halt_classification.py:160-172) +_NEVER_DEFERRABLE_STRUCTURALsubset ofCORRECTNESS_HALT_REASONS(:123-134,:63-92) already enumerate every named failure class.classify_halt_reason->HaltClass.BLOCKER(:254-283) already routes each to a non-retried_haltviaapply()’s hook-failureelsebranch and the push-failure branches.- Module-load disjointness guards (
:190-216, exhaustiveness guard:175-201) mechanically proveDEFERRABLE_HALT_REASONSis disjoint fromBLOCKER/_NEVER_DEFERRABLE_STRUCTURAL. - S2 adds a consolidated matrix TEST asserting each member routes to
_halt; S2 MUST NOT add a new classifier or special-case any reason (e.g.push_rejected_non_ff) outsideclassify_halt_reason’s existingHaltClass.BLOCKERbucket (GAP:conditional-fallthrough-not-exhaustive— preserve the guard, never work around it).
The real gap — falsifier (b) at the two except PushFailure sites
- Point of action:
apply()Step 7except PushFailurehard-haltelsebranch (state_machine.py:1798-1803) + Step 7.5 clean-ahead recovery branch (:1817-1822). - Both branches today
return _halt(..., payload=dict(exc.payload_extra) if exc.payload_extra else None); forpush_rejected_non_ff(and every BLOCKER reason lacking a dedicatedpayload_extra)payload_extrais{}, so the halt names NEITHER the repos pushed-before-the-failure NOR the repo whose own commit is stranded local-only. - This is an OMITTED code path, not a duplicated-and-diverged one (
MISSING_ABSTRACTIONaudit found no dedup twin) — the whole fix is wiring already-computed data into the halt payload, no new plumbing. - Halt-payload contract (extend, do NOT invent a fresh descriptor builder):
payload["pushed"]= repos pushed this run before the failure (fromPushFailure.partial_results,push.py:139-161);payload["commits"]= still-local committed SHAs (Step-5stage_and_commit_allresult at:1513diffed againstpartial_results). - Reuse
_recover_stranded_siblings+_build_recovery_commit_descriptors(state_machine.py:498-566) — the BUG-07-532 pattern, today HookFailure-branch-only (exactly ONE call site at:1705, graph-confirmed). Extend its call sites into the two PushFailure branches; do NOT re-derive its logic (LEAK:algorithmic-duplication). - The
pushed/commitsfield shape is the DATA S3 (per-repo-journal,s-117cfbf9) will persist — fix the shape here so S3 does not re-derive it; the shape is already emitted by the HookFailure hard-halt branch, so it staysnext_action.schema.json-valid.
Preservation constraints — do NOT
- Do NOT widen
DEFERRABLE_HALT_REASONS(rules/halt_classification.py:104-117) to swallow any S2-protected reason — S1’s commit-dirty gate keys off it; the BLOCKER/structural-vs-deferrable disjointness must continue to hold (S1s-8cf8f007left this as an explicit scope boundary). - Do NOT resurrect
_try_defer_push_failure— a structural no-op deleted during S1’s landing; still named in S1 prose, not a live extension point (is_snapshot_deferrableis never true for a push halt_reason). - Do NOT touch
stale_lock.classify_lock— used only byscripts/plan_orchestrator/block_wait.py, zero import sites inscripts/commit_push/; unrelated to commit_push’s “lock/provenance ambiguity” (which maps tomessages_invalidviaMessagesInvalidDetaildrift sub-reasonshead_drift/diff_digest_mismatch/stale_run_id/staged_set_drift).
Sequencing with BUG-07-532
- BUG-07-532 (
bug-tracker/plans/BUG-07-532, open/in-progress) fixed the analogous gap for the twoexcept HookFailureexits ONLY; its root-cause scope explicitly excludes the Step-7/7.5 PushFailure branches — S2’s falsifier (b) is the correctly-scoped, currently-unclaimed sibling. - Both edit the same 765-line
apply()this review cycle: read BUG-07-532’s landed diff (content/root-cause-analysis--s-c7f5a90c.md+implementation--s-cdc33d68.md) before touching the shared helper to avoid a conflicting diff. - Update
_recover_stranded_siblings’s docstring (state_machine.py:536-541, currently claims it is shared by both HookFailure exits while only one call site exists) in the SAME edit that adds the PushFailure callers, naming its now 2-3 real callers (COMMENT_HYGIENE_DRIFT:inaccurate-comment).
Test discipline
- Positive matrix (work item 1): one consolidated test enumerating
BLOCKER_HALT_REASONS | _NEVER_DEFERRABLE_STRUCTURAL, asserting every member routes to_haltviaclassify_halt_reason— no such single matrix exists today (coverage is scattered across per-reason pins). - Close
invalid_repo_selectorin the SAME matrix — a BLOCKER halt_reason with zero test-file references anywhere inscripts/commit_push/tests/(the sole unpinned BLOCKER member). - Negative pins (work item 2): pair every “halts correctly” positive with a disjointness assertion that the reason is NOT in
DEFERRABLE_HALT_REASONS/WARNING_ELIGIBLE_HALT_REASONS— the module-load guard is a SET invariant, not a behavioral pin thatapply()actually halts (INVERTED-TDDnegative-pin discipline, per S1’s precedent). - L12 production entry point (falsifier (b)): drive the REAL
apply()/run_state_machine()(whatpython -m scripts.commit_push runinvokes), never a mocked_haltseam — mirrortest_hard_halt_recovers_and_surfaces_earlier_committed_sibling(tests/test_clean_ahead_push_path.py:453-530, samepayload["pushed"]/payload["commits"]assertions) with a new pin triggered bypush_rejected_non_ff(LAYER_GAP:production-entry-pointguard). - Extend
tests/test_push_failure_classification.py(11+ existing per-reason pins) rather than duplicate; there is no dedicatedtest_halt_classification.py. - Regression scope:
apply()is the hottest, highest-betweenness, widest-fanout function in the file — re-run the FULLtest_clean_ahead_push_path.py+test_push_failure_classification.py+test_state_machine.pysuites, not a scoped subset; runpython -m scripts.commit_push run --self-testbefore + after as a cheap end-to-end smoke.
Ownership + terrain
- Terrain is contained in one community (
[128],scripts.commit_push.state_machine) — the fix is architecturally local, no cross-boundary contract to reconcile. - Live claimants of
apply()’s failure-exit branches: this section (S2) + BUG-07-532; no other plan/bug references this code (symbol-plansempty, expected forscripts/**Python). scripts/**is tooling-scope: universal SRP/SSOT +STRUCTURE:*/LEAK:*/GAP:*families apply; compiler-only deep TDD-matrix/dual-exec families do NOT.- No external design doc exists (
scripts/commit_push/design/2026-07-24-durability-redesign-tp-discuss.mdis absent from the tree) — the load-bearing contract is this section body + falsifier (b) + the live code; tp-discuss host binding isdiscussions/01-commit-push-durability/discussion.md:95.
Coverage caveats (intel-graph staleness at package time)
ori-platformderived facets were ~8.5h stale (embedding/cpg/call-resolution null; co-change/test-topology/sibling-divergence stamped 2026-07-24T08:07 vs code sync 16:39) — everysimilar/test-posture/history-coupling/parallel-driftrow in the dossier is UNKNOWN-coverage, corroborated against live source, not clean. Re-syncori-platformembedding/cpg before TDD-matrix authoring to sharpensimilarand close thetest-posturefalse-zero (direct_tests: 0onhalt_classification.pyis an extractor artifact, not real).- Python free-name
callers/calleesresolution is systematically weak for this module — treat any 0-edge Pythoncallersresult as an extractor gap requiring grep corroboration, never as dead code.
Work Items
- Verify (via one consolidated matrix test enumerating BLOCKER_HALT_REASONS | _NEVER_DEFERRABLE_STRUCTURAL from scripts/commit_push/rules/halt_classification.py) that every structural/durability failure class named in this section’s goal — message/manifest invalidity (messages_invalid), mod-resolution invalidity (mod_resolution_fail), git/index and provenance-ambiguity failures (the MessagesInvalidDetail drift sub-reasons), non-FF (push_rejected_non_ff), push rejection (push_network_failure/push_unclassified/push_upstream_name_mismatch/push_no_upstream), and auth-required (auth_required) — routes to a terminal bannered halt via classify_halt_reason. Close the untested invalid_repo_selector gap (currently zero test-file references in scripts/commit_push/tests/) in the same matrix rather than leaving it the sole unpinned BLOCKER member.
- Assert these are NEVER relabeled as dirty-commit success
- Falsifier (b): extend the existing sibling-recovery pattern (_build_recovery_commit_descriptors + _recover_stranded_siblings, state_machine.py:498-566) into apply()‘s two except PushFailure hard-halt sites (Step 7 :1798-1803, Step 7.5 :1817-1822) so the halt payload names both the repos pushed successfully earlier this run (from PushFailure.partial_results, push.py:139-161) and the repo(s) left stranded committed-but-local (diffed against the commits bound at Step 5, :1513) — never a bare
{}payload_extra. Mirror the existing test_hard_halt_recovers_and_surfaces_earlier_committed_sibling shape (tests/test_clean_ahead_push_path.py:453-530: payload[“pushed”]/payload[“commits”] assertions) with a new pin triggered by push_rejected_non_ff. Sequence this edit against BUG-07-532 (bug-tracker/plans/BUG-07-532, in-progress, same apply() function, HookFailure branches only) to avoid a conflicting diff on the shared helper; update _recover_stranded_siblings’s docstring (currently claims exactly 2 callers when there is 1 today) to name its real callers once this item adds 2-3 more.
Intel dossier — pointer and tier disposition
Dossier: preserve-hard-halts--s-4ddb5995.intel.md (this section’s content/intel/ sidecar)
Read that dossier from line 1 through EOF before acting on this section. This block is a pointer and an audit record, never a substitute: not the BLUF, not a summary, not selected tiers.
| Dossier tier | Title | Lines | Disposition | Where / why |
|---|---|---|---|---|
| 0 | 0. Bottom line up front | 21 | integrated | Goal + Implementation Sketch §Classification is already correct — do NOT re-implement + §The real gap — falsifier (b) at the two except PushFailure sites |
| 1 | 1. The target, verbatim | 17 | integrated | Goal (restated as the (b) half of the mission disjunction) + Implementation Sketch §Ownership + terrain (tp-discuss host binding; no external design doc exists) |
| 2 | 2. Terrain — symbols in the objective area (with files) | 64 | integrated | Implementation Sketch §Classification is already correct + §The real gap (file:line pointers for BLOCKER_HALT_REASONS/_NEVER_DEFERRABLE_STRUCTURAL/classify_halt_reason in halt_classification.py, the two except PushFailure sites + _recover_stranded_siblings/_build_recovery_commit_descriptors in state_machine.py, PushFailure.partial_results in push.py) + §Preservation constraints (MessagesInvalidDetail drift sub-reasons) |
| 3 | 3. Code-graph recon | 77 | integrated | Implementation Sketch §The real gap (single call site graph-confirmed) + §Test discipline (apply() hottest/highest-betweenness → full-suite regression scope; test-posture false-zero) + §Ownership + terrain (community [128] containment) + §Coverage caveats (Python callers resolution weak; co-change push.py corroboration) |
| 3D | 3D. Diagnostic / observability surface | 19 | integrated | Implementation Sketch §Test discipline (L12 production entry point drives real apply()/run_state_machine via python -m scripts.commit_push run; run —self-test smoke before/after) |
| 3H | 3H. Hygiene constraints | 17 | integrated | Implementation Sketch §Classification is already correct (GAP:conditional-fallthrough exhaustiveness guard) + §The real gap (LEAK:algorithmic-duplication — extend not re-derive) + §Test discipline (INVERTED-TDD negative pins; LAYER_GAP:production-entry-point) + §Sequencing with BUG-07-532 (COMMENT_HYGIENE_DRIFT docstring update) |
| 4 | 4. Cluster / family — sibling sections + related bugs | 25 | integrated | Implementation Sketch §Sequencing with BUG-07-532 (coordinate, do-not-duplicate, shared apply() this cycle) + §Preservation constraints (S1 disjointness boundary) + §The real gap (S3 per-repo-journal field-shape dependency) |
| 5 | 5. Conformance audit — MISSING_ABSTRACTION (ori-platform, stale facet) | 10 | integrated | Implementation Sketch §The real gap (framed as an OMITTED/MISSING code path, not a duplicated-and-diverged one — no confirmed dedup twin; reinforces extend-existing-helper over new descriptor builder) |
| 6 | 6. Plan ownership — who owns this code now | 11 | integrated | Implementation Sketch §Ownership + terrain (S2 + BUG-07-532 are the two live claimants of apply()‘s failure-exit branches; symbol-plans empty for scripts/** Python is expected) + §Sequencing with BUG-07-532 |
| 7 | 7. Prior art — declared N/A (evidence-backed, not assumed) | 11 | not_applicable | Dossier declares prior art N/A — cross-repo similar/search returned only weak, semantically-coincidental matches; scripts/commit_push is internal wrapper-tooling with no compiler analog. The only relevant precedent is in-house BUG-07-532’s _recover_stranded_siblings pattern, already integrated via the reuse directive in §The real gap + §Sequencing. This tier introduces no external technique or constraint the section must answer to. |
| 8 | 8. Sentiment — declared N/A | 5 | not_applicable | Sentiment corpus (pain_pctile/controversy_pctile) is scoped to the 11 reference-language PUBLIC issue trackers; scripts/commit_push is internal AI-seam tooling with no public tracker and no external community-heat signal — structurally empty by construction. Imposes nothing on the section. |
| 9 | 9. Declared coverage gaps | 17 | integrated | Implementation Sketch §Coverage caveats (ori-platform ~8.5h derived-facet staleness; Python callers/free-name resolution weak) + §Preservation constraints (_try_defer_push_failure dead/do-not-resurrect; stale_lock.classify_lock unrelated/do-not-touch; lock/provenance ambiguity maps to messages_invalid via MessagesInvalidDetail) |
| 10 | 10. Recommended recon entry points (for /continue-roadmap execution) | 20 | integrated | Implementation Sketch §Sequencing with BUG-07-532 (read BUG-07-532 root-cause + implementation first) + §The real gap (the two except PushFailure sites :1770-1822; recovery helpers :498-566) + §Test discipline (mirror test_hard_halt_recovers_and_surfaces_earlier_committed_sibling; extend test_push_failure_classification.py; halt_classification.py full read; run —self-test) — the recon file:line pointers are now section-resident |