Commit Graph

12 Commits

Author SHA1 Message Date
drew 4d969eaf2b feat(controller): Phase 3 — Gate 3 reviewer-abandon
When the reviewer finishes a review and judges the work fundamentally
unworkable (implementation surfaced misdiagnosis, obsoleted-by-other-work,
or irreducible complexity), it can now emit
verdict='abstain' + suggested_next_action='abandon' with a Gate-3
abandon_reason_category. The controller routes REVIEWING → ABANDONED
and (when the kill switch is on) performs the Forgejo close via the
reviewer-abandon side-effect tick — no implementer/CI/merge cycles.

Wired with the same defense-in-depth pattern Phase 2 established:
MCP setter validation + outcomes mapper dispatch with confidence
gating + Pydantic atomicity validator + side-effect tick with
audit-trail attribution (cause=REVIEWER_ABANDON,
event_type='reviewer_abandon'). Default-off
CONTROLLER_GATE3_ABANDON_ENABLED kill switch so a fresh deploy is
audit-only until the operator explicitly enables Forgejo writes.

Bundled refactor: hoisted the 9 Gate-2 + 3 Gate-3-exclusive abandon
categories into tools/controller/contracts/abandon_categories.py
(triggered by Phase 3 per the plan's follow-up backlog). Both gates
now consume the shared frozensets; doc-contract tests grep each
agent prompt against the canonical list.

Adversarial review (2 rounds): caught + fixed MCP cross-check
ordering (atomicity FIRST so missing-setter shows actionable error),
confidence=None symmetric downgrade across both gates, dead
blocking-issues extraction in _run_close, idempotency clock-collision
in the test, low-vs-missing reason-string conflation, and several
test-quality gaps. 4064/4071 tests passing (7 pre-existing failures
unrelated to Phase 3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:27:40 -04:00
drew a91df787d7 feat(controller): Phase 2 — Gate 2 estimator-abandon
Adds the second of three abandon gates: the estimator (Gate 2) can
mark a work item fundamentally unworkable, transitioning the
workflow ANALYZING -> ABANDONED and triggering a Forgejo close via
Phase 1's decomposed close_act orchestrator. Catches abandon cases
at the cheapest LLM stage, before implementer/reviewer tiers fire.

Substantive:
- EstimatorOutputV1: additive verdict + abandon_reason_category +
  abandon_reason_detail fields (pre-Phase-2 outputs still parse).
  @model_validator enforces abandon-requires-category atomicity at
  parse time — third defense layer beyond MCP setter + outcomes
  mapper
- state_machine: estimator_abandon event + (ANALYZING,
  estimator_abandon) -> ABANDONED. 57 transitions; invariants clean
- mcp/estimator_builder: estimator_set_verdict setter validates
  verdict enum + 9-category whitelist (scope_intractable,
  intent_wrong, security_regression, deprecated_dependency,
  breaks_protected_invariants, out_of_scope, low_value,
  unmaintained_path, policy_violation) + cross-field rules
- outcomes._map_estimator_outcome: dispatch verdict='abandon'
  -> estimator_abandon, with confidence-low downgrade to
  estimator_done (honors the agent prompt's documented "high or
  medium" requirement)
- estimator_abandon_side_effects.py: per-state side-effect tick
  modeled on grooming_side_effects.py; invokes close_act with
  cause=Cause.ESTIMATOR_ABANDON + event_type='estimator_abandon'
- _events.py: shared latest_transition_event +
  workflows_with_latest_transition_in helpers; dialect-aware
  payload['event'] extraction (SQLite json_extract +
  PostgreSQL ->>); centralizes the event_type='transition' +
  payload['event'] convention that side-effect ticks consume
- gate2_abandon_config.py: CONTROLLER_GATE2_ABANDON_ENABLED kill
  switch (default false). Fresh Phase 2 deploys are audit-only
  until operator explicitly enables; dry_run shared with grooming
  for unified safe-rollout staging
- .opencode/agents/estimator-implementation.md: GATE 2 ABANDON
  section with 9-category criteria + low_value disqualifier ("PR
  cites an issue/ticket -> route to reviewer instead")

Round-2 adversarial-review fixes (all required pre-commit):
- forgejo_writes.close_issue / close_act: NEW cause + event_type
  kwargs (defaults preserve Phase 1 grooming behavior; Phase 2
  callsite overrides). Fixes audit-trail attribution: telemetry
  queries SELECT WHERE cause='estimator_abandon' now return the
  right rows. Phase 1 regression test pins the grooming defaults
- tick.py operator_unstick lookback: dialect-aware json_extract
  fix (Phase 1 carry-over bug; would silently no-op on PostgreSQL)
- grooming_side_effects.py: idempotency filter now keys on
  check_name set (grooming check_names only) so a Phase 1 close
  and a Phase 2 close on the same workflow don't cross-cancel

Tests (+50): TestEstimatorOutputV1Phase2,
TestEstimatorAbandonStateMachine, TestMapEstimatorOutcomePhase2
(including confidence-low downgrade), TestEstimatorSetVerdict
(all 9 categories + cross-field rules), TestEventsHelper,
TestEstimatorAbandonSideEffectTick (including
test_close_writes_estimator_abandon_cause_and_event_type pinning
the audit-trail attribution, and Phase 1 regression guard).
Doc-contract test asserts all 9 categories appear in the agent
prompt. 1509/1509 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:44:09 -04:00
drew 016b348117 feat(controller): grooming gate (Phase 0 + Phase 1 worker-shape dispatch)
Phase 0 (foundation):
- Cause enum (controller_events.cause) for action attribution
- Schema: grooming_decisions audit table; workflows gains
  grooming_evaluated_at + deferred_reason + deferred_at +
  deferred_target_workflow_id; pulls gains touched_files
- audit_comments: CLOSE / DEFER templates + render_comment_template
- forgejo_writes: close_issue + defer_issue 5-step crash-safe protocol
  (fingerprint dedup, error matrix, dry-run)
- patch_pr_state callback in forgejo_http
- grooming_config: 22-env-var frozen-dataclass config + log_effective
- pulls.touched_files cache extension (_pipeline_cache.py schema v8)
- reaper.reap_grooming_decisions audit-retention sweep
- reconciliation RESUME guard (deferred_reason)

Phase 1 (worker-queue shape, 2026-05-25):
- New state: GROOMING. New events: grooming_started, groom_verdict_
  {proceed,defer,close}. 5 new transitions; all invariants still clean
- GroomingInputV1 + GroomingOutputV1 Pydantic contracts
- outcomes._map_grooming_outcome routes verdicts to state-machine events
- prefetch.build_grooming_stage_b_input + list_open_prs callback
- scheduler GROOMING -> grooming_stage_b role
- promote: cfg-gated DISCOVERED -> GROOMING when CONTROLLER_GROOMING_
  ENABLED=true; issues skip grooming
- forgejo_writes decomposed: close_act/defer_act (Forgejo writes only;
  state-machine already transitioned) + close_decide_and_act/
  defer_decide_and_act (Phase 0 callers); _apply_workflow_transition
  is underscore-private
- grooming.py library: tokenization, suspicion scoring (Jaccard +
  weighted overlap), deterministic checks, action -> verdict mapping
- mcp/grooming_builder.py: 14-tool FastMCP server emits GroomingOutputV1
- .opencode/agents/grooming-stage-b.md: duplicate-detection agent
  prompt (claude-haiku-4-5)
- grooming_side_effects.run_grooming_side_effects_tick: per-state tick
  performs Forgejo writes after groom_verdict_{defer,close} fires.
  Filters on event_type='transition' + payload.event (centralizes the
  convention pending Phase 2's latest_transition_event helper)
- GroomingCallbacks frozen dataclass; loop.py + __main__.py wired

Worker role registry (single source of truth):
- worker/roles.py: WORKER_ROLES + WorkerRoleSpec + default_roles_csv
  + output_filename_for. agent_runner.ROLE_TO_MCP_MODULE / ROLE_TO_
  OUTPUT_MODEL derive from it; opencode_session.agent_name_for reads
  it for flat cases; all 6 prompt builders use output_filename_for;
  worker --roles default = default_roles_csv(); launcher script
  derives --roles via shell substitution. Cross-site invariant test
  enforces alignment across 5 sites + opencode.json MCP registry.

Phase 0 silent-bug fix:
- reconciliation.py RESUME guard SELECT now includes deferred_reason
  (was missing since Phase 0; guard was a silent no-op). Tightened
  from getattr to attribute access to fail fast on future omissions.

Tests (1456 total, +91 grooming-specific):
- test_grooming_phase0.py: 34 tests (orchestrator matrix, crash
  recovery, idempotency, dry-run)
- test_grooming_phase1.py: 60 tests (library, contracts, state
  machine, outcomes, scheduler, promote, prefetch, act-variants
  with signature parity, side-effect tick incl. natural-idempotency
  + executed-flag-skip + verdict-mismatch + reconciliation RESUME)
- test_mcp_builders.py TestGroomingBuilder: 29 tests (happy paths
  + 22 validation rules + Pydantic round-trip + master-tick-read-
  path companion)
- test_worker_agent_runner.py TestRoleMaps: cross-role wiring
  alignment + agent-prompt-vs-worker-fallback filename contract +
  inspect.signature equality (close_act/defer_act vs
  close_issue/defer_issue)
- test_state_machine.py: transition count 51 -> 56 +
  events_from_grooming

Live-validated end-to-end on 4 staged sentinel PRs (#55-#58) in
dry_run: agent emits verdicts via MCP, state-machine transitions
fire, side-effect tick writes audit row, deferred_reason gates
reconciliation RESUME correctly.

Deferred refinements + Phase 2 prerequisite (latest_transition_event
helper) tracked in .drew/regressions-plan.md "Phase 1 follow-up
backlog".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:05:29 -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
drew 615a05b982 feat(controller): rebase-default conflict resolution with merge fallback
PR branches 177-180 commits ahead of base cannot be rebased
commit-by-commit by a single-shot resolver agent (too many conflict
stops for one session). Conflict-prep now defaults to rebase (linear
history) and falls back to a single 3-way merge when the branch is too
divergent (commit count over CONTROLLER_CONFLICT_REBASE_MAX_COMMITS,
default 60). The merge pipeline derives the track from branch shape via
a Do:rebase -> Do:merge ladder in _make_merge_pr — no stored flag.

Adds a git_rebase_continue MCP tool plus status rebase/merge-in-progress
fields so the conflict-resolver agent is fully MCP-driven and dual-mode
(mid-rebase or mid-merge). Also routes a green-CI implementer noop
straight to REVIEWING instead of a deadlock-prone AWAITING_CI round
trip. No state-machine change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:17:16 -04:00
drew f76568a871 feat(controller): ci-infra-failure implementer outcome → bounded rerun
The implementer-side counterpart to the `indeterminate` verdict. When
the implementer IS dispatched onto a CI failure and finds the log
carries no verdict (a hard-kill / OOM — nothing in the diff to fix),
it can now emit `outcome=ci-infra-failure` instead of being forced to
`blocked` → STUCK.

`ci-infra-failure` forbids commits/files/blockers (no-work invariant,
like `noop`) and fires `implementer_ci_infra_failure`, routing
IMPLEMENTING → DISCOVERED so the CI-freshness gate reruns CI under its
bounded RERUN_BUDGET. Backstopped by `_MAX_CI_INFRA_FAILURE=4` so a
mis-classification cannot loop the gate forever.

Wired through: the V1 contract enum, the implementer MCP builder
(outcome value + no-work invariant), the state machine (event +
IMPLEMENTING→DISCOVERED transition), the outcome mapper (+ per-workflow
cap), tick's `_count_prior_ci_infra_failure`, and the implementer
prompt — which now surfaces the outcome whenever the CI summary shows
a failing gate, with guidance to use it ONLY when the log genuinely
shows no verdict.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 22:55:28 -04:00
drew 521117882e fix(controller): conflict resolver lands resolutions on the PR
Run-2 PR #40 exposed that the conflict-resolver stage never resolved
conflicts *on the PR*. The resolver agent owned the whole git flow
(rebase + resolve + push) but was never given the PR head branch name,
so it pushed the resolved branch to a guessed branch ("main"), the push
was lost, and it reported "resolved" anyway — the controller advanced
the workflow on a resolution that never reached the PR.

This splits the work along the controller's principle: deterministic
git mechanics in the worker, only the semantic merge in the agent.

- conflict_rebase.py (new): the worker starts the rebase
  (prepare_conflict_rebase — hands the agent a real mid-rebase
  worktree) and, after the agent's resolution, lands it with a
  *verified* push (finalize_conflict_resolution) — force-with-lease
  pinned to the PR head the resolver started from, then re-reads the
  remote to confirm it moved. A failed/rejected push is never reported
  as resolved. run_conflict_resolver_attempt orchestrates it.
- ConflictResolverInputV1 / prefetch: carry head_ref so the worker
  knows the PR branch (this also makes the worker's fetch_and_validate
  run for the conflict_resolver — it gated on head_sha AND head_ref).
- worker/__main__.py: route conflict_resolver attempts through the new
  orchestration.
- conflict-resolver-worker.md: the agent resolves + continues the
  rebase and never pushes — the controller lands it.
- mcp_git_server.py: add the controller worktree root to the git MCP's
  allowed bases (every git_* MCP call errored for this role before).

The committed-marker check is scoped to the PR's own diff — a
whole-tree grep false-positived on repo files that legitimately
document conflict markers. The worker's rebase/push are lost-lock
guarded so a reaped worker cannot race the shared worktree.

1136 controller tests pass (+11 new in test_conflict_rebase.py).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:27:19 -04:00
drew 0db0a15dad feat(controller): RUN_CI_LOCAL verdict source, ci-not-ready outcome, escalation hardening
Adds RUN_CI_LOCAL — an on-demand local-CI verdict source for when the
cluster's Forgejo CI is broken — plus robustness fixes, the telemetry
Live-tab rewrite, and PR-level cost attribution.

Controller:
- RUN_CI_LOCAL: the master swaps its Forgejo CI callbacks for local
  `forgejo-runner exec` runs (tools/run-ci-full-local.sh + local_ci.py).
  Async per-(owner,repo,SHA) on-disk job cache; preflights the
  forgejo-runner binary + Docker daemon at startup (fail loud, not a
  red verdict on every PR); GCs finished run dirs + per-run actcache.
- ci-not-ready implementer outcome + implementer_ci_not_ready event:
  an implementer that runs before the on-demand verdict exists parks
  in AWAITING_CI instead of dead-ending at STUCK; capped against
  ci_red ping-pong.
- ci_poll_exhaustion skips its sweep while a local CI run is in
  flight, so AWAITING_CI workflows queued behind on-demand CI are not
  STUCK'd by the remote-CI-sized timeout.
- Escalate the workflow after repeated worker-internal-error at a
  tier instead of retrying to pickup-exhaustion -> STUCK.
- forgejo_http: normalise Forgejo's per-gate `status` key to `state`
  so failing gates are actually counted (they previously all read as
  pending).
- Per-tier worker timeouts bumped +15 min; a timed-out attempt's
  dirty-worktree residue is preserved on auto-scratch/pr-<N> before
  the next attempt's reset.
- Implementer agents now verify only the CI-flagged gate(s) via a
  targeted re-run rather than the full local battery before claiming
  resolved/noop. Re-running the whole suite CI will run anyway was the
  #1 cause of implementer timeouts; CI remains the real gate and
  re-dispatches the implementer on red.

Telemetry:
- Live tab rebuilt on /api/live (controller DB run state + the live
  OpenCode session forest) after the live_log_writer sidecar was
  retired with the legacy dispatchers.
- Durable per-attempt input/output payloads surfaced in the Live
  drill-down, archived-session detail, and Workflows timeline.
- PR-level cost attribution: worker session tags carry -pr-<n>;
  backfill_llm_activity_pr.py repairs rows written before the fix.

Shared:
- tools/controller/session_tag.py — one canonical controller-tag
  parser shared by the telemetry server and the backfill.

Tests: new coverage for local_ci (state machine, log parsing,
_summarize_run, GC, in-flight probe, preflight), the ci-not-ready
path, the escalation/ci-not-ready SQL counters, the ci_poll
in-flight skip, and CI-status payload parsing across both sources.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:49:05 -04:00
drew 0bc734c020 style: ruff format the controller-state-machine branch (288 files)
Applies `ruff format` to the accumulated formatting debt on this branch.
Formatting-only — no behavioral changes. Required for CI/lint's format
gate (`nox -s format -- --check`), which the branch was failing on 288
tracked files that drifted from ruff's canonical style.

In-progress WIP files are intentionally excluded so this commit stays a
clean formatting-only diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:09:17 -04:00
drew a6986008ee feat(controller): trial-5 batch — dispute path, merge pipeline split, conflict-resolver hardening
T5-1   reviewer feedback rendered full-body to the implementer
T5-4/9 implementer dispute path — dispute-at-any-tier with per-tier cap,
       OPERATOR_ATTENTION state on stalemate, pr-review-worker-dispute agent
T5-5   reviewer BLOCKING ISSUE EVIDENCE RULE + 5-step validation
T5-7   merge step split into a singleton process — impl/review masters write
       APPROVED and stop; merge_drive owns APPROVED -> MERGING -> MERGED
T5-10  merge process is fully deterministic; base conflicts bounce to the
       controller's CONFLICT_RESOLVING (LLM); conflict_drive sidecar retired
T5-11  implementer fast success path — verified-clean outcome so a no-op
       after conflict resolution doesn't force busywork
T5-12  conflict-resolver permissions fixed across all paths (/tmp/** glob)
T5-13  conflict-resolver PR-intent prehydration (title/body/comments)

Adds tools/_controller_db_bridge.py so merge_drive reads the controller DB
directly (Option B), plus APPROVED + OPERATOR_ATTENTION states, the
dispute/verified-clean events, and the V1 contract fields backing them.
Reviewer model: baseline -> sonnet, dispute -> opus.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:54:07 -04:00
drew febb352618 fix(controller): pipeline run-blockers — promoter, scheduler, owner/repo, workspace_dir patch
Round-3 deep pass identified four issues that would have prevented an
actual end-to-end pipeline run:

RB1 — DISCOVERED → ANALYZING never fired in production:
The state machine defines (DISCOVERED, discovery_picked_up) →
ANALYZING but NO production code fires the event. Workflows
created by discovery would sit in DISCOVERED forever.

Fix:
- New ``master/promote.py``: ``run_promote_discovered_tick`` scans
  for DISCOVERED workflows + fires ``discovery_picked_up`` via
  apply_event (state-machine invariants stay enforced) + emits a
  ``discovery-promoted`` controller_events row per transition.
- Composes with the master loop's other ticks; runs every iteration
  (cheap — typically 0-1 row).

RB2 — scheduler.schedule_next_attempts never called from master loop:
The scheduler was exported by the master package but never invoked.
It creates the ``workflow_attempts`` rows that workers dequeue —
without it, workers would have nothing to pick up.

Fix:
- ``master/loop.py`` now accepts a ``prefetch: PrefetchCallback``
  kwarg. When provided, the loop runs promote_discovered + scheduler
  every iteration after tick/reaper/reconciliation.
- ``MasterTickReport`` gains ``promote_discovered`` and ``scheduler``
  optional fields so on_iteration callbacks see both.
- ``master/__main__.py`` builds a ``PrefetchDataCallbacks`` from the
  Forgejo callback bundle and constructs the production
  ``make_prefetch_callback(engine, callbacks)`` — wires through to
  the loop's new prefetch kwarg.

RB3 — owner / repo missing from V1 input contracts:
The implementer / reviewer / estimator / conflict-resolver V1 inputs
had pr_number but not owner/repo. The OpenCode agent would have
had no way to know which Forgejo repo to clone — it would have had
to derive owner/repo from process env, coupling the worker to a
single repo.

Fix:
- ``contracts/v1.py``: added ``owner: str`` and ``repo: str``
  (min_length=1) to ImplementerInputV1, ReviewerInputV1,
  EstimatorInputV1, ConflictResolverInputV1.
- ``master/prefetch.py``: builders populate owner/repo from the
  Workflow row (already known at prefetch time).
- Existing test fixtures in ``test_contracts_v1.py`` updated.

RB4 — input_payload.workspace_dir placeholder reached the agent:
Prefetch wrote ``workspace_dir = "<worker-injected>"`` as a
placeholder; the worker never patched it before invoking the
OpenCode session. The prompt builder rendered the literal
placeholder string into the agent's prompt — the agent had no idea
where to clone.

Fix:
- ``worker/agent_runner.py``: patches input_payload.workspace_dir
  with the real path immediately before calling run_opencode_session.
  Uses a shallow copy so the caller's dict isn't side-effected.
- ``worker/__main__.py``: workspace_dir naming convention is now
  ``pr-{owner}-{repo}-{pr_number}`` (matches workspace.py's
  PerPRWorkspace convention) so the janitor's pr-* glob + the
  agent's expected workspace location agree. Falls back to
  ``pr-attempt-{N}`` for legacy input_payloads missing owner/repo.

Tests:
- ``test_master_promote.py`` (NEW, +7 tests):
  - empty DB no-op
  - single workflow promoted
  - multiple promoted in one tick
  - only DISCOVERED targeted (non-DISCOVERED untouched)
  - controller_events row emitted with correct shape
  - idempotent after first promotion
  - LoopIntegration end-to-end: DISCOVERED → ANALYZING → pending
    estimator attempt visible in workflow_attempts (pins the entire
    previously-broken pipeline from discovery to enqueue)

Total: 703 controller tests pass (+7 net), 0 regressions.

Without these four fixes, the pipeline would have looked alive in
unit tests but produced zero work in a real deployment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 16:43:36 -04:00
drew a10758177c feat(controller): Phase 0 foundation — V1 contracts + CI gate enumeration
Lays the foundation for the planned controller + DB-owned PR state
machine rewrite (see .drew/controller_state_machine.md).

Phase 0a — Pydantic V1 worker I/O contracts (tools/controller/contracts/):
- v1.py: ImplementerInputV1/OutputV1, ReviewerInputV1/OutputV1,
  EstimatorOutputV1, IssueImplementerOutputV1,
  ConflictResolverInputV1/OutputV1, SummarizerInputV1/OutputV1.
  Plus CISummary / CIFailure / GateResult / FailedAssertion /
  FileLocation. All models use extra="forbid" + required
  output_version Literal discriminator (no defaults).
- parse.py: strict_parse() + strict_parse_with_retry() helpers.
  Corrective prompt template quotes the Pydantic ValidationError +
  the JSON schema. TTL guard skips retry when remaining time too
  short. Defense-in-depth around the response-builder MCPs that
  arrive in Phase 1a.

Phase 0b — Static CI parser-coverage discovery:
- NOX_SESSION_TO_PARSER map in tools/controller/ci_summary_parsers/.
  Derived from .forgejo/workflows/*.yml + noxfile.py. Covers ruff,
  pyright, behave, robot_framework, slipcover, vulture, radon,
  bandit, semgrep, build. Cron-only sessions (benchmark,
  benchmark_regression) are in NOX_SESSIONS_SKIP_COVERAGE.
- scripts/enumerate-ci-gates.py: reads the workflow YAML, joins
  against the map, exits non-zero on coverage gaps. Wired into
  tests so the actual .forgejo/ is regression-guarded.

63 new tests; full auto_agents suite still passes (2424).
2026-05-18 11:02:56 -04:00