Commit Graph

2 Commits

Author SHA1 Message Date
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
drew a103a31bbf feat(controller): worker-owned gated push for the implementer
The implementer agent no longer pushes to git — the controller worker
now owns the push: it gates the agent's commits on lint+typecheck and
pushes via a single leased primitive. Closes two production defects:

- Clobber: the pre-fix MCP --force-with-lease leased against a
  freshly-fetched tip, so the lease always passed — an in-flight
  implementer destroyed a commit pushed to the PR branch during its
  run (lost a hand-pushed skip_coverage fix on PR #46).
- Gate-skip: the agent verified only the CI-flagged gate, so a fix for
  one gate shipped fresh violations in another (lint flapped
  pass->fail across CI runs 198->199).

Step 1 — worker_push primitive:
- New git_push.py: one leased push, pinned to the SHA the worker
  started from; classifies pushed / stale_input / diverged /
  infra_error; bounded infra-retry.
- mcp_git_server.push gains expected_sha for a correctly-pinned lease.
- finalize_conflict_resolution migrated onto worker_push.

Step 2 — deterministic gate:
- New gate.py: per-slot nox env-dirs (no venv races), manifest-hash
  staleness keying, lazy warm-up.
- local_ci_gate.sh gains --envdir.

Step 3 — worker-owned gated push:
- New implementer_finalize.py: divergence pre-check -> lint+typecheck
  gate -> ruff auto-fix -> leased push. finalize's outcome is
  authoritative over the agent's emitted outcome.
- agent_runner integrates finalize; salvage no longer pushes.
- outcomes/tick: gate-failed + push-time stale-input caps,
  epoch-scoped; WorkerError carries an output_payload so the gate
  report reaches the next attempt's prompt; prefetch surfaces
  gate-failed attempts.
- The 5 task-implementor prompts drop the agent push step.

Reviewed across 4 adversarial rounds; full controller+MCP suite green
(1379 passed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:38:52 -04:00