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

4.1 KiB
Raw Permalink Blame History

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

  1. Subprocess failure in --check-gate — _run_json returns None, cmd_tick skips with gate_subprocess_failed. No crash. ✅
  2. 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). ✅
  3. Empty verifier stdout — parse_verdict returns None; cmd_tick halts verifier_failed without advancing state. ✅
  4. Fenced JSON verdict — handled by _FENCE_RE regex, tries fenced body before raw text. ✅
  5. Line-commented JSON verdict — stripped by _strip_comments. ✅
  6. Missing pass key — parse_verdict requires it; returns None. ✅
  7. Score history shorter than window — no capping until length > window; oldest dropped. ✅
  8. No roles configured in loop.json — _role_prompt returns None or ""; harness gets empty prompt-path token. User's config responsibility; runtime refuses on empty cwd (Path resolve) if _find_project_dir fails. ✅
  9. Worktree declared but missing — runner uses project_root as cwd and logs nothing (per R8 deferred to task 5). ✅
  10. KeyboardInterrupt mid-tick — bubbles up; no state write happens; next tick starts fresh. ✅
  11. KeyboardInterrupt in daemon mode — _append_tick_log(DAEMON_STOPPED) then exit 0. ✅

Code-quality observations

  1. _run_json parses 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-gate and vram_detect --loop-mode. Safe.
  2. Token substitution is string-only — {verdict} gets json.dumps(verdict). Not shell-escaped. The harness command is parsed with shlex by opencode's own runner; subprocess.run with list argv means no shell injection. Safe as long as harness.command stays list-typed (it does — the cfg loader rejects non-list commands via the if not command: command = [...default...] fallback). ✅
  3. 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.
  4. _invoke_harness passes cwd=cwd to subprocess.run — if cwd doesn't exist, subprocess.run raises FileNotFoundError. Caught by the outer except (OSError, subprocess.SubprocessError) which emits stderr and returns empty — fine. ✅
  5. _read_state_loop swallows JSONDecodeError — returns None. Caller treats as untracked. A corrupt .state.loop becomes an untracked loop. Acceptable for v1; --audit flags untracked. ✅
  6. _write_state_loop uses tmp.replace(f) atomic write — same pattern as status.py; crash-safe. ✅

Verdict

APPROVE. Ready for bug_find.