# 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_loop` - `cmd_create_loop`, `cmd_install_schedule`, `cmd_pause_loop`, `cmd_resume_loop` - `cmd_approve_loop` (R3 halt-clear) - `cmd_check_gate`, `cmd_can_continue` - `cmd_loop_list`, `cmd_version` - `cmd_can_edit_loop` (R7 worktree scope) ## Existing functions extended - `cmd_can_edit` — early hook: if `args.loop`, delegate to `cmd_can_edit_loop`. - `cmd_transition` — R8 halt-refusal inserted after `_require_state`; `_loop_owning_task` scan. - `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 presence - `TestCreateLoop` (R2) — kebab/dup/template rejection + name-patching - `TestVersionAndApprove` (R3) — version regex; approve refuses non-halted; clears halted + bumps `resumed_count` - `TestCanContinue` (R4) — running ok, halted denied, unknown → exit 2 - `TestCheckGate` (R5) — fresh-pass, status-halt, iterations-exhausted, iterations-remaining, budget-exhausted, budget-informational, task-phase-halt, score-plateau, short-history-ok, JSON output - `TestInstallSchedule` (R6) — stub generation, default interval from config, unknown-loop rejection - `TestCanEditLoop` (R7) — in-scope allowed, out-of-scope denied, outside-root denied, no-file rejected - `TestTransitionHaltRefusal` (R8) — refused when halted owner, allowed when running owner, allowed when no owner - `TestAuditAndList` (R9) — empty list, populated list, Cat-6 header on empty, halted flag, untracked flag, running-pass - `TestTickLog` (R10) — PAUSED/RESUMED/APPROVED/HALT all logged - `TestPauseResume` — 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.