CI / build (push) Has been cancelled
- 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
4.7 KiB
4.7 KiB
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 mirroringstatus.pyfor 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. ReturnsNoneon subprocess failure, non-zero exit, empty output, or JSON parse failure. Used by both_gateand_context_floor_ok._substitute(template, mapping)— token substitution forloop.jsonharness.commandstrings. 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, runssubprocess.run, returns stdout. Default command whenharness.commandis 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. ReturnsNonewhen missingpasskey 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.KeyboardInterruptstops cleanly with aDAEMON_STOPPEDlog 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; haltshuman_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; missingpasskey → None; empty text → None.TestScoreHistory(1): 5 ticks with window=3 → finalscore_historylength is 3 and equals[0.4, 0.4, 0.4].TestHarnessSubstitution(1): customharness.commandwith--prompt/--cwd/--out/--artifacttokens; verify-role invocation sees the implement role's output path as--artifact <...-implement.json>.TestDaemonMode(1):--max-iterations 3runs 3 ticks then exits 0;time.sleepno-op via monkeypatch.TestOrchestratorOrdering(1): implement → verify → orchestrate order observed via tagged handlers.TestJsonOutput(1):--jsonprints structured tick summary as last line; parsed vialr.main()+capsys(sincesubprocess.runis 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)