- **Restore 82 completed tasks** from tasks/complete/ back to tasks/ top level (all <7 days old per the cleanup policy; premature bulk archive was fixed). - **Dashboard: fix scroll-reset on auto-refresh** — renderBoard rebuilds the board via innerHTML every 2s, destroying each column-body's scrollTop. Now snapshots column-body scrollTop + board.scrollLeft + view.scrollTop before rebuild and restores after (matched by PHASE_GROUPS index). - **Dashboard UI additions** (pre-existing unstaged work): approval section cards, transition buttons, inline artifact editor (textarea for writing missing SPEC/VERDICT/etc from the detail modal). - **Bind ornith as Implement model** — config.md: Model explicit to omlx/Ornith-1.0-35B-4bit-mlx, context window 32768. Interactive autopilot already used ornith via opencode default; now explicit. - **Fix cleanup stub** — automaton-cleanup.sh had a stale --project arg pointing at a pytest temp dir (test isolation leak). Rewired to point at ~/.automaton. - **Fix plist-isolation test** — test asserted host plist doesn't exist, but a real install creates it. Now snapshots mtime before run, asserts unchanged after (only a write during the test counts as bleed). - **New Playwright smoke test** (tests/test_dashboard_ui.py) — 2 tests: board renders tasks, column scroll survives auto-refresh tick. Verified the test fails without the scroll fix (scrollTop resets to 0). Skipped via importorskip when playwright is absent (main CI stays green). - **Clarify SI loop scope in README** — new-project onboarding section documents the framework-scoped self-improvement loop and options (leave/pause/create project loop). - **CHANGELOG** documents all changes including the known model-divergence gap (mde tasks marked complete but per-role model binding was never implemented).
7.3 KiB
7.3 KiB
SPEC: add-blast-radius-scheduler
Context
Task 2 (add-status-brakes) shipped --can-edit --loop [--loop-worktree], the _gate_worktree_drift brake gate, and platform.system() dispatch for scheduler generation. Task 3 (add-loop-runner) shipped the runner with a stub at step 4: # worktree plumbing lands in task add-blast-radius-scheduler. The runner currently uses state["worktree_path"] if set, else falls back to project_root -- but never creates the worktree. This task closes that gap: the runner ensures a per-loop git worktree exists before spawning the Implement role, per technical.md section 7 step 4 and D2.
Non-Goals (deferred)
--no-worktreeCLI flag for--create-loop-> v1.1 (theblast_radius.use_worktree: falsefield inloop.jsonis the v1 opt-out mechanism; a CLI flag is convenience sugar).- Worktree garbage collection / pruning -> v1.1 (BACKLOG
worktree-gc). blast_radius.base_branchparameterization -> v1.1 (hardening item; v1 hardcodesmainas the base).--claim-loop-taskatomic ownership -> v1.1.- Fcntl lock on worktree creation -> v1.1 (same TOCTOU item as
add-status-brakesA6).
Requirements
R1 -- _ensure_worktree helper in loop-runner.py
- New function
_ensure_worktree(state, cfg, loop_path, project_dir) -> strthat returns the cwd to use for harness invocations. - Reads
blast_radius.use_worktreefromloop.json(default:Truewhen the field is missing, matching D2 "default is worktree-on"). - When
use_worktreeisFalse: returnstr(project_dir)immediately. No git calls. No state mutation. - When
use_worktreeisTrueandstate["worktree_path"]is already set and the path exists: return the existing worktree path. No state mutation. - When
use_worktreeisTrueandstate["worktree_path"]is null or the path no longer exists:- Determine the worktree path:
<loop_path>/worktree(usingLOOP_WORKTREE_DIR = "worktree"). - Determine the branch name:
loop/<name>where<name>isstate["name"]or the loop dir name. - Run
git rev-parse --is-inside-work-treefromproject_dirto verify it is a git repo. If not, fall back to R2. - Run
git worktree add <worktree_path> -b loop/<name>fromproject_dir. If the branch already exists, usegit worktree add <worktree_path> loop/<name>(checkout existing branch, no-b). - On success: update
state["worktree_path"]andstate["worktree_branch"], write state atomically, return the worktree path. - On failure: fall back to R2.
- Determine the worktree path:
- Tests:
test_ensure_worktree_creates_worktree,test_ensure_worktree_reuses_existing,test_ensure_worktree_use_worktree_false_returns_project_root,test_ensure_worktree_missing_field_defaults_true.
R2 -- Graceful degradation (no git / not a repo / worktree creation fails)
- If
gitis not found (FileNotFoundError), orgit rev-parse --is-inside-work-treefails (non-zero exit), orgit worktree addfails (non-zero exit): log a WARNING to.state.logand returnstr(project_dir)as cwd. - The loop does NOT halt. The tick proceeds with
cwd = project_dir. The drift gate (_gate_worktree_drift) will skip itself becauseworktree_pathremains null. - This makes worktree creation best-effort: a loop configured with
use_worktree: trueon a non-git project simply edits the primary checkout. The operator is responsible for understanding this trade-off (documented infunctional.md). - Tests:
test_ensure_worktree_falls_back_when_not_git_repo,test_ensure_worktree_falls_back_when_git_missing,test_ensure_worktree_falls_back_when_worktree_add_fails,test_ensure_worktree_logs_warning_on_fallback.
R3 -- Integration into cmd_tick
- Replace the current step 4 block in
cmd_tick(lines ~493-498 ofloop-runner.py) with a call to_ensure_worktree(state, cfg, loop_path, project_dir). - The returned cwd is used for all three role invocations (Implement, Verify, Orchestrate).
- The state mutation (setting
worktree_path/worktree_branch) happens inside_ensure_worktreevia_write_state_loop. This is safe because it occurs before any harness subprocess; a crash after this point but before step 10 leaves the worktree path recorded (which is correct -- the worktree exists on disk). - Tests:
test_tick_creates_worktree_on_first_tick,test_tick_reuses_worktree_on_second_tick,test_tick_falls_back_to_project_root_when_no_git.
R4 -- Worktree branch already exists
- When
git worktree add <path> -b loop/<name>fails because the branch already exists (exit code 128, stderr containsalready exists), retry withgit worktree add <path> loop/<name>(checkout existing branch without-b). - If the retry also fails, fall back to R2.
- This handles the case where a loop was previously created, the worktree was deleted, but the branch remains in the repo.
- Tests:
test_ensure_worktree_reuses_existing_branch,test_ensure_worktree_falls_back_when_branch_checkout_fails.
R5 -- State consistency
_ensure_worktreewritesworktree_pathandworktree_branchto.state.loopatomically via_write_state_loop(same tmp+rename pattern).- If the worktree path was previously set but the directory no longer exists (e.g. manually deleted), clear
worktree_pathandworktree_branchin state before attempting recreation. If recreation fails, leave them cleared (R2 fallback). - Tests:
test_ensure_worktree_clears_stale_worktree_path,test_ensure_worktree_recreates_after_deletion.
R6 -- Platform path handling
- Use
pathlib.Pathfor all path construction. On Windows,Pathhandles backslash separators automatically. - The
git worktree addcommand receives the worktree path as a string; git handles OS-specific path normalization on its own. - No
platform.system()calls needed in the runner for worktree creation (unlike--install-schedulewhich generates OS-native scheduler units). The runner's worktree creation is platform-agnostic viaPath. - Tests:
test_worktree_path_uses_pathlib(verify the path is constructed viaPathnot string concatenation; checked by examining the argv passed tosubprocess.run).
R7 -- Doc updates
- Update
design/loops/technical.mdsection 7 step 4 to note the runner now creates the worktree (remove the "deferred" language if present). - Update
CHANGELOG.mdunder[unreleased]. - Tests: none (doc-only).
R8 -- New test file tests/test_blast_radius.py
- Mirrors
test_loop_runner.py's stubbing pattern (monkeypatch.setattr(subprocess, "run", fake_run)). - Covers R1-R6 as itemized above; target 12-16 tests.
- All subprocess calls stubbed; no live git operations in CI. For tests that need a real git repo, use
tmp_path+subprocess.run(["git", "init"])in a fixture (these are integration tests that hit the real git binary but are fast and deterministic). - Add one regression test:
test_existing_loop_with_worktree_path_ticks_unchanged-- a loop withworktree_pathset and the path existing still ticks without callinggit worktree add. - Tests: self-referential (the file IS the test).
Verification
python3 -m py_compile scripts/loop-runner.pypython3 -m pytest tests/test_blast_radius.py -vpython3 -m pytest tests/ -q-- full suite must remain green; expected total approx 370 (354 + 12-16 new).bash -n scripts/*.sh(no shell changes; safety check).