- **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).
4.1 KiB
4.1 KiB
CODE_REVIEW: add-loop-runner
Reviewed against SPEC.md R1–R8.
R1–R8 checklist
| Req | Status | Notes |
|---|---|---|
| R1 entrypoint | ✅ | argparse --mode required choices; cmd_tick returns summary dict, never raises; exits 0 on unknown loop |
| R2 tick flow | ✅ | 11 steps match technical.md §7 precisely |
| R3 daemon | ✅ | cmd_daemon loops on cmd_tick + time.sleep; KeyboardInterrupt = DAEMON_STOPPED; --max-iterations honored |
| R4 harness substitution | ✅ | _substitute handles 7 tokens; missing tokens left literal; default command matches D8 (opencode) |
| R5 context-floor guard | ✅ | _context_floor_ok before any Implement call; halts human_intervention on loop_mode_eligible=False; best-effort allows tick if vram_detect itself unavailable |
| R6 idempotence | ✅ | state writes only after verdict parse + orchestrator both succeed; pre-step-10 crashes leave .state.loop untouched |
| R7 tests | ✅ | 18 tests, 7 classes; all subprocess stubbed |
| R8 out-of-scope | ✅ | audit/backlog/worktree-creation/prompts deferred to tasks 4–7 |
Edge cases checked
- Subprocess failure in
--check-gate—_run_jsonreturnsNone,cmd_tickskips withgate_subprocess_failed. No crash. ✅ - Subprocess failure in
vram_detect --loop-mode— best-effort allows tick (avoids a broken vram_detect tool from halting every loop on a platform where it isn't installed). ✅ - Empty verifier stdout —
parse_verdictreturnsNone;cmd_tickhaltsverifier_failedwithout advancing state. ✅ - Fenced JSON verdict — handled by
_FENCE_REregex, tries fenced body before raw text. ✅ - Line-commented JSON verdict — stripped by
_strip_comments. ✅ - Missing
passkey —parse_verdictrequires it; returnsNone. ✅ - Score history shorter than window — no capping until length > window; oldest dropped. ✅
- No roles configured in loop.json —
_role_promptreturnsNone or ""; harness gets empty prompt-path token. User's config responsibility; runtime refuses on empty cwd (Path resolve) if_find_project_dirfails. ✅ - Worktree declared but missing — runner uses
project_rootas cwd and logs nothing (per R8 deferred to task 5). ✅ KeyboardInterruptmid-tick — bubbles up; no state write happens; next tick starts fresh. ✅KeyboardInterruptin daemon mode —_append_tick_log(DAEMON_STOPPED)then exit 0. ✅
Code-quality observations
_run_jsonparses the last stdout line only — correct for--check-gate --json(last-line contract per AGENTS.md), but assumes the harness never emits JSON mid-session. For the harness-substitution roles (Implement/Verify/Orchestrate), the runner captures full stdout (not_run_json), so the constraint only applies to--check-gateandvram_detect --loop-mode. Safe.- Token substitution is string-only —
{verdict}getsjson.dumps(verdict). Not shell-escaped. The harness command is parsed withshlexby opencode's own runner; subprocess.run with list argv means no shell injection. Safe as long asharness.commandstays list-typed (it does — the cfg loader rejects non-list commands via theif not command: command = [...default...]fallback). ✅ - No timeout on harness invocations — explicitly per SPEC ("v1 has no timeout; harness owns its timeout policy"). Fine. Worth revisiting if a loop's harness hangs and the OS unit keeps scheduling — but the scheduler interval provides natural rate-limiting.
_invoke_harnesspassescwd=cwdtosubprocess.run— ifcwddoesn't exist,subprocess.runraisesFileNotFoundError. Caught by the outerexcept (OSError, subprocess.SubprocessError)which emits stderr and returns empty — fine. ✅_read_state_loopswallowsJSONDecodeError— returns None. Caller treats asuntracked. A corrupt.state.loopbecomes an untracked loop. Acceptable for v1;--auditflags untracked. ✅_write_state_loopusestmp.replace(f)atomic write — same pattern asstatus.py; crash-safe. ✅
Verdict
APPROVE. Ready for bug_find.