Repository navigation
Conversation
Plumb `kind` (lint|format) through plan/classify/optimize/simulate/verify so format candidates flow through the same budgeted DP path as lint rules. Stages (check, pre-commit, post-edit) now route through cmd_fix_optimize via a shared `stages/_budgeted.py` helper; the default `dynamic` budget keeps mutations review-sized while `--renovate` / `--mutation-budget=` opt into broader churn. - author_edit_cost counts deleted-file lines toward review surface but filters deleted files from Ruff mutation inputs - doctor surfaces the dynamic-budget default; new howto under docs/ - drop dead conflict-range loop in optimize.conflicts (stays conservative) - drop unreachable _empty_success stub in verify
In a non-uv project with no in-tree .venv, `interlocks ci` previously ran coverage, typecheck, and test against the installer's interpreter, yielding installer-dependent verdicts instead of real findings. Gate these gates (plus the CRAP/mutation post-coverage steps) on `project_env_ready`, emit a warn-skip advisory, and stay exit-0. Removes the redundant stage guard in check.py and registers the `ci-no-venv-skip` behavior.
Declare every CLI flag a task reads as a FlagSpec on its CommandDoc so the dispatcher can reject any undeclared -* token (exit 1) and surface it by name, closing a silent-typo gap where bad flags were ignored. Per-task help now renders the declared flags, a source-introspection drift guard keeps the registry honest, and deprecated --quiet exits 1 (was 2) for consistency with other usage errors.
Add ui.is_json()/print_json() and gate every chrome primitive on is_json() so --json dominates --verbose. Gate runner.section/ok and print_stage_verdict. Declare --json as a boolean FlagSpec on ci, check, evaluate, trust, and doctor. The flag-drift guard excludes --json since it is read centrally via ui.is_json() rather than from a task module. No command emits JSON yet — this is the foundation for the per-command JSON branches.
Replace the (label, passed) tuple accumulator with a frozen GateResult dataclass carrying name/label/status/elapsed/detail so ci and check can later build a structured gates array. record_result takes the richer signature; _print_status derives a best-effort failure detail from the failed command line. print_stage_verdict's human verdict line is byte-identical to before.
Wire the first JSON-emitting commands. ci and check share one stage-JSON collector (runner.stage_json) over the GateResult accumulator, plus a structured skip accumulator (record_skip) parallel to _RESULTS. Suppress the human failure dump under --json so stdout stays exactly one object; failure detail is carried structurally in each gate's `detail` field. Register behavior IDs cli-json-ci and cli-json-check.
Add the JSON branch to the two advisory scored commands. evaluate emits
{command,score,verdict,checks}; trust emits {command,score,verdict,
coverage_pct,crap_offenders,suspicious_tests}. Both serialise their report
dataclass and return before any ui.* call; exit code stays 0. Error paths
(evaluate unreadable pyproject, trust missing .coverage) emit a minimal
{command,error} object so stdout is always parseable. Register behavior
IDs cli-json-evaluate and cli-json-trust.
Adds the --json branch to the two static-report commands. doctor emits
{command, status, blockers, warnings, detected, setup_checklist} and
mirrors its exit-1-on-failures quirk; config emits {command, preset,
pyproject_path, keys} with a dedicated _json_value resolver that coerces
Path/frozenset/tuple to JSON scalars rather than human-display strings.
config consumes --json as a declared output flag so it no longer trips
the positional-arg validation; config show now reads mode via
ui.is_json(). Registers behavior IDs cli-json-doctor/cli-json-config.
Documents the six-command --json surface in README.md and AGENTS.md, and unifies config's --json FlagSpec description with the other five. Extracts the doctor render arms into _render_doctor_json / _render_doctor_verbose over a shared _DoctorReport value so cmd_doctor stays under the complexity gate after gaining the JSON branch.
`name` and `label` always carried the identical row label, so the separate `name` field on `GateResult` and the duplicate argument to `record_result` were dead weight. Collapse to a single `label`; the JSON `name` key still ships, mirroring `label`, to keep the schema contract stable.
Three gates printed raw text on stdout regardless of output mode, corrupting `--json` output so `json.loads(stdout)` failed: - `check` emitted `scope=`/`changed vs` headers gated only on `is_verbose()` - `behavior_attribution` printed the attribution failure summary unconditionally - `crap` printed `CRAP=` offender rows and the `… N more` tail unconditionally Gate each on `not ui.is_json()` so `--json` stdout stays a single parseable object. Add regression tests for the CRAP-offender and verbose-changed-scope paths.
`interlocks setup` previously installed a git pre-commit hook silently even when run outside a git repo, producing a phantom hook that never fired. It now refuses non-git directories with a `git init` hint before touching any artifact, and the default-mode status block prints one row per artifact (via `ui.row(force=True)`) so a successful install is no longer silent under minimal-default output.
Default-mode doctor now names blockers and gaps inline (gaps capped at 3, with an overflow bullet pointing at --verbose) instead of only reporting a `ready (N gap)` count. Add a `--strict` flag that exits 2 when the verdict is blocked, leaving plain doctor advisory at exit 0. Delete the false "tool not found on PATH" warnings for uvx-dispatched tools — those tools are never expected on PATH since 0.2.0 ships zero runtime deps.
interlocks check in default mode was near-silent — a clean run printed only the final verdict even though it ran 6+ gates, so a slow strict run looked frozen. - Add ui.gate_row: a thin wrapper over row(..., force=True) that always shows in default mode while is_json() still dominates. - Route runner._print_status and the budgeted-mutation legacy fix/format rows through gate_row, so check prints one row per running gate. - Drop the fix-optimize summary leak: empty plan is silent in default mode; a populated plan emits one [fix-optimize] plan written -> ... gate row. The rich SELECTED / NOT SELECTED / plan kv_blocks are now explicitly verbose-gated (ui.kv_block is ungated). - Register the check-default-gate-rows behavior and cover it with a default-mode acceptance scenario plus a negative stage @then step. The test harness opt-out token is now the harness-only --default-mode sentinel (--quiet was removed from the CLI). --json output is byte-for-byte unchanged; --verbose output is unchanged.
Behavior-identical comment cleanup from the code-review step of the readable-check-output work. The comment block now sits next to the `ui.kv_block` verbose guard it actually explains, instead of straddling the `gate_row` call above it. No code paths change.
Add an always-print `ui.group_header` helper and route both `cmd_help` branches through it, so `Start here:` / `Common gates:` / `Project:` labels render at default verbosity instead of being suppressed by the verbose-gated `ui.section`.
- help: emit a one-line `Detected:` summary (preset/src/tests/runner) at default verbosity; `(no pyproject.toml)` when the on-disk file is absent. The full verbose Detected/Thresholds/Crash-Reports block is unchanged below the verbose early-return. - explain: no-arg now prints a grouped one-row-per-command index via `_explain_index`; the full per-command prose dump moves behind a new `--all` flag, declared as a FlagSpec so `unknown_task_flags` accepts it. - Register behavior IDs cli-help-groups-default, cli-help-detected-summary, cli-explain-default-is-index; repoint the cli-explain-all scenario to `explain --all` and add default-mode help + explain-index scenarios.
Code-review simplification pass on the first-touch help work: - explain.py: factor the duplicated TASK_GROUPS walk in `_explain_index` and `_explain_all` into a shared `_command_docs_by_group` iterator, collapsing the repeated lazy-import and drift-guard handling - ui.py: tighten the `group_header` docstring - cli.py: trim the `_detected_summary_line` docstring to its essential one-line description Pre-commit test gate skipped: the lone failure (test_interlocks_check_blocks_on_lint_failures) is a pre-existing environment issue — bare `python` is absent from PATH — and reproduces on the unmodified tree at 0ab3d6d, unrelated to these edits.
…itch footer Move the config "Resolved values" section behind the verbose gate so the default `interlocks config` output stays focused on Status and Config keys. Add a "Switch with:" footer to default-mode `interlocks presets` output and extract `_presets_set_invocation()` as the single source for the canonical `presets set <baseline|strict|legacy>` phrase, reused by usage strings. Cover both behaviors with `--default-mode` feature scenarios, extended test_cli assertions, and new behavior IDs (cli-config-single-presenter, cli-presets-default-footer). Pre-existing environmental test failure (greenfield test invokes a bare `python` binary absent from PATH) is unrelated to this change.
There was a problem hiding this comment.
Code Review
This pull request introduces machine-readable JSON output for core commands, a new dynamic mutation budget based on author edit cost, and stricter CLI flag validation. It also includes significant documentation updates and refactors stage logic to use a shared budgeted mutation flow. Feedback focuses on ensuring skip policies are correctly enforced in the new budgeted logic, preventing duplicate CLI output in pre-commit, and ensuring all skipped gates are properly recorded in the JSON output for machine-readability parity. Additionally, it is suggested to make the uv presence check unconditional in doctor as it is now a core requirement for tool dispatch.
| if cfg is not None and cfg.test_invoker == "uv" and shutil.which("uv") is None: | ||
| blockers.append("test_invoker is `uv`, but `uv` was not found on PATH") |
There was a problem hiding this comment.
Since interlocks now primarily dispatches tools via uvx (as stated in the PR description and seen in simulate.py), the presence of uv on the PATH is a hard requirement for the entire tool to function, not just when test_invoker is set to uv. This check should be made unconditional to provide accurate diagnostic feedback to the user.
| if cfg is not None and cfg.test_invoker == "uv" and shutil.which("uv") is None: | |
| blockers.append("test_invoker is `uv`, but `uv` was not found on PATH") | |
| if shutil.which("uv") is None: | |
| blockers.append("`uv` not found on PATH; interlocks requires `uv` for tool dispatch") |
| warn_skip(project_env_skip_message("coverage")) | ||
| return None |
There was a problem hiding this comment.
When a task skips itself due to a missing project environment, it should be recorded in the stage's skip accumulator so it appears in the skipped array of the --json output. Currently, warn_skip only handles human-readable output and does not populate the machine-readable skip list.
| warn_skip(project_env_skip_message("coverage")) | |
| return None | |
| if not project_env_ready(cfg): | |
| from interlocks.runner import record_skip | |
| record_skip("coverage", "no project environment") | |
| warn_skip(project_env_skip_message("coverage")) | |
| return None |
| if not no_env and _should_run_mutation(cfg.mutation_ci_mode, run_in_ci=cfg.run_mutation_in_ci): | ||
| _run_post_coverage_gate( | ||
| "mutation", | ||
| lambda: cmd_mutation(changed_only=cfg.mutation_ci_mode == "incremental"), |
There was a problem hiding this comment.
The mutation gate is silently skipped when no_env is true, which means it won't appear in the gates or skipped arrays in the JSON output. For machine-readability parity with the human-readable output (which shows warnings for other skipped gates), this should use warn_skipped or record_skip to ensure the skip is documented in the JSON payload.
Summary
A series of CLI friction-polishing changes (the
cli-friction-polishtask line, tasks 01–08, plus the preceding--jsonwork). The theme throughout: makeinterlocksoutput honest, readable, and machine-consumable, and make the flag surface a strict contract.What's included
JSON output mode
--jsonflag contract + JSON render surface; per-stage renderers forcheck/ci,evaluate/trust,doctor/config.GateResult; redundantGateResult.namedropped.--jsonmode; self-dogfood sweep.Flag contract
-*token not declared as aFlagSpecon the task'sCommandDoc.Honest setup & doctor
setuprefuses non-git directories and shows an honest per-artifact summary.doctoremits truthful default output and a--strictexit code.Readable default-mode output
checkrenders readable gate-row output in default mode.helpshows visible group headers;help/explaingain a first-touch Detected summary and an explain index.configdefault mode shows a single sectioned presenter (flat resolved block moved to verbose);presetsdefault mode gains aSwitch with:footer.Supporting
cigates coverage/typecheck/test onproject_env_ready.Testing
interlocks checkandinterlocks acceptancegreen.behavior_coverage.py.🤖 Generated with Claude Code