Files
automaton/tasks/complete/add-blast-radius-scheduler/SPEC.md
T
Lap Tran 4a2301b077
CI / build (push) Has been cancelled
Archive completed tasks, add cleanup commands, self-documenting dashboard UI
- Archive 79 completed framework-dev tasks from tasks/ -> tasks/complete/
- status.py: add --cleanup-done and --install-cleanup-schedule commands
- Add scripts/automaton-cleanup.sh for periodic task archiving
- Dashboard: rename 'Background' tab -> 'Agent', 'Cleanup' agent -> 'Completed Task Archiver', remove redundant group headers and pill badges, dim inactive agent placeholders
- .rules.md: add Self-Documenting UI Names rule
- New tests: test_cleanup_done.py, expanded test_app.py and test_task.py
2026-06-24 22:43:33 -04:00

78 lines
7.3 KiB
Markdown

# 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).