Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ breaking changes may land in a minor release.

### Added

- Restore and normalize attempt-owned specs on Windows through NT handle-relative
opens instead of pausing for manual adoption. `win32_at` binds `NtCreateFile` with a
`RootDirectory` handle plus `FileRenameInformationEx`/`FileDispositionInformationEx`
(POSIX semantics, classic classes as the fallback) and refuses a symlink or junction
as `O_NOFOLLOW` refuses a link; `platform_util.HANDLE_ANCHORED_WRITES` joins that arm
to the POSIX `dir_fd` one behind `open_at`/`stat_at`/`replace_at`/`unlink_at`, so the
confined spec writers and `open_dir_confined` anchor at a handle on both hosts. The
DW-309/DW-310 refusal now fires only on a host with neither arm; its journaled
`problem` reads `lacks handle-anchored writes`.

- Accept a session-asserted artifact-only sweep bundle at the dev proof-of-work gate
(DW-273). A bundle whose only deliverable lives under a gitignored
`implementation_artifacts` (a spec-only erratum) burned every attempt on
Expand Down
4 changes: 2 additions & 2 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ Result` section. Every other spec keeps warn-and-continue, and the record says w
(#705). Sweep migration and triage tasks make the same rollover automatically when an
`ESCALATED` task restarts with a fresh attempt budget; mid-flight, non-escalated restarts keep
their current generation because their continuing attempt counter already provides a fresh id.
- Attempt-owned sprint-spec recovery (#123, #630): a bound plain attempt whose only residue is its own lifecycle flip is normalized back to its pre-attempt lifecycle status, proven Git-clean, and retried. Every bound retry chain snapshots its first spec input byte-for-byte and retains it across both dev-verification and review-verification repair sessions; a resolved re-drive therefore retains the operator-corrected `ready-for-dev` input rather than a failed child's later body. Repair entry points validate retained authority before constructing a prompt that can reset the spec. A non-fixable retry parks the failed child first, restores that snapshot, and re-establishes the promised route after resetting sibling residue. Descriptor-capable restoration retains the staged inode across publication and verifies that exact inode before accepting it. A platform without descriptor-relative writes refuses before staging or lifecycle normalization, leaves the current attempt-owned spec bytes untouched by restoration, and pauses for manual adoption. On descriptor-capable platforms, the same snapshot restores pre-launch operator edits when a plain child puts a tracked spec back at Git baseline. Git-ignored and pre-existing-untracked bound specs use the byte snapshot as their dirtiness oracle and are force-included only in the private recovery ref before restoration; index-only force-adds and cached removals also trigger cleanup and restore baseline index ownership. That real repair reports `rollback-owned-spec-restored`, never `rollback-skipped-clean`. Missing, unreadable, deleted, retargeted, changed external, or unsafe legacy authority pauses once with spec-specific adoption instructions and clears the unusable pair so manual recovery can converge; recovery also refuses a reset whose baseline would replace the canonical path or a parent directory with a symlink, tree, file, or other unsafe shape. An initial Sprint binding fault may safely degrade to an unbound bare-key launch; an existing Stories folder+id target instead aborts unless it can be snapshotted. Once an explicit binding is durable, a later snapshot fault aborts before child launch while retaining that authority for recovery. Fresh sprint tasks with no recorded path remain bare-key dispatches; other substantive changes or sibling residue follow rollback policy; Stories remains folder+id; Sweep remains intent-bundle routing; snapshots are retired after commit; and recovery never auto-commits the human correction.
- Attempt-owned sprint-spec recovery (#123, #630): a bound plain attempt whose only residue is its own lifecycle flip is normalized back to its pre-attempt lifecycle status, proven Git-clean, and retried. Every bound retry chain snapshots its first spec input byte-for-byte and retains it across both dev-verification and review-verification repair sessions; a resolved re-drive therefore retains the operator-corrected `ready-for-dev` input rather than a failed child's later body. Repair entry points validate retained authority before constructing a prompt that can reset the spec. A non-fixable retry parks the failed child first, restores that snapshot, and re-establishes the promised route after resetting sibling residue. Restoration is anchored at a handle on the spec's parent directory on both supported hosts — the POSIX `dir_fd` family, and on Windows NT handle-relative opens (`win32_at`: `NtCreateFile` with a `RootDirectory`, `FileRenameInformationEx`/`FileDispositionInformationEx` with POSIX semantics, reparse points refused as a link) — retains the staged inode across publication and verifies that exact inode before accepting it. A host with neither arm refuses before staging or lifecycle normalization, leaves the current attempt-owned spec bytes untouched by restoration, and pauses for manual adoption. The same snapshot restores pre-launch operator edits when a plain child puts a tracked spec back at Git baseline. Git-ignored and pre-existing-untracked bound specs use the byte snapshot as their dirtiness oracle and are force-included only in the private recovery ref before restoration; index-only force-adds and cached removals also trigger cleanup and restore baseline index ownership. That real repair reports `rollback-owned-spec-restored`, never `rollback-skipped-clean`. Missing, unreadable, deleted, retargeted, changed external, or unsafe legacy authority pauses once with spec-specific adoption instructions and clears the unusable pair so manual recovery can converge; recovery also refuses a reset whose baseline would replace the canonical path or a parent directory with a symlink, tree, file, or other unsafe shape. An initial Sprint binding fault may safely degrade to an unbound bare-key launch; an existing Stories folder+id target instead aborts unless it can be snapshotted. Once an explicit binding is durable, a later snapshot fault aborts before child launch while retaining that authority for recovery. Fresh sprint tasks with no recorded path remain bare-key dispatches; other substantive changes or sibling residue follow rollback policy; Stories remains folder+id; Sweep remains intent-bundle routing; snapshots are retired after commit; and recovery never auto-commits the human correction.
- Intent-gap patch-restore (BMAD-METHOD#2564): when review halts on an `intent gap`, the dev primitive saves the attempted change as a patch file (referenced from the halt output) before reverting the tree. If that reading turns out to be correct, the resolve agent adds `"restore_patch": "<path>"` to its `resolution.json`; the orchestrator re-arms the spec to `in-review` (not `ready-for-dev`) and re-applies the patch after every reset, so the re-driven session resumes _review_ on the restored diff instead of re-implementing. `bmad-loop resolve --no-interactive --restore-patch <path>` does the same by hand. A patch that fails to apply escalates rather than dispatching onto a half-restored tree. Sweep bundles get the same recovery.

### Git worktree isolation (opt-in)
Expand Down Expand Up @@ -708,7 +708,7 @@ persisted artifacts.
### Configuration (`.bmad-loop/policy.toml`)

- Single policy file written by `init`, stamped into the run at every engine start — `run`, `sweep`, `resume` — so it always describes the policy that process enforces (applies to new runs and resumes; editable live from the TUI).
- Rewrites of it are confined and permission-honoring (#593, #597). On POSIX such a write walks the components below the project `O_NOFOLLOW` and lands through the descriptor that walk produced; so a symlink planted at `.bmad-loop/` is refused rather than followed — refusing a link at the file alone never covered its parent, and `mkdir(parents=True, exist_ok=True)` accepts a symlinked directory. Windows has no `*at()` family and degrades to a documented check-then-write; `init`'s one-time seeding of a missing policy predates any session and stays a plain write. A `policy.toml` an operator marked read-only is refused with a `PermissionError` instead of being replaced and left still reading `0444`. The confined walk also covers story specs inside the checkout, park records, the decisions store, and the sweep's triage cache and bundle intent document (DW-269); the read-only refusal reaches further — story specs, `sprint-status.yaml`, park records, the decisions store, hook `settings.json` — but `sprint-status.yaml` deliberately keeps its symlink-following writer (an operator may keep the board behind a link) and the hook-settings and worktree-provisioning writers keep their own pre-existing link checks rather than the descriptor walk. The read-only refusal deliberately skips machine-minted state (run archives, stop requests, the config-digest stamp) — those are channels, not operator signals.
- Rewrites of it are confined and permission-honoring (#593, #597). Such a write walks the components below the project no-follow and lands through the directory handle that walk produced — `O_DIRECTORY|O_NOFOLLOW` opens with `dir_fd` on POSIX, `NtCreateFile` relative to the handle above with reparse points refused on Windows (`win32_at`) — so a symlink or junction planted at `.bmad-loop/` is refused rather than followed; refusing a link at the file alone never covered its parent, and `mkdir(parents=True, exist_ok=True)` accepts a symlinked directory. A host with neither arm degrades to a documented check-then-write; `init`'s one-time seeding of a missing policy predates any session and stays a plain write. A `policy.toml` an operator marked read-only is refused with a `PermissionError` instead of being replaced and left still reading `0444`. The confined walk also covers story specs inside the checkout, park records, the decisions store, and the sweep's triage cache and bundle intent document (DW-269); the read-only refusal reaches further — story specs, `sprint-status.yaml`, park records, the decisions store, hook `settings.json` — but `sprint-status.yaml` deliberately keeps its symlink-following writer (an operator may keep the board behind a link) and the hook-settings and worktree-provisioning writers keep their own pre-existing link checks rather than the descriptor walk. The read-only refusal deliberately skips machine-minted state (run archives, stop requests, the config-digest stamp) — those are channels, not operator signals.
- Sections — all 15: `[gates]`, `[limits]`, `[verify]`, `[notify]`, `[review]`, `[stories]` (which planning pipeline drives the loop: sprint-status or a typed `stories.yaml`), `[dev]` (see below), `[adapter]` (+ per-stage `[adapter.dev|review|triage]`), `[sweep]`, `[scm]` (worktree isolation + merge-back), `[cleanup]` (run-dir retention + disk reclamation), `[plugins]` (trust allowlist + per-plugin `[plugins.<name>]` config — e.g. the opt-in game-engine layer via `[plugins.unity]`, off by default), `[tui]` (`low_frame_rate` for slow/SSH links; persisted dashboard pane sizes), `[operator]` (whether a dev session may park a story at `awaiting-operator`), `[mux]` (machine-scoped multiplexer backend choice).
- `[dev] skill` names the inner dev skill the orchestrator drives. `"bmad-dev-auto"` — the generic upstream dev primitive — is the only accepted value; the field is retained as the seam for a future alternative dev skill, and any other value is rejected at load. It is **not** the name sessions are dispatched with: upstream renamed the primitive to `bmad-build-auto`, so the invoked name is resolved from what is actually installed and a project on either era works with this field untouched. It has no entry in the core settings schema, so it is edited in the file rather than from the TUI settings editor.
- Tunable limits: `max_review_cycles`, `max_dev_attempts`, `artifact_file_max_mb`, `artifact_payload_max_mb` (binary MiB, exactly 1,048,576 raw bytes each), `max_followup_reviews`, `session_timeout_min`, `git_timeout_s`, `teardown_grace_s` (one shared budget bounding the verified window kill _and_ the follow-on reap of any straggler descendant the session detached — e.g. a `setsid` background writer — combined; whatever remains after the window dies is what the straggler reap gets, before the worktree is merged and removed), `stop_without_result_nudges`, `dev_stall_grace_s`, `dev_stall_nudges`, `dev_stall_nudges_cap`, `workflow_stall_nudges_cap`, `max_tokens_per_story`.
Expand Down
Loading
Loading