Files
automaton/tasks/complete/add-loop-runner/IMPLEMENTATION.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

4.7 KiB
Raw Blame History

Implementation: add-loop-runner

Implements scripts/loop-runner.py per SPEC R1–R8.

File added

scripts/loop-runner.py — single entry point for --mode tick and --mode daemon. Stdlib only (no new pip deps).

Layout

  • LOOP_* constants mirroring status.py for the few state-shape facts the runner needs.
  • Small helpers duplicated inline rather than imported across scripts (per technical.md: scripts stay independent; no cross-script imports): _find_project_dir, _loops_dir, _loop_dir, _read_state_loop, _write_state_loop, _read_loop_config, _append_tick_log, _halt_loop.
  • _run_json(args) — invokes a subprocess and parses the last stdout line as JSON. Returns None on subprocess failure, non-zero exit, empty output, or JSON parse failure. Used by both _gate and _context_floor_ok.
  • _substitute(template, mapping) — token substitution for loop.json harness.command strings. Recognized tokens: {prompt}, {cwd}, {output}, {artifact}, {verdict}, {current_task}, {current_phase}.
  • _invoke_harness(harness_cfg, role, prompt_path, cwd, extras) — builds the harness command, substitutes tokens, runs subprocess.run, returns stdout. Default command when harness.command is missing is ["opencode", "run", "--prompt-file", "{prompt}", "--cwd", "{cwd}"].
  • parse_verdict(text) — strict graded-JSON parser. Accepts raw JSON, fenced ```json blocks, lines with leading // or # comments stripped. Returns None when missing pass key or total garbage. Otherwise returns {"pass": bool, "score": float, "reasons": list, "next_hint": str?}.
  • _gate(...), _context_floor_ok(), _role_prompt(...), _score_window(...), _loop_max_iterations(...), _outputs_dir(...), _make_completed (test helper used inline).
  • cmd_tick(args) — the tick flow per technical.md §7. Returns a summary dict, never raises (clean-exit on every path).
  • cmd_daemon(args) — time.sleep(interval) loop bounded by --max-iterations. KeyboardInterrupt stops cleanly with a DAEMON_STOPPED log entry.
  • main() — argparse with --mode {tick,daemon}, --loop, --project, --interval, --max-iterations, --json.

R-by-R coverage

Req Code
R1 entrypoint main() argparse, --mode required choices; cmd_tick returns summary with skipped:True and reason:"untracked" for missing .state.loop
R2 tick flow cmd_tick 7-route: load → gate → find_work → cwd → ctx-floor → spawn Implement → spawn Verify → parse verdict → cap score → spawn Orchestrate → atomic write state → tick log
R3 daemon cmd_daemon
R4 harness substitution _substitute, _invoke_harness
R5 context-floor guard _context_floor_ok called before any harness subprocess; halts human_intervention on loop_mode_eligible=False
R6 idempotence state writes only in step 10 (after parse_verdict succeeds and orchestrator ran); pre-step-10 crashes leave .state.loop untouched
R7 tests tests/test_loop_runner.py (18 tests)
R8 out-of-scope none — deferred to tasks 4–7 (audit work_source, backlog, worktree creation, the prompts themselves)

Tests (tests/test_loop_runner.py)

18 tests across 7 classes; all subprocess.run and _run_json calls stubbed via monkeypatch so no live LLM calls hit in CI.

  • TestEntrypoint (2): unknown-loop exits 0; unknown-mode exits 2.
  • TestTickFlow (5): tick-pass advances iteration_count; skip-when-halted; skip-when-untracked; skip-no-current-task; skip-when-gate-subprocess-fails.
  • TestContextFloor (1): refuses below floor; halts human_intervention; implement harness never invoked.
  • TestVerifierParseFailure (5): parse-failure halts and does not advance iteration_count (idempotence); fenced JSON parses; JSON with line comments parses; missing pass key → None; empty text → None.
  • TestScoreHistory (1): 5 ticks with window=3 → final score_history length is 3 and equals [0.4, 0.4, 0.4].
  • TestHarnessSubstitution (1): custom harness.command with --prompt/--cwd/--out/--artifact tokens; verify-role invocation sees the implement role's output path as --artifact <...-implement.json>.
  • TestDaemonMode (1): --max-iterations 3 runs 3 ticks then exits 0; time.sleep no-op via monkeypatch.
  • TestOrchestratorOrdering (1): implement → verify → orchestrate order observed via tagged handlers.
  • TestJsonOutput (1): --json prints structured tick summary as last line; parsed via lr.main() + capsys (since subprocess.run is patched).

Verification

python3 -m py_compile scripts/loop-runner.py
python3 -m pytest tests/test_loop_runner.py -q   # 18 passed
python3 -m pytest tests/ -q                       # 328 passed (was 310 + 18 new)