Skip to content

feat(adapters,engine,devcontract): deterministic missing-marker catch + repair#277

Merged
pbean merged 6 commits into
mainfrom
feat/deterministic-marker-catch-276
Jul 23, 2026
Merged

feat(adapters,engine,devcontract): deterministic missing-marker catch + repair#277
pbean merged 6 commits into
mainfrom
feat/deterministic-marker-catch-276

Conversation

@pbean

@pbean pbean commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Hardens the #224/#271 missing-marker fallback (PR #273), whose attribution was heuristic — a 2-stable-Stops fingerprint, or a single sighting post-kill — and which left the offending spec non-compliant on disk. PR #273 shipped with three documented accepted risks; this PR retires all three by making catch and fix deterministic, across four harness-side mechanisms delivered in four single-session phases on one branch.

The problem

The bmad-dev-auto review HALT intermittently finalizes a spec's frontmatter to a terminal status: (done/blocked) without appending the ## Auto Run Result section our harvest scan keys on. A finished story then reads no-artifact on every Stop, the stall nudges re-invoke an already-exited workflow (#149), and after max_review_cycles the verify-passing work is rolled back (#224). PR #273's synthesis-from-frontmatter rescued this, but its attribution was a heuristic and it never repaired the on-disk spec.

Mechanisms

  1. Launch-state snapshot + content-hash gate (M1). Right after the [BUG] The #109 stall-grace fix cannot reach the review leg: a review pass is killed on its first Stop by the dev pass's stale ## Auto Run Result #160 marker strip, the engine captures a SpecSnapshot (content sha256, mtime, frontmatter status) and threads it onto the review SessionSpec. The fallback refuses to synthesize from a candidate whose bytes still hash-equal the snapshot — provably untouched by this session (a done spec re-opened for review, never re-written) — in every mode, including the dead-window post-kill reconcile. This kills the documented dead-window false positive (a review killed after an mtime-only bump but before the in-review flip, previously scored done without having run). New verdict unmodified-since-launch; dead-window crumb frontmatter-unmodified-refused. Snapshot is process-transient (crash-resume degrades to the conservative path).

  2. Mid-session status-transition observation (M2). On each heartbeat tick the generic (and OpenCode) dev adapter samples the snapshotted spec and records the first status it observes this session drive off its launch state to a live, non-terminal value (in practice in-review), crumb spec-status-transition-observed. A recorded transition is deterministic proof the terminal frontmatter the spec later carries is this session's own write, so the fallback synthesizes on one terminal sighting instead of the 2-observation fingerprint (synth crumb gains transition=). The M1 hash gate still outranks it.

  3. Artifact repair (M3). On synthesis the engine appends the owed ## Auto Run Result marker onto the on-disk spec (new devcontract.append_auto_run_result, the inverse of the [BUG] The #109 stall-grace fix cannot reach the review leg: a review pass is killed on its first Stop by the dev pass's stale ## Auto Run Result #160 strip), so the once-invisible spec re-enters the normal marker scan and the next review launch strips it exactly like a skill-written marker. Best-effort at the session-synthesized-from-frontmatter site (covers live-Stop, crash-path, and post-kill synthesis); refused for a spec outside the orchestrator-owned roots or whose fresh frontmatter no longer agrees with the synthesized status, so it can never author a marker that disagrees (spec-marker-repaired / -repair-failed / -repair-skipped).

  4. Targeted contract nudge (M4), gated by new knob limits.dev_contract_nudge (bool, default true). On the first terminal-frontmatter-pending Stop — one candidate, not the hash-gate refusal, transition not yet proven — the adapter sends one nudge asking the skill to append the section itself and end its turn, repairing the omission at its source; a compliant append is harvested by the ordinary scan (no synthesis flag). Fires exactly once per session — marked before the send (a raising transport still counts), never refilled, touching no stall counters — so an mtime bump that resets the observation counter can never re-nudge (Claude adapter: Stop hook + idle detection re-entrant loop prevents session from ending #149's refill hazard structurally cannot apply). contract-nudge-sent crumb; MultiplexerError swallowed.

Decision ladder (_frontmatter_fallback, final shape)

scan → 0 candidates: no-artifact → >1: ambiguous-frontmatter → read candidate (mtime, fm status, sha256 when the snapshot path matches; OSError → torn-read degrade) → hash gate (M1, all modes) → evidence: transition_proven (M2) + fingerprint/stable/observations → synthesize when dead_window or transition_proven or (stable and obs >= 2) → else record-obs branch: fingerprint + terminal-frontmatter-pending + contract nudge (M4).

HARD CONSTRAINT

The spec's status: at review launch is load-bearing routing input to the upstream skill (a done spec routes to a fresh step-04 review pass). The review-launch frontmatter is therefore never mutated — all four mechanisms are observation or a prose-append, never a status write.

Upstream

The omission originates upstream: bmad-dev-auto intermittently skips the ## Auto Run Result append at the review HALT while finalizing frontmatter to a terminal status. Filed as bmad-code-org/BMAD-METHOD#2616 with the resultless-Stop evidence pattern and a request to make the HALT append unconditional-and-verified. This PR is the downstream harness-side mitigation regardless of the upstream fix.

Tests

Full uv run pytest -q green — 2887 passed, the only 2 failures the pre-existing test_module_skills_sync.py bmad-loop-setup module.yaml drift (unrelated). Full trunk check --no-fix clean across all 15 modified files. New coverage: adapter hash-gate refusal (live/dead-window/wait-false/other-path), transition single-sighting harvest + missed-transition-stays-conservative + hash-gate-beats-transition, heartbeat-drives-observe-tick, append_auto_run_result roundtrip + guards, engine snapshot threading + _repair_spec_marker (repaired/failed/skipped/fm-mismatch), 9 contract-nudge cases (sent-once / exactly-once-across-mtime-reset / not-sent on transition/ambiguous/dead-window/unmodified / send-failure-marks-sent / disabled-by-policy / e2e nudge-then-skill-appends-marker), and the policy/settings-schema knob sync.

Closes #276.

Summary by CodeRabbit

  • Bug Fixes
    • Improved recovery for completed stories where the expected ## Auto Run Result marker is missing, including deterministic refusal to synthesize when content hasn’t actually changed.
    • Added guardrails to avoid false “completed” detection from unchanged or ambiguous specs.
  • New Features
    • Best-effort repair now appends the missing result section when appropriate.
    • Optional one-time “contract nudge” prompts the skill to append the marker (enabled by default).
  • Documentation
    • Updated feature and journal/TUI descriptions with the new missing-marker forensics and repair lifecycle breadcrumbs.

pbean added 4 commits July 23, 2026 09:08
Second of four harness-side mechanisms hardening the #224 missing-marker
fallback. A new `_observe_tick(handle, spec)` no-op hook on `CodingCLIAdapter`
is called each heartbeat tick from both dev wait loops (generic + opencode).
`_DevSynthesisMixin` implements it: sample the snapshotted spec's frontmatter
and record the FIRST status this session drives off its launch state to a
live, non-terminal value (in practice `in-review`) into `_fm_transition_obs`,
with a `spec-status-transition-observed` lifecycle crumb.

A recorded transition is deterministic proof the terminal frontmatter the spec
later carries is this session's own write, so `_frontmatter_fallback` widens its
synthesis predicate to `dead_window or transition_proven or (stable and obs >=
FM_FALLBACK_MIN_OBS)` — a single terminal sighting harvests, live or dead, and
the synthesized crumb gains a `transition` flag. The M1 hash gate still outranks
it: bytes reverted exactly to the launch snapshot refuse even with a transition
recorded. A transition that flips between two ticks is missed and falls back to
the conservative 2-observation fingerprint. Sampling-only: no snapshot or a torn
read is a silently skipped sample, and terminal/blank/unchanged statuses record
nothing.
…276 M4)

