Files
cleveragents-core/tools/controller/master
drew 981ddd6a8e fix(controller): incident hardening — PR-44 dispute guard + PR-46 fixes
Bundles fixes for three production incidents (PR-44, PR-46 a/b/c).
Every change has an incident-reference comment in the code and a
named regression test. 199 tests pass on the impacted modules.

PR-44 — fabricated dispute escape
---------------------------------
A tier-0 implementer, bounced twice by red CI, emitted a fabricated
``dispute-reviewer`` outcome and shortcut a red PR into REVIEWING →
APPROVED → MERGING, bypassing the CI gate. ``dispute-reviewer`` is
the only IMPLEMENTING → REVIEWING edge that doesn't pass through
AWAITING_CI, so it must be defended.

tools/controller/master/outcomes.py: ``_map_implementer_outcome``
now guards ``dispute-reviewer`` with two preconditions —
(1) ``attempt_saw_green_ci`` (the HEAD must already be CI-verified;
a dispute can't jump the gate on a red/pending head), and
(2) ``prior_reviews >= 1`` (must reference a review that actually
happened, not a hallucinated one). Either guard fails →
``implementer_competence_failure`` → tier escalation. A weak model
can't game its way past CI; a stronger tier is given the real problem.

tools/controller/master/tick.py: new ``_count_prior_reviews`` helper
counts completed reviewer attempts (epoch-scoped so an
``operator_unstick`` resets the count). Wired into the
``map_outcome_to_event`` call.

PR-46(a) — stale gate-script preferred over in-repo
---------------------------------------------------
``gate.py`` was preferring the seeded ``/tmp/local_tools`` copy of
``local_ci_gate.sh`` over the version-matched in-repo copy. The
seed predated the ``--envdir`` flag; the controller pipeline's
invocations rejected as bad-argv every committing implementer's
gate. The seed-refresher (``dispatch_implementer.py``) is on the
retired dispatcher path, so the staleness was permanent.

tools/controller/worker/gate.py: resolution order is now
``CONTROLLER_LOCAL_CI_GATE`` env > in-repo > seeded ``/tmp``. The
seeded copy survives only as a last-resort fallback. Module-level
constants ``_IN_REPO_GATE_SCRIPT`` / ``_SEEDED_GATE_SCRIPT`` let
tests substitute paths.

PR-46(b) — 6-second-old run flagged zombie
------------------------------------------
``classify_ci_run`` instantly classified a CI run as ``stale`` when
the Actions API reported no active task. A freshly-pushed run has
no task simply because no runner has picked it up yet, and there
are brief gaps between jobs — both false positives. A 6-second-old
PR-46 run was bounced before CI could even start.

tools/controller/master/ci_run_status.py: new ``ZOMBIE_GRACE``
(default 3 min, env: ``CONTROLLER_CI_ZOMBIE_GRACE_MIN``). "No
active task" only classifies a run as stale once the run has ALSO
gone quiet past the grace. Much shorter than ``STALE_AFTER`` since
the active-task absence is corroborating evidence, not the sole
signal.

PR-46(c) — ruff-format-only violation slipping through lint
-----------------------------------------------------------
CI's ``lint`` job runs both the ``lint`` nox session (ruff check)
AND ``ruff format --check``. The pre-push local gate only ran the
former; a formatting-only violation passed pre-push then failed CI.

tools/local_ci_gate.sh: the ``lint`` gate now runs ``ruff check``
followed by ``ruff format --check``, unconditional. Either one
failing marks the gate red. ``ruff format --check`` is whole-repo
and takes no posargs. tests/auto_agents/test_local_ci_gate.py
updated for the new two-call shape.

Supporting changes
------------------
.forgejo/workflows/ci.yml: gates ``coverage`` and ``docker`` jobs
on repo variables ``skip_coverage`` / ``skip_docker`` so the long
reaper-prone jobs can be skipped per-fork without editing CI.
Guard step always runs so the job still reports ``success`` and
``status-check`` stays green.

tools/duplicate_prs_to_fork.py: bakes the same ``skip_coverage`` /
``skip_docker`` gates into every sentinel PR's ci.yml at PR-creation
time so fork-mode runs inherit the gating. Idempotent.

.opencode/opencode.json: adds ``timeout: 1860000`` (31 min) to the
``ci`` MCP server so long CI waits don't timeout the tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 08:32:35 -04:00
..