Files
Lap Tran bc7daf8590 Restore archived tasks, fix dashboard scroll-reset, bind ornith, add Playwright smoke test
- **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).
2026-06-26 10:05:18 -04:00

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-worktree CLI flag for --create-loop -> v1.1 (the blast_radius.use_worktree: false field in loop.json is the v1 opt-out mechanism; a CLI flag is convenience sugar).
  • Worktree garbage collection / pruning -> v1.1 (BACKLOG worktree-gc).
  • blast_radius.base_branch parameterization -> v1.1 (hardening item; v1 hardcodes main as the base).
  • --claim-loop-task atomic ownership -> v1.1.
  • Fcntl lock on worktree creation -> v1.1 (same TOCTOU item as add-status-brakes A6).

Requirements

R1 -- _ensure_worktree helper in loop-runner.py

  • New function _ensure_worktree(state, cfg, loop_path, project_dir) -> str that returns the cwd to use for harness invocations.
  • Reads blast_radius.use_worktree from loop.json (default: True when the field is missing, matching D2 "default is worktree-on").
  • When use_worktree is False: return str(project_dir) immediately. No git calls. No state mutation.
  • When use_worktree is True and state["worktree_path"] is already set and the path exists: return the existing worktree path. No state mutation.
  • When use_worktree is True and state["worktree_path"] is null or the path no longer exists:
    1. Determine the worktree path: <loop_path>/worktree (using LOOP_WORKTREE_DIR = "worktree").
    2. Determine the branch name: loop/<name> where <name> is state["name"] or the loop dir name.
    3. Run git rev-parse --is-inside-work-tree from project_dir to verify it is a git repo. If not, fall back to R2.
    4. Run git worktree add <worktree_path> -b loop/<name> from project_dir. If the branch already exists, use git worktree add <worktree_path> loop/<name> (checkout existing branch, no -b).
    5. On success: update state["worktree_path"] and state["worktree_branch"], write state atomically, return the worktree path.
    6. On failure: fall back to R2.
  • 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 git is not found (FileNotFoundError), or git rev-parse --is-inside-work-tree fails (non-zero exit), or git worktree add fails (non-zero exit): log a WARNING to .state.log and return str(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 because worktree_path remains null.
  • This makes worktree creation best-effort: a loop configured with use_worktree: true on a non-git project simply edits the primary checkout. The operator is responsible for understanding this trade-off (documented in functional.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 of loop-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_worktree via _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 contains already exists), retry with git 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_worktree writes worktree_path and worktree_branch to .state.loop atomically 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_path and worktree_branch in 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.Path for all path construction. On Windows, Path handles backslash separators automatically.
  • The git worktree add command 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-schedule which generates OS-native scheduler units). The runner's worktree creation is platform-agnostic via Path.
  • Tests: test_worktree_path_uses_pathlib (verify the path is constructed via Path not string concatenation; checked by examining the argv passed to subprocess.run).

R7 -- Doc updates

  • Update design/loops/technical.md section 7 step 4 to note the runner now creates the worktree (remove the "deferred" language if present).
  • Update CHANGELOG.md under [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 with worktree_path set and the path existing still ticks without calling git worktree add.
  • Tests: self-referential (the file IS the test).

Verification

  • python3 -m py_compile scripts/loop-runner.py
  • python3 -m pytest tests/test_blast_radius.py -v
  • python3 -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).