Final mechanism of the deterministic missing-marker catch+repair (#276): a
targeted, once-per-session tmux nudge asking the bmad-dev-auto skill to append
the '## Auto Run Result' section it owed when a Stop finds a marker-less
terminal-frontmatter spec, so the omission is repaired at its source (a
compliant append is harvested by the ordinary marker scan; frontmatter
synthesis stays the backstop).

- policy: LimitsPolicy.dev_contract_nudge (bool, default true) + parse + template
  + core.toml [limits] switch entry (default_ref, sync tests pass bidirectionally).
- adapters/generic.py: CONTRACT_NUDGE_TEXT; _configure_dev_knobs gains the
  never-cleared _contract_nudge_sent set + _contract_nudge_enabled; the
  _frontmatter_fallback record-obs branch fires the nudge on the first pending
  observation (marked BEFORE the send so a raising transport still counts;
  MultiplexerError swallowed; touches no stall counters, so #149's refill hazard
  cannot apply). _DevSynthesisMixin declares send_text as a bare annotation.
- docs (FEATURES.md, tui-guide.md) + consolidated CHANGELOG #276 entry finalized
  with all four mechanisms, the knob, and the never-mutate-launch-frontmatter
  hard constraint.
- tests: 9 adapter nudge tests (sent/exactly-once/transition/ambiguous/dead-
  window/unmodified/send-failure/disabled/e2e-append) + policy knob test.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5066253e-275a-4312-9813-9e53b2992675

📥 Commits

Reviewing files that changed from the base of the PR and between c5c85c2 and e6eda85.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • src/bmad_loop/adapters/generic.py
  • src/bmad_loop/devcontract.py
  • src/bmad_loop/engine.py
  • tests/test_devcontract.py
  • tests/test_engine.py
  • tests/test_generic_tmux.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/bmad_loop/devcontract.py
  • tests/test_generic_tmux.py
  • CHANGELOG.md
  • src/bmad_loop/adapters/generic.py
  • src/bmad_loop/engine.py

Walkthrough

The review adds launch-time spec snapshots, heartbeat frontmatter observation, deterministic fallback refusal, optional contract nudging, and guarded engine-side repair of missing ## Auto Run Result markers. Policy parsing, documentation, and focused tests cover the new behavior.

Changes

Missing-marker recovery

Layer / File(s) Summary
Launch snapshot and fallback gate
src/bmad_loop/adapters/base.py, src/bmad_loop/adapters/generic.py, src/bmad_loop/engine.py, tests/test_engine.py, tests/test_generic_tmux.py
Review sessions capture spec fingerprints, thread them through SessionSpec, and refuse synthesis when candidate bytes match the launch snapshot.
Session observation and contract nudge
src/bmad_loop/adapters/generic.py, src/bmad_loop/adapters/opencode_http.py, src/bmad_loop/policy.py, src/bmad_loop/data/settings/core.toml, tests/test_generic_tmux.py, tests/test_opencode_http.py, tests/test_policy.py
Heartbeat hooks record frontmatter transitions and optionally send one targeted nudge to append the missing marker.
Marker repair and validation
src/bmad_loop/devcontract.py, src/bmad_loop/engine.py, tests/test_devcontract.py, tests/test_engine.py
The engine validates synthesized results before appending an idempotent, line-ending-preserving marker with atomic writes.
Operational documentation and changelog
CHANGELOG.md, docs/FEATURES.md, docs/tui-guide.md
Documentation records the recovery mechanisms and associated lifecycle and journaling breadcrumbs.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Engine
  participant DevAdapter
  participant SpecFile
  participant SkillTransport
  Engine->>SpecFile: capture launch snapshot
  Engine->>DevAdapter: pass snapshot in SessionSpec
  DevAdapter->>SpecFile: observe frontmatter on heartbeat
  DevAdapter->>SkillTransport: send one contract nudge
  SkillTransport->>SpecFile: append Auto Run Result marker
  Engine->>SpecFile: repair marker after synthesized result
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: dracic

Poem

A rabbit found a marker gone,
Then watched the status hop along.
A nudge was sent, a snapshot kept,
An atomic repair softly crept.
The spec now tells the tale just right—
And bunnies rest beneath moonlight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: deterministic missing-marker catch and repair across adapters, engine, and devcontract.
Linked Issues check ✅ Passed The changes implement the #276 requirements: snapshot gating, transition observation, on-disk repair, policy nudge, docs, and tests.
Out of Scope Changes check ✅ Passed The diff stays focused on #276 support code, documentation, and tests with no unrelated feature work evident.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/deterministic-marker-catch-276

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bmad_loop/data/settings/core.toml`:
- Line 144: Remove the empty duplicate [[section.field]] declaration immediately
before max_tokens_per_story in the settings configuration, leaving only
field-table entries that define their key and widget metadata.

In `@src/bmad_loop/engine.py`:
- Around line 1947-1956: Update the exception handler around
devcontract.append_auto_run_result in the spec-marker repair flow to catch both
OSError and UnicodeDecodeError. Preserve the existing spec-marker-repair-failed
journal entry and return behavior so undecodable specs are handled like other
append failures and the run can complete cleanly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c8a501cd-575b-42b4-8cf0-f2e09c737bf7

📥 Commits

Reviewing files that changed from the base of the PR and between 2fd4f1a and c5c85c2.

📒 Files selected for processing (15)
  • CHANGELOG.md
  • docs/FEATURES.md
  • docs/tui-guide.md
  • src/bmad_loop/adapters/base.py
  • src/bmad_loop/adapters/generic.py
  • src/bmad_loop/adapters/opencode_http.py
  • src/bmad_loop/data/settings/core.toml
  • src/bmad_loop/devcontract.py
  • src/bmad_loop/engine.py
  • src/bmad_loop/policy.py
  • tests/test_devcontract.py
  • tests/test_engine.py
  • tests/test_generic_tmux.py
  • tests/test_opencode_http.py
  • tests/test_policy.py

Comment thread src/bmad_loop/data/settings/core.toml
Comment thread src/bmad_loop/engine.py
…elper DRY

Addresses CodeRabbit review on #277:
- engine._repair_spec_marker widened its best-effort guard from OSError to
  (OSError, UnicodeDecodeError): append_auto_run_result reads the spec's raw
  bytes and, by contract, raises UnicodeDecodeError (a ValueError, not an
  OSError) on an undecodable spec — a torn-mid-write between the frontmatter
  read and the append. Unguarded it would crash a run whose result was already
  synthesized and recorded. Matches _post_kill_reconcile's torn-read doctrine.
  New engine test covers the UnicodeDecodeError path.
- test-helper cleanup: fold _transition_crumbs/_nudge_crumbs into one
  parameterized _lifecycle_events(adapter, event) so both read uniformly (and
  black stops collapsing only the shorter of the two).

The other CodeRabbit finding (core.toml line 144 'empty duplicate section.field')
is a false positive: line 144's [[section.field]] is the header for
max_tokens_per_story (key on the next line); the settings-schema sync tests
confirm every field has a key.
@pbean

pbean commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Comprehensive review findings

Reviewed 2fd4f1a..9a94e43 against issue #276 and the PR contract using independent adversarial, edge-case, verification-gap, and acceptance audits.

Blocking findings

  1. High: hash equality overrides stronger same-session transition evidence. The hash gate returns before transition_proven is evaluated (generic.py L1278-L1307). A clean review can legitimately round-trip done -> in-review -> done without changing any other spec bytes. When the marker omission occurs, the final digest equals the launch digest even though M2 observed deterministic proof that the review ran. The current order permanently refuses synthesis, skips the contract nudge, and can end in stall/retry/defer or rollback. Decision: transition evidence should win; hash equality should refuse only when no transition was observed.

  2. High: stories-mode readback bypasses all four Deterministic missing-marker catch + repair (follow-up to #224/#271, PR #273) #276 mechanisms. BMAD_LOOP_SPEC_FOLDER short-circuits directly into stories readback (generic.py L1086-L1096). That branch accepts a terminal spec using only the launch-time mtime floor (generic.py L1403-L1422); it ignores the snapshot hash, transition evidence, marker presence, nudge, and repair. An unchanged launch-state done spec with only an mtime bump is accepted immediately, retaining the exact false-positive completion and non-compliant artifact behavior Deterministic missing-marker catch + repair (follow-up to #224/#271, PR #273) #276 is intended to remove.

  3. High: best-effort marker repair can truncate the canonical spec. append_auto_run_result reads the complete spec and rewrites it with truncating Path.write_text (devcontract.py L510-L528). The caller catches an OSError and continues (engine.py L1947-L1962), but a disk-full event, interruption, or short write may already have left the spec empty or partial. Fault injection reduced a 46-byte spec to 12 bytes. This violates the stated invariant that a failed repair never loses work.

Verification gap

  1. Medium: the primary review repair path lacks engine-level coverage. Existing engine coverage exercises only a synthesized dev/done result with follow-up review disabled (test_engine.py L5820-L5847). Add integration coverage proving synthesized review and blocked results reach marker repair and journal the correct outcome.

Suggested resolution plan

  1. Compute transition evidence before the hash decision. Refuse an equal launch hash only when no same-session transition was observed. Add regressions for equal-hash/no-transition refusal and equal-hash/observed-transition synthesis.
  2. Route stories-mode terminal readback through the same evidence ladder. Missing-marker stories results must receive the synthesis flag, optional one-time nudge, and engine repair; add an mtime-only stale-done rejection test.
  3. Replace the whole-file truncating repair with a sibling temporary file plus atomic replacement, preserving original bytes, permissions, and line endings. Add injected short-write/disk-full tests asserting the original spec remains intact.
  4. Add a cross-path matrix for sprint/stories, live/dead-window, equal/different hash, observed/missed transition, done/blocked, Generic/OpenCode, and dev/review engine repair.
  5. Re-run targeted tests, full pytest, Trunk, and pyright, then re-review the PR.

Verification performed

  • Focused suite: 596 passed
  • Full suite: 2,888 passed, 1 skipped; only the two documented pre-existing installed-skill module.yaml drift failures remained
  • trunk check --no-fix: clean across all 15 modified files
  • Current GitHub CI, lint, typecheck, and version-sync checks: green

Recommendation: hold merge until findings 1-3 are addressed and the review-path coverage is added.

@pbean

pbean commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Review addendum: lower-priority findings

The main review comment contains all blocking findings and the medium verification gap. For completeness, these additional hardening findings were raised during the independent review layers and should remain visible even though they are not merge blockers:

  1. Low: lexical path comparison can disable M1/M2 evidence. Snapshot matching uses raw string equality at generic.py L1256-L1259 and generic.py L1305-L1307, while snapshot capture stores the task path spelling directly at engine.py L2439-L2443. An equivalent path containing .., a symlink alias, or a case variant on a case-insensitive filesystem can skip both hashing and transition attribution. Normal generated paths are canonical, which lowers reachability, but the deterministic gate should compare resolved paths or filesystem identity. Add alias-path coverage.

  2. Low: dev_contract_nudge silently coerces invalid TOML types. policy.py L720-L722 applies bool(...), so dev_contract_nudge = "false" and other non-empty invalid values enable the nudge. This follows an existing repository-wide parsing convention, so it is better handled as strict policy-type validation rather than broadening this PR unexpectedly. Validate that the raw value is exactly boolean and add invalid-type tests.

  3. Low: a spec ending with a bare carriage return can receive an invisible marker. devcontract.py L519-L528 treats a trailing bare \r as an existing newline but chooses \n for the appended block. The marker regex recognizes line starts after \n, not bare \r, so the appended heading can remain invisible to future scans. Bare-CR files are uncommon, but newline normalization should ensure the heading begins after a recognized line boundary; add a regression test.

These do not change the prior recommendation: findings 1-3 remain the merge blockers, finding 4 is the required verification gap, and findings 5-7 are lower-priority hardening work.

…findings (#276)

A second-model adversarial review of PR #277 surfaced 7 valid findings. This
lands the fixes for six of them (the 7th, a repo-wide policy-coercion
consistency issue, is deferred to a follow-up):

- #1 (High) M2 transition now outranks the M1 hash gate. A clean review that
  round-trips done->in-review->done back to byte-identical launch bytes while
  omitting its marker no longer false-refuses; the hash gate refuses only when
  no transition was observed. Safe: the dead-window false positive never records
  a transition (status never leaves `done`).
- #2 (High) The stories-mode folder+id read-back now applies the same
  launch-snapshot gate, closing the identical false completion it previously
  bypassed (it accepted on the mtime floor alone). Inert on a dev leg (no
  snapshot).
- #3 (High) The three in-place spec rewriters (append/strip/reset) now write
  atomically (temp + atomic_replace), so an interrupted/disk-full repair leaves
  the original spec intact instead of truncated.
- #4 (Med) Adds engine-level coverage of the synthesized-REVIEW repair path
  (including a follow-up review that rewrites the spec) plus regressions for
  #1/#2/#3/#5/#7 and a _snapshot_verdict truth-table.
- #5 (Low) Snapshot/candidate identity is now by resolved filesystem path
  (_same_spec), not raw string spelling, so a `..`/symlink/case alias can't
  disable M1/M2.
- #7 (Low) A spec ending in a bare `\r` no longer receives an invisible heading
  (completed to CRLF so the scan's `^` regex sees it).

#1 and #2 share a single `_snapshot_verdict` helper so the "M2 outranks M1"
rule can't drift between the two paths. All edits sit on `_DevSynthesisMixin`,
covering both the tmux and OpenCode adapters.
@pbean

pbean commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Review response — findings addressed in e6eda85

Thanks for the thorough adversarial pass. Validated all 7 against the code; 6 are fixed on-branch, #6 is deferred with rationale. Summary per finding:

Blocking (from Comprehensive review findings)

  1. Hash equality overrides transition evidence — fixed. Reordered so a recorded mid-session transition (M2) outranks the M1 content-hash gate: the gate now refuses only when no transition was observed. A clean review that round-trips done → in-review → done back to the launch bytes while omitting its marker (M2 recorded the in-review) now synthesizes instead of false-refusing. Pressure-tested safe — the genuine dead-window false positive never records a transition (status never leaves done, so _observe_tick records nothing). The old test_hash_gate_beats_transition_evidence asserted the inverted semantics and was replaced by test_transition_beats_hash_gate.

  2. Stories-mode readback bypassed all four mechanisms — fixed. _stories_synth_result now applies the same launch-snapshot decision before accepting a terminal spec, so an unmodified done spec with only an mtime bump and no transition is refused (unmodified-since-launch) exactly as on the scan path. The enabler was already present: stories-mode review sessions carry spec_snapshot/_fm_transition_obs via the shared review loop; the readback simply ignored them. Inert on a dev leg (no snapshot). ci: bump actions/checkout from 6 to 7 #1 and docs(roadmap): TEA plugin shipped (experimental) #2 route through one shared _snapshot_verdict helper so the ordering rule can't drift between paths (covered by a truth-table unit test).

  3. Marker repair could truncate the spec — fixed. append_auto_run_result and its siblings strip_auto_run_result / reset_spec_status (same repair-write doctrine) now write atomically via a same-dir temp + platform_util.atomic_replace; a failed/interrupted/disk-full write leaves the original spec byte-for-byte intact with no .tmp litter. Regression test injects an OSError mid-rename for all three writers and asserts the original is preserved.

  4. Review-repair path lacked engine coverage — fixed. Added test_synthesized_review_result_repairs_marker_and_story_completes and test_synthesized_review_repair_survives_followup_rewrite (a synthesized review cycle repairs the marker, then a clean cycle rewrites the spec — story still converges, exactly one repair, no livelock).

Addendum

  1. Lexical path comparison — fixed. Snapshot/candidate identity is now filesystem-based via _same_spec (Path.resolve(), the repo convention; non-strict, degrades to "not same file" on an unresolvable path), so a ../symlink/case-variant alias can no longer disable M1/M2. Alias-path regression added.

  2. dev_contract_nudge type coercion — deferred → Strict scalar-type validation for limits.* policy fields #278. Confirmed real (bool("false") is True), but the whole limits.* path is uniformly coerce-then-validate; a strict bool for this one field would be an inconsistent island. Filed Strict scalar-type validation for limits.* policy fields #278 for a cross-cutting strict scalar-type validation sweep following the existing plugin-settings precedent. (Only the quoted "false" misbehaves; a well-formed TOML false already works.)

  3. Bare-CR invisible marker — fixed. A spec ending in a bare \r is now completed to CRLF before the append, so ## Auto Run Result lands after a \n the ^-anchored AUTO_RUN_HEADING_RE recognizes. Regression asserts the heading parses and a second append is idempotent (i.e. visible to the scan).

All edits sit on _DevSynthesisMixin, covering both the tmux and OpenCode adapters. Full suite green (the two bmad-loop-setup skill-sync failures are pre-existing and unrelated). CHANGELOG #276 entry updated to match the corrected M1/M2 behavior.

@pbean
pbean merged commit 2a614a8 into main Jul 23, 2026
10 checks passed
@pbean
pbean deleted the feat/deterministic-marker-catch-276 branch July 24, 2026 01:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deterministic missing-marker catch + repair (follow-up to #224/#271, PR #273)

1 participant