- **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).
5.5 KiB
Implementation: add-status-brakes
Implements SPEC.md R1–R10. All new code lives in scripts/status.py (loop extensions) plus a new test file tests/test_status_brakes.py and a minimal loop template at templates/loops/ci-triage/loop.json.
Surface added (R1–R10)
| Req | CLI surface | Behavior |
|---|---|---|
| R1 | n/a | .state.loop schema v1 with 13 default fields; written atomically via tmp+rename |
| R2 | --create-loop NAME [--from-template T] |
Refuses non-kebab, duplicates, unknown template; patches name into copied loop.json; seeds empty .state.log |
| R3 | --version; --approve --loop NAME |
--version reads ## Framework Version from config.md; --approve --loop is the only way to clear a halt (D4); increments resumed_count |
| R4 | --can-continue NAME [--json] |
Cheap status probe: ok := status == "running" |
| R5 | --check-gate NAME [--json] |
Runs 6 gates in order; first failure halts the loop and emits structured verdict |
| R6 | --install-schedule NAME [--interval S] |
Generates run-tick.sh/.bat; installs launchd plist / crontab block / schtasks unit per platform.system(); --pause-loop best-effort disables the unit |
| R7 | --can-edit --loop NAME [--loop-worktree] --file P |
Checks file against loop's blast_radius.file_scope; refuses files outside project/framework root |
| R8 | --transition extension |
Refuses if a HALTED loop owns the task (_loop_owning_task scan); points user at --approve --loop |
| R9 | --audit Cat-6 block; --loop-list |
Reuses _audit_loops_block; runs even when no tasks exist |
| R10 | .state.log tick trail |
Every state-changing op appends an ISO-timestamped line; tests assert PAUSED/RESUMED/APPROVED/HALT are all logged |
Gate order (R5)
gate_loop_status -> not running -> halt w/ existing halt_reason
gate_iterations -> iteration_count >= max_iterations -> iterations_exhausted
gate_budget -> spent_usd >= max_budget_usd -> budget_exhausted (remote-only, informational)
gate_task_phase -> current_task in human_intervention -> human_intervention
gate_worktree_drift -> changed files outside file_scope -> drift_detected
gate_score_plateau -> score_history flat across window -> verifier_failed
First failure wins. Halt is written atomically; schedule is best-effort disabled.
Helper functions added (scripts/status.py, before def main())
LOOP_*constants (states, halts, schema version, file names)_loops_dir,_loop_dir,_all_loop_dirs_read_state_loop,_write_state_loop,_initial_state_loop,_read_loop_config_append_tick_log,_loop_untracked_hint_halt_loop,_disable_schedule,_enable_schedule_loop_owning_task(R8 ownership scan)_gate_*(6 gate functions)_loop_max_iterations_task_phase_for_loopcmd_create_loop,cmd_install_schedule,cmd_pause_loop,cmd_resume_loopcmd_approve_loop(R3 halt-clear)cmd_check_gate,cmd_can_continuecmd_loop_list,cmd_versioncmd_can_edit_loop(R7 worktree scope)
Existing functions extended
cmd_can_edit— early hook: ifargs.loop, delegate tocmd_can_edit_loop.cmd_transition— R8 halt-refusal inserted after_require_state;_loop_owning_taskscan.cmd_audit—_audit_loops_block(args)helper called twice (early-return empty-tasks path + main path); Cat-6 header always printed.
Argparse additions (main())
--create-loop, --from-template, --install-schedule, --interval, --pause-loop, --resume-loop, --loop, --loop-worktree, --check-gate, --can-continue, --loop-list, --version.
Dispatch order places loop commands before task commands so --approve --loop doesn't fall through to the --task-required cmd_approve.
New file: templates/loops/ci-triage/loop.json
Minimal template used as --create-loop default. Defines brakes.max_iterations=25, score_plateau_window=5, blast_radius.use_worktree=true. Full prompt/template expansion is task 6.
Tests
tests/test_status_brakes.py — 46 tests across 10 classes mirroring R1–R10:
TestStateLoopSchema(R1) — default-schema assertions + tick log file presenceTestCreateLoop(R2) — kebab/dup/template rejection + name-patchingTestVersionAndApprove(R3) — version regex; approve refuses non-halted; clears halted + bumpsresumed_countTestCanContinue(R4) — running ok, halted denied, unknown → exit 2TestCheckGate(R5) — fresh-pass, status-halt, iterations-exhausted, iterations-remaining, budget-exhausted, budget-informational, task-phase-halt, score-plateau, short-history-ok, JSON outputTestInstallSchedule(R6) — stub generation, default interval from config, unknown-loop rejectionTestCanEditLoop(R7) — in-scope allowed, out-of-scope denied, outside-root denied, no-file rejectedTestTransitionHaltRefusal(R8) — refused when halted owner, allowed when running owner, allowed when no ownerTestAuditAndList(R9) — empty list, populated list, Cat-6 header on empty, halted flag, untracked flag, running-passTestTickLog(R10) — PAUSED/RESUMED/APPROVED/HALT all loggedTestPauseResume— pause sets paused; resume only from paused; halted→approve pointer
Verification
python3 -m py_compile scripts/status.py # OK
python3 -m pytest tests/test_status_brakes.py -q # 46 passed
python3 -m pytest tests/ -q # 310 passed (was 264 + 46 new)
No existing tests changed. Full suite green.