Skip to content

feat(core): add inert Phase 6I deferral primitives - #450

Merged
pbean merged 4 commits into
mainfrom
port/phase-6i-deferral-primitives
Aug 4, 2026
Merged

pbean merged 4 commits into
mainfrom
port/phase-6i-deferral-primitives

Conversation

@pbean

@pbean pbean commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add inert persisted deferred-work state to StoryTask with backward-compatible rehydration.
  • Add pure deferred-finding parsing/fingerprinting and a guarded ledger mark_open primitive.
  • Add fixtures and regression coverage for YAML shapes, normalization, guard behavior, and atomic writes.

This is pure/inert Phase 6I foundation work. It adds no product readers, engine threading, sweep behavior, verification behavior, or Phase 6J+ behavior.

References #433.

Validation

  • Full suite on Python 3.13 and 3.14: 4,072 passed, 30 skipped (4,102 collected) per lane.
  • 11-row ablation matrix on both lanes; every intended assertion reddened.
  • uv run pyright
  • trunk check --no-fix
  • git diff --check

Summary by CodeRabbit

  • New Features

    • Added support for reopening eligible deferred-work items using matching resolution details.
    • Added deferred-review finding capture with normalized severity, validated details, and stable identification.
    • Deferred-work progress is now preserved across task state saves and restores, including older state files.
  • Bug Fixes

    • Improved handling of missing, malformed, or mismatched deferred-work entries without altering unrelated records.
  • Tests

    • Expanded coverage for deferred-work updates, finding validation, fingerprints, and state persistence.

@greptile-apps greptile-apps 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.

pbean has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR adds operation-owned reopening for deferred-work entries, tolerant parsing and fingerprinting for deferred findings, and persisted deferred-work metadata on StoryTask. Tests cover validation, normalization, serialization, compatibility, and atomic writes.

Changes

Deferred-work lifecycle

Layer / File(s) Summary
Reopen closed ledger entries
src/bmad_loop/deferredwork.py, tests/conftest.py, tests/test_deferredwork.py
Reopenable closes record operation-specific undo metadata. mark_open validates that metadata and atomically restores the prior status. Tests cover validation, tampering, idempotence, normalization, and write failures.
Parse and fingerprint deferred findings
src/bmad_loop/devcontract.py, tests/conftest.py, tests/test_devcontract.py
Deferred frontmatter entries are normalized, validated, severity-mapped, and assigned stable fingerprints. Malformed entries produce diagnostics without rejecting valid siblings. Tests cover limits, ordering, immutability, and fingerprint inputs.
Persist deferred-work task state
src/bmad_loop/model.py, tests/test_model.py
StoryTask serializes and restores ledger snapshots, harvest flags, deferrals, bundle closures, and carryover state with legacy defaults and container isolation.

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

Possibly related PRs

Poem

