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