A rabbit marks each close with care,
Then checks the matching key is there.
Findings gain a stable name,
Task state joins the JSON frame.
Safe writes guard each ledger gate.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% 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 pull request's main change: adding inert Phase 6I deferral primitives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch port/phase-6i-deferral-primitives

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/deferredwork.py`:
- Around line 385-398: The mark_done update path currently rewrites any non-open
status with a matching resolution; restrict it to entries whose status matches
the `status: done <date>` form before calling `atomic_write_text`. Update the
status validation around `STATUS_RE` in the relevant deferred-work function, and
add a regression test proving a non-done status and its resolution line remain
unchanged.

In `@src/bmad_loop/model.py`:
- Line 410: Deep-copy each restored record in the harvested_deferrals handling
of the model deserialization flow instead of using a shallow dict copy,
preserving independence of nested values. In tests/test_model.py lines 236-247,
add a nested mutable deferred-record value and verify that mutating the restored
value does not modify the original doc; update src/bmad_loop/model.py lines
410-410 for the implementation.
🪄 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: 75c5bbb3-f909-4d7a-a100-5a5b150c0fb9

📥 Commits

Reviewing files that changed from the base of the PR and between f7e17e4 and 3aa2b26.

📒 Files selected for processing (7)
  • src/bmad_loop/deferredwork.py
  • src/bmad_loop/devcontract.py
  • src/bmad_loop/model.py
  • tests/conftest.py
  • tests/test_deferredwork.py
  • tests/test_devcontract.py
  • tests/test_model.py

Comment thread src/bmad_loop/deferredwork.py Outdated
Comment thread src/bmad_loop/model.py Outdated
@pbean

pbean commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3aa2b2656d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +384 to +386
entry = _find_entry(text, dw_id)
if entry is None or entry.open:
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refuse malformed statuses before reopening

When an entry has a non-open, non-closed status such as status: malformed followed by a matching resolution note, this guard treats it as closed and rewrites it to status: open. parse_ledger and classify distinguish such entries as malformed, so mark_open can mutate a human-authored or corrupt entry even though it promises to undo only a specific mark_done; require a valid done status before applying the rewrite.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in d2e5895. mark_open now requires a canonical done YYYY-MM-DD status via the existing validator before it can reopen an entry. Regression coverage preserves malformed, non-date, and impossible-date statuses byte-for-byte. The focused changed-area suite passes (417 tests), as do Pyright and trunk check.

Comment thread src/bmad_loop/devcontract.py Outdated
Comment on lines +253 to +257
summary = _flatten(item.get("summary"), _SUMMARY_LIMIT)
if not summary:
malformed.append(f"item {i}: no usable `summary`")
continue
location = _flatten(item.get("location"), _LOCATION_LIMIT)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject collection-valued finding fields

When malformed YAML supplies a collection for a text field, for example summary: [first, second], _flatten stringifies it and the item is accepted as a valid finding rather than being added to malformed. This files Python container representations into the ledger and derives persistent dedup fingerprints from them, while suppressing the malformed-item report; validate that summary, evidence, and location are YAML scalars before flattening them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in d2e5895. Deferred summary, evidence, and location values now reject YAML collection shapes instead of stringifying them; the malformed item is reported while valid siblings remain harvestable. Added list/dict coverage across all three fields. The focused changed-area suite passes (417 tests), as do Pyright and trunk check.

@greptile-apps greptile-apps 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.

pbean has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@pbean

pbean commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2e589557d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/bmad_loop/deferredwork.py Outdated
Comment on lines +399 to +400
res_m = _MARK_DONE_TAIL_RE.match(entry.body, status_m.end())
if res_m is None or res_m.group(1).strip() != _one_line(note).strip():

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Track the specific close before reopening it

When an ID was already closed by an earlier run of the same story, the current close skips it, but Engine._apply_deferred_closes uses the reusable note resolved by story {task.story_key} for every run. A rollback that passes the intended ID and that note therefore satisfies this guard and reopens work the current run never closed. Persist the IDs actually returned by mark_done_many, or use an operation-specific marker, rather than treating the note as proof of ownership.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Validated and fixed in a8817cc. Reusable resolution text is no longer treated as ownership proof: rollback-capable closes now use mark_done_many_reopenable, which writes an operation-specific, ledger-resident undo marker only for IDs actually flipped. The operation ID is derived from durable run/task identity, so it can be recomputed after crash/resume; an earlier same-note close has no matching marker and remains closed. Ordinary mark_done/mark_done_many output and return contracts are unchanged. Regression coverage includes same-note earlier operations, wrong owners, markerless legacy closes, crash-time identity reconstruction, tampering, and atomic failures. Focused suite: 445 passed; Pyright, trunk check, and git diff --check are clean.

Comment thread src/bmad_loop/deferredwork.py Outdated
Comment on lines +402 to +404
start = entry.span[0] + status_m.start()
end = entry.span[0] + res_m.end()
atomic_write_text(path, text[:start] + "status: open" + text[end:])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the original open status line

When the original entry has parser-accepted formatting such as status: open or status: open # annotation, mark_done replaces the entire line and this undo recreates only status: open. Consequently a close/reopen rollback is not character-for-character and leaves the user-authored persisted ledger modified even though the operation claims to undo one specific close; retain enough pre-close text to restore the original line.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Validated and fixed in a8817cc. Reopenable closes now encode the exact original raw status: line in their durable undo marker, and mark_open restores that line verbatim after validating ownership, date, note, and adjacency. Regression cases cover extra spaces, tabs, inline annotations, malformed/tampered payloads, and splitlines characters such as U+2028. Standard non-reopenable close formatting remains unchanged. Focused suite: 445 passed; Pyright, trunk check, and git diff --check are clean.

Comment on lines +216 to +218
return hashlib.sha1(
"\0".join(parts).encode("utf-8"),
usedforsecurity=False,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Escape embedded NULs before fingerprinting

When a YAML scalar contains an escaped NUL, this encoding is not injective: harvest_fingerprint("a\0b", "c") equals harvest_fingerprint("a", "b\0c") because both produce the same joined bytes. Distinct deferred findings can therefore receive the same persistent identity and be deduplicated as one; reject embedded NULs, escape them, or length-prefix each part before hashing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Validated and fixed in a8817cc. PyYAML does preserve escaped NULs, and the two example tuples produced the same fingerprint. parse_deferred_findings now rejects embedded NULs in summary, evidence, and location per item while preserving valid siblings, and harvest_fingerprint guards direct callers. Rejection happens before clamping, while every existing non-NUL fingerprint remains unchanged. Regression coverage includes all three fields, post-clamp-position NULs, both ambiguous tuples, and the pinned clean fingerprint. Focused suite: 445 passed; Pyright, trunk check, and git diff --check are clean.

@greptile-apps greptile-apps 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.

pbean has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@pbean

pbean commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 1

🤖 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/deferredwork.py`:
- Around line 358-381: Serialize the complete ledger read-modify-write
transactions with one per-ledger lock. In src/bmad_loop/deferredwork.py lines
358-381, update _mark_done_many to acquire the shared lock before reading,
validating, applying mutations, and calling atomic_write_text; in
src/bmad_loop/deferredwork.py lines 442-487, use that same lock for the entire
mark_open transaction, re-reading and validating its target entry while locked.
🪄 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: 136b33a5-8d9f-4d66-9a04-1ec0baa66dbe

📥 Commits

Reviewing files that changed from the base of the PR and between 3aa2b26 and a8817cc.

📒 Files selected for processing (6)
  • src/bmad_loop/deferredwork.py
  • src/bmad_loop/devcontract.py
  • src/bmad_loop/model.py
  • tests/test_deferredwork.py
  • tests/test_devcontract.py
  • tests/test_model.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/test_model.py
  • tests/test_devcontract.py
  • tests/test_deferredwork.py
  • src/bmad_loop/model.py

Comment on lines +358 to +381
def _mark_done_many(
path: Path,
dw_ids: Sequence[str],
date: str,
note: str,
*,
operation_id: str | None = None,
) -> list[str]:
"""Shared atomic implementation for the public close operations."""
_require_iso_date(date)
undo_owner = _operation_digest(operation_id) if operation_id is not None else None
if not path.is_file():
return []
text = path.read_text(encoding="utf-8")
marked: list[str] = []
for dw_id in dw_ids:
updated = _apply_done(text, dw_id, date, note, undo_owner=undo_owner)
if updated is None:
continue
text = updated
marked.append(dw_id)
if not marked:
return []
atomic_write_text(path, text)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Serialize ledger read-modify-write operations.

atomic_write_text does not prevent lost updates. If mark_done_many_reopenable and mark_open, or two close operations, read the ledger concurrently, the later replacement can discard the other operation's change.

Use one per-ledger lock around the read, validation, mutation, and atomic write. Re-read and validate the target entry while holding that lock.

  • src/bmad_loop/deferredwork.py#L358-L381: lock the complete _mark_done_many transaction.
  • src/bmad_loop/deferredwork.py#L442-L487: use the same lock for the complete mark_open transaction.
📍 Affects 1 file
  • src/bmad_loop/deferredwork.py#L358-L381 (this comment)
  • src/bmad_loop/deferredwork.py#L442-L487
🤖 Prompt for 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.

In `@src/bmad_loop/deferredwork.py` around lines 358 - 381, Serialize the complete
ledger read-modify-write transactions with one per-ledger lock. In
src/bmad_loop/deferredwork.py lines 358-381, update _mark_done_many to acquire
the shared lock before reading, validating, applying mutations, and calling
atomic_write_text; in src/bmad_loop/deferredwork.py lines 442-487, use that same
lock for the entire mark_open transaction, re-reading and validating its target
entry while locked.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: a8817cc7f6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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.

1 participant