Commit Graph

34 Commits

Author SHA1 Message Date
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 e28b41f7ec fix(auto-agents): bump post-push CI verify per-call budget 90s → 180s
Observed healthy slow-runner days where 90 s was insufficient:
the verifier returned ``pending`` and let bad pushes through to
the next dispatcher cycle. 180 s catches the fast-failing checks
(lint/format/push-validation typically fail in 30-60 s) AND gives
headroom for a backlogged runner where those checks may queue for
a minute or two before they actually run.

- Per-call budget: 90 → 180 s (doubles polls-per-window from 9 to
  18; 10 s poll interval unchanged).
- Worst-case cycle math updated in the cycle-budget block comment:
  5 PRs × 180 s = 15 min (was 7.5 min) of polling per cycle in the
  uncapped case. The per-cycle budget (commit 2c43179e7) still
  caps total polling at IMPLEMENTER_POST_PUSH_CI_CYCLE_BUDGET_S
  (default 300 s), so the practical worst case is unchanged.
- Existing TestPostPushCIVerify docstrings update to reference the
  new number.

Escalation integration tests get an autouse fixture that disables
the verifier flag: ``test_implementer_escalation_integration.py``
scripts many ``outcome=resolved`` + head-advanced scenarios but
doesn't stub ``fetch_ci_status``, so without the disable the
verifier would poll the FakeReviewAPI default for the full 180 s
on a real ``time.sleep``. The verifier itself has dedicated
coverage in ``TestPostPushCIVerify`` / ``TestPostPushCIVerifyCycleBudget``.

uv.lock change for the new ``mcp-servers`` optional dep stays
unstaged — it's missing its matching ``pyproject.toml`` entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 00:21:51 -04:00
drew 2c43179e70 feat(auto-agents): R3.7 post-push CI verification + per-cycle budget
Closes the local-vs-remote CI divergence the 2026-05-17 run-7
observation exposed: the implementer worker's ``ci_run_local_gate
--fast`` doesn't catch what remote CI does (test sharding,
parallel-job orchestration, remote-only timing). So the worker
could push a commit + claim ``outcome=resolved`` even when remote
CI would reject it; the dispatcher would treat the cycle as a
success and not re-dispatch.

With this gate, the dispatcher is the source of truth for "did
this PR actually pass CI."

How it works:
- Runs ONLY when the worker claims ``outcome=resolved`` AND
  ``head_sha_advanced is True`` (a real push happened).
- Polls Forgejo's combined CI status on the post-session head_sha
  every 10 s for up to 90 s.
- If CI lands in {failure, error}: rewrites parsed_json's
  ``outcome`` to ``post-push-ci-failed`` so
  ``_implementer_escalation.decide`` routes it as a failure
  (ESCALATE / EXHAUSTED). Stashes the original outcome +
  failing-context list under ``_post_push_ci_verification`` for
  telemetry.
- If CI is pending after the budget: outcome unchanged (don't
  penalise the worker for slow CI; next dispatcher cycle
  re-classifies).
- If fetch fails: outcome unchanged (Forgejo flake protection —
  "I couldn't check" must not equal "the worker lied").
- Dry-run short-circuits to no-op so ``--dry-run`` cycles don't
  burn 90 s polling.

Per-cycle polling budget:

Without a cycle-wide cap, a dispatcher cycle processing N PRs all
landing in ``outcome=resolved`` after a push could spend
``N * 90 s`` polling — at 5 PRs/cycle that's 7.5 min eating the
dispatcher cycle budget. Added a sliding-window budget:

- ``IMPLEMENTER_POST_PUSH_CI_CYCLE_BUDGET_S`` (default 300 s) caps
  total polling time across all verify calls in one dispatcher
  cycle.
- ``IMPLEMENTER_POST_PUSH_CI_CYCLE_RESET_AFTER_S`` (default 120 s)
  auto-resets the budget after that much idle — naturally fires
  on cycle boundaries without the dispatcher's outer loop having
  to call a reset hook.
- Per-call budget is also capped by remaining cycle budget so a
  near-exhausted cycle doesn't get blown through by one fat call.
- ``reset_post_push_ci_cycle_budget()`` exposed for tests and any
  future dispatcher hook that wants to reset explicitly.

Tests:
- 22 tests in ``TestPostPushCIVerify`` (the pre-existing class)
  cover happy path, rewrite-on-fail, head_sha_advanced gating,
  feature-flag short-circuit, transport flake handling, failing-
  contexts extraction, etc.
- 4 new tests in ``TestPostPushCIVerifyCycleBudget`` pin the
  budget contract: exhausted budget skips verification, idle
  auto-reset works, explicit reset zeroes state, per-call cap
  respects remaining cycle budget.

Tuning notes:
- 90 s per-call: enough to catch fast-failing checks (lint/format
  fail in 30-60 s typical) without dominating the cycle.
- 10 s poll interval: 9 polls per per-call budget. Forgejo's CI
  status endpoint is fast (< 1 s typical).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 00:05:30 -04:00
drew 8ef2f949c2 fix(auto-agents): R3.5 — cap parity + attempt-label conclusive-outcome gate
Two audit findings from the post-R3.4 walk-through, both addressing
gaps where one pool's stop-signal didn't propagate to the other.

P0 — Universal triage-label exclusion at the candidate collector
------------------------------------------------------------------

Pre-R3.5 only the reviewer-side Python filter honoured
``auto/needs-human-triage`` (via ``_evaluate_filter``'s ``is_excluded``
check). The implementer (and any other dispatcher that uses
``collect_candidates``) would still pick up triage-labeled PRs because
their filter scripts (``list_prs_ci_failing`` etc) had no knowledge
of the label. Result: the cycle-cap's "all automation pauses on this
PR" signal was half-effective — the reviewer stopped iterating but the
implementer kept trying, defeating the cap's purpose.

Fix: filter triage-labeled items inline in ``collect_candidates`` so
the exclusion applies to every dispatcher routed through that path,
regardless of which filter (Python or legacy TS) produced the item.
Uses the existing ``_cycle_cap.labels_carry_triage`` helper which
handles both Forgejo label shapes (dict-of-name and flat string).

P1.1 — apply_attempt_label gated on conclusive outcomes
-------------------------------------------------------

``_post_session_action_with_escalation`` wrote
``auto/last-attempt-tier-N`` unconditionally after the first worker
attempt, including on ``timeout`` / ``transport-error`` sessions that
never produced a verdict. The next cycle's
``_read_start_tier_from_labels`` then bumped the start tier to N+1 —
wasting a tier on a fix the lower one might have handled, AND feeding
the estimator's step 2a constraint a false "tier N failed" signal.

Fix: gate the label write on ``terminal_state == "completed"`` AND a
parsed_json with an outcome field. Environmental failures (network
blip, OpenCode 5xx) now leave the prior cycle's tier label intact
instead of bumping. An ``INFO`` log fires on the skip path so an
operator grepping the journal can correlate "no label written this
cycle" with the underlying terminal_state.

The in-cycle escalation respawn (site 2 at line 3162) is unchanged —
it only fires when ``_implementer_escalation.decide()`` returns
ESCALATE, which already accounts for the retry budget and is only
reached for conclusive failures.

P1.2 — Cross-pool parity test
-----------------------------

Two new tests in ``test_dispatch_runtime.py``:

- ``test_collect_candidates_excludes_triage_labeled_items`` pins the
  cross-pool invariant with both label shapes (dict-of-name and flat
  string). Regression-guard: a future dispatcher that bypasses the
  collector or weakens the label check here would re-enable the
  doom-loop pattern.

- ``test_collect_candidates_accepts_missing_labels_field`` covers the
  defensive case — items returned from older list scripts may omit
  ``labels`` entirely; the filter must treat that as "no labels" and
  pass the item through, not crash.

Tests: 2305 auto_agents passing (+2 new from this commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:14:57 -04:00
drew ee120011c8 feat(auto-agents): R3.2 — estimator cache invalidation on failure + polish
Addresses the P0/P1 + P2 items from the post-R3.1 review pass.

What changed
------------

1. **Cache invalidation on non-success worker outcome** (P2 #24,
   doom-loop defense). New ``_invalidate_estimator_cache_on_failure``
   helper is called from ``_dispatch_post_session_action`` BEFORE
   the inner action runs. When the cycle's worker terminal_state
   isn't "completed" with ``outcome=resolved``, the cache entry for
   that PR is dropped — so the NEXT cycle re-asks the estimator
   with the freshly-updated attempt-history digest in the prompt.

   Without this, a stuck PR whose ``auto/last-attempt-tier-N`` label
   fails to land (the run-15 doom-spiral failure mode) would re-serve
   the same cached tier recommendation across cycles until the cache
   TTL (default 1 h) expired. The cache now ALSO closes the gap that
   amplified the failure mode the estimator's step 2a cross-cycle
   constraint exists to defend against.

   Success path preserved: ``outcome=resolved`` keeps the cache so
   the (rare) "same PR / same SHA next cycle" case still hits.

2. **``estimator-implementation.md`` prose updated** (P0):
   - Line 203 dropped the obsolete ``task_prompt`` reference (the
     retired tier-dispatcher's parameter). The body IS the prompt
     directly post-R3 — the agent reads sections inline at the top
     level.
   - Added a one-paragraph note documenting the cache + invalidation
     contract so operators / future readers know the agent's
     recommendation may be cached for up to 1 h and is invalidated
     on worker failure. Reinforces why step 2a (refuse-failed-tier
     constraint) is load-bearing — when the cache invalidates, the
     estimator MUST recommend a strictly-higher tier per the
     attempt-history digest.

3. **Code polish** (P1):
   - Hoisted ``import time`` to module level (was inline in two
     cache helpers — no circular-import reason for the local form).
   - Added explicit INFO log when the estimator returns
     ``is_confident: false`` so operators see the no-confidence
     path in the journal alongside the other estimator outcomes.
   - Expanded the ``_ESTIMATOR_CACHE`` comment to document all
     three invalidation modes (head_sha change, TTL, failure) AND
     the single-threaded-dispatch assumption (with a future-async
     warning for the lock requirement).
   - New ``_estimator_cache_drop_pr(pr_number)`` helper for
     single-PR invalidation (used by the failure path); the
     existing ``_estimator_cache_clear()`` still wipes everything.

4. **New test coverage** (+11 tests):
   - ``TestEstimatorCacheInvalidationOnFailure`` (7 tests): drops
     on unresolved / timeout / transport-error / synthesized
     failure outcomes; preserves on resolved; no-op for issue
     items (no head_sha); end-to-end via
     ``_dispatch_post_session_action``.
   - ``TestEstimatorResultCache::test_cache_does_not_bleed_across_pr_numbers``
     — same head_sha + different PR must NOT share a cache slot.

Full auto_agents suite: 2291 passing (+11 from new tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:36:27 -04:00
drew 12286b8a3f feat(auto-agents): R3.1 — estimator wrapper, cache, prose refinements
Three follow-ups to the R3 wrapper-chain retirement (80d61de94),
addressing review-pass findings on the Python estimator path:

1. **``_wrap_for_estimator`` drops the triple-backtick fence**
   around the body. The fence had turned the body's
   ``## Pre-fetched …`` headers into code-block content, which the
   estimator's section-finder logic might miss (subtle tokenizer
   ambiguity). Post-refinement the body is emitted at the top
   level under a short directive — the estimator's existing
   "Look for the following sections in your prompt" logic sees
   the headers exactly where it expects them.

2. **``(pr_number, head_sha)`` cache for estimator results**, with
   1 h default TTL (``IMPLEMENTER_ESTIMATOR_CACHE_TTL_S`` env
   override). Defense against the run-15 doom-spiral failure mode
   (2026-05-16): when the ``auto/last-attempt-tier-N`` label
   mechanism falls open, the dispatcher's strict-walk doesn't seed
   ``start_tier > 0`` and every cycle re-runs the estimator on the
   same PR + same commit to confirm the same answer. The cache
   short-circuits that. Both confident and no-confidence outcomes
   are cached so the null-result case doesn't re-burn the estimator
   either. Transport / timeout failures are NOT cached
   (environmental — retry next cycle). New commit (different
   head_sha) implicitly invalidates the cache entry.

3. **``estimator-implementation.md`` prose updated** to reflect
   post-R3 reality: the estimator runs as a top-level OpenCode
   session, no intervening ``task`` hops, prefetched sections
   survive intact in the prompt. The pre-R3 prose said summarisation
   stripped most sections by depth 2 and instructed the LLM to
   compensate by always calling ``handoff_fetch_pr_context`` to
   recover the digest. That defensive call is now wasted on the
   normal path; the prose marks it as the canonical fallback for
   genuine missing-section cases (``PREFETCH=0`` rollback, upstream
   prefetch failure) but states the digest is normally present.

Tests
-----

8 new tests in ``TestEstimatorResultCache``:
- cache hit skips the session call
- new head_sha invalidates the entry
- no-confidence outcome is cached (avoids re-burn on null-result)
- transport-error is NOT cached
- callers without pr_number / head_sha bypass cache entirely
- ``_estimator_cache_pr_key`` / ``_estimator_cache_head_sha``
  helpers handle PR vs issue items correctly

``TestEstimatorPromptShape`` updated for the unfenced wrapper —
pins that the directive leads the prompt, the body appears
verbatim at the top level, and no ``\`\`\`fence`` surrounds the
body (the regression mode the refinement addresses).

Full auto_agents suite: 2280 passing (+18 from new estimator-cache
tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:29:00 -04:00
drew 80d61de942 feat(auto-agents): R3 wrapper-chain retirement — direct task-implementor variants
Eliminates the remaining LLM wrapper chain (``tier-dispatcher`` +
``tier-{min,0,1,2}`` selectors) between the Python dispatcher and the
``task-implementor`` worker. Follows the R2 implementation-worker
retirement (6e63073ad, 2026-05-16); both wrappers were pure routing
agents with no per-cycle judgment that could not be moved to Python.

Architecture
------------

Before (R2 baseline):
  dispatch_implementer.py
    → tier-dispatcher (LLM)
        → estimator-implementation (LLM, judgment)
        → tier-N selector (LLM, pure pass-through)
            → task-implementor (LLM, the actual work, via `task` hop)

After (R3):
  dispatch_implementer.py
    → estimator-implementation (LLM, judgment — invoked top-level)
    → task-implementor-tier-N (LLM, the actual work, NO `task` hops)

Two LLM hops eliminated per cycle. The ``task`` tool hop between the
tier-N selector and task-implementor is gone too, so the dispatcher's
prefetched ``## Pre-fetched …`` sections survive intact in the
worker's prompt — closing the structural cause of the ~30-80
per-session ``implementer_pr_context.py read --pr N`` round-trips
the worker burned to recover summarised-away context.

Cost savings (4-day measurement window, $-figures based on
local-claude pricing with caching):

- Eliminating tier-dispatcher sessions (32/day): ~$5-15/day
- Eliminating tier-N selector sessions (15/day): ~$2-5/day
- Eliminating prefetch round-trips (229/4d → expected near 0): ~$20-40/day

Aggregate at current traffic: roughly $30-60/day, $900-1,800/month.

What changed
------------

1. **New ``sync_tier_models.py`` scope** — generates per-tier
   ``task-implementor-{slot}.md`` + matching
   ``.opencode/models/task-implementor-{slot}.txt`` files from
   ``task-implementor.md`` (the byte source). Dropped: the bare
   ``tier-N.txt`` model files (no consumer) and the
   tier-dispatcher.md mapping-table generation (no file).

2. **New ``_call_python_estimator``** in dispatch_implementer.py
   invokes ``estimator-implementation`` as a top-level OpenCode
   session, parses ``{is_confident, recommended_tier}``, returns the
   tier integer or None. Includes a heartbeat-refresh on_poll so a
   30-180 s estimator call cannot trigger the launcher's hung-
   process watchdog. Estimator switched from ``mode: subagent`` to
   ``mode: all`` so the dispatcher can spawn it directly.

3. **New ``_resolve_task_implementor_for_tier(tier)`` helper** maps
   manifest tier integers to the matching ``task-implementor-{slot}``
   variant. Used by both the initial dispatch (in the prompt
   factory) and the in-cycle escalation respawn.

4. **WorkGroup contract extended** with
   ``requires_worker_agent_override: bool`` (default False, opt-in
   per group). The implementer's three WorkGroups set True;
   ``_resolve_effective_worker_agent`` raises a clear RuntimeError
   if the prompt_factory failed to populate the override (a code
   bug that would otherwise silently run every cycle at the static
   fallback tier).

5. **``_implementation_prompt_dispatch`` refactored** to:
   - Resolve the tier in Python (label-driven hint → estimator →
     default 0), honouring both the in-cycle escalation flag and the
     estimator-enabled flag.
   - Stash the resolved ``task-implementor-tier-<slot>`` agent name
     on the item context under
     ``WORKER_AGENT_OVERRIDE_ITEM_KEY`` (single source of truth in
     ``_dispatch_runtime``; imported into the higher layer).
   - Emit the worker body with ``escalation_tier: \`N\``` directly —
     no more ``escalation_tier_hint``, ``task_prompt:`` fence, or
     ``task_agent:``/``estimator_agent:`` outer parameters (all
     consumed by the retired tier-dispatcher).
   - Skip the estimator call on ``--dry-run`` so the operator-
     visible no-I/O contract holds.

6. **Retired agent files DELETED**:
   - ``.opencode/agents/tier-dispatcher.md``
   - ``.opencode/agents/tier-{min,0,1,2}.md``
   - ``.opencode/models/tier-{min,0,1,2}.txt``
   - Matching entries in ``opencode.json``'s agent block.

7. **Prose updates** to ``task-implementor.md`` (the byte-source for
   variants), ``estimator-implementation.md``, and production
   docstrings (``_block_store.py``, ``_pr_context_sentinel.py``,
   ``implementer_workspace.py``, ``_review_post.py``,
   ``_review_finalize.py``) reflecting the post-R3 chain. The
   filesystem handoff scripts (``implementer_pr_context.py``,
   ``implementer_workspace.py``) remain in place as the canonical
   read path — defensive against any future regression that re-
   introduces summarisation.

Tests
-----

2262 auto_agents passing (was 2268 pre-R3; net -6 from
removing tests pinning the retired wrapper-chain contract,
offset by +14 new tests pinning the post-R3 contract):

- ``TestEstimatorEnabledFlag`` rewritten to assert
  ``escalation_tier`` + agent-override semantics.
- New ``TestEstimatorPromptShape`` (5 tests) pins the body shape
  the Python estimator helper passes to the agent and the
  call shape into ``run_session_blocking``.
- New ``TestResolveEffectiveWorkerAgent`` (8 tests) directly
  covers the override priority chain — override present, empty,
  whitespace, non-string, whitespace-stripped, required-but-missing
  (loud fail), required-and-present.
- ``test_dry_run_never_calls_estimator`` pins the dry-run no-I/O
  contract via an exploding-stub guard on the estimator helper.
- ``TestDirectTierDispatch`` replaces the retired
  ``TestTierDispatcherShortCircuit`` suite in
  ``test_worker_permissions.py``.
- ``TestTaskImplementorVariantsAreByteIdentical`` ensures the
  four per-tier variants never hand-diverge from each other.
- ``test_no_legacy_tier_agents_in_opencode_agent_block`` fails
  loudly if any of the retired tier-* entries are re-introduced
  to ``opencode.json``.

Operator notes
--------------

- The C3 footgun (model swaps need OpenCode restart) still applies
  to the generated variants — edit ``tiers.yaml``, re-run
  ``python3 tools/sync_tier_models.py``, then restart OpenCode.
- The estimator now runs as a top-level OpenCode session; an
  operator grepping the session archive will see
  ``[AUTO-IMP-PR-N-estimator] estimator-implementation`` entries
  alongside the worker sessions.
- Roll-back: revert this commit + the R3 prep commit (b8c1e4903).
  Both wrappers + the static-fallback ``worker_agent`` come back;
  no schema migration needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:03:30 -04:00
drew 2658deee94 feat(auto-agents): PR State Warmer substrate + supporting infra
Adds a long-lived sidecar (pr_state_warmer.py) that polls Forgejo's
/pulls endpoint every 30s and writes the full PR snapshot to a
shared SQLite store, eliminating the dispatcher's per-cycle
cold-cache stalls (24-30s rebuilds on flaky cycles) and the silent
50-PR pagination cap on the legacy single-page fetch.

Substrate
- tools/_pr_state_cache.py  — SQLite store with (owner, repo) PK,
  WAL mode, additive v2→v3 migration (comments_refreshed_updated_at),
  bounded fcntl.flock migration lock, threading.Lock for per-process
  init, @_with_reheal decorator (catches OperationalError no-such-
  table + DatabaseError corruption with file quarantine), atomic
  TEMP-table chunking for >32k seen-set, _normalize_updated_at to
  canonicalize Forgejo tz-marker drift
- tools/pr_state_warmer.py — poll/upsert/vanish/comments-refresh
  loop with fcntl.flock singleton (rejects second warmer), bounded
  comments-refresh cap, persistent deferral via SQL pending query,
  PermissionError-tolerant lock setup, cold-start log suppression
- tools/_pr_classification_cache.py — three-layer fall-through
  (warmer cache → list cache → live fetch) with staleness gate
  (PR_STATE_WARMER_STALE_AFTER_S floored at 30s in prod)

Comments cache hardening
- Bot-filter at write time drops bot status/claim/release/sentinel
  while preserving **Implementation Attempt** markers (94.6%
  reduction on bot-heavy PRs like #30's 19k-comment thread)
- _normalize_since_cursor strips microsecond precision before
  building ?since= query (fixes the live-observed Forgejo HTTP 422
  bug on PRs #25 + #28); handles uppercase Z, lowercase z, ±HH:MM
  offsets (including non-zero like +05:30), naive ISO
- Lazy migration of legacy null-key by_author entries on _read_cache
- _newest_cursor walks tail-back skipping malformed entries

Supporting infrastructure (cumulative dmpipeline-v2 work)
- Telemetry server: SSE live tail, run-sessions enumeration,
  cost/token tracking, app.js UI rewrite with collapsible sections
- MCP servers (mcp_ci_server, mcp_forgejo_server, mcp_git_server,
  mcp_handoff_server, mcp_graphify_server) for opencode worker
  context access
- Live log writer (tools/live_log_writer.py) — SSE-streaming
  dispatcher event log
- Tier-dispatcher escalation flow with prompts trimmed for budget
- Shared bot-logins resolver (tools/_bot_logins.py) replacing two
  drift-prone copies
- token_usage_audit.py for opencode cost analysis

Tests
- 2259 passing across 65 changed/new files
- New suites: test_pr_state_cache, test_pr_state_warmer,
  test_pr_state_warmer_integration, test_pr_classification_cache,
  test_pr_list_cache_backoff, test_mcp_* (5 servers),
  test_live_log_writer_sse, test_telemetry_run_sessions,
  test_review_post_ready_label
- Test_pr_comments_cache expanded with bot-filter coverage,
  cursor-normalization regression pins, format-drift, atomicity,
  failed-comments-not-stamped (silent-data-loss class)
- Parametrized @_with_reheal coverage across 7 wrapped APIs
- Real fault-inject atomicity test for chunked mark_vanished path
  via Connection wrapper class
- Subprocess-based singleton flock test (cross-process contract)
- Event-driven SIGTERM-mid-poll test (no fixed-sleep flake)

Architecture notes
- Schema v3 migration is additive (ALTER ADD COLUMN); v0/v1 still
  need destructive rebuild because pre-v2 column shape lacks
  owner/repo. Cross-process drop-table-ping-pong prevented by the
  fcntl migration lock + per-process _initialized flag.
- Comments-refresh deferral is persistent via
  comments_refreshed_updated_at column — survives warmer restart,
  picks up next cycle even if PR didn't change again. Replaces
  in-memory changed_numbers list.
- Rollback path: PR_STATE_WARMER_PREFER=0 bypasses the warmer
  cache and reverts to live-fetch behavior. PR_STATE_CACHE_DISABLE=1
  short-circuits the warmer process at startup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 10:02:53 -04:00
drew dc4e9368f2 feat(auto-agents): block-store substrate + DRY refactors
Externalise large prompt sections (PR diff, comments, CI failure
logs, reviews, linked issues) into a cross-process SQLite-backed
block store so the worker can recover original content when an
intermediate tier-* agent's summarisation strips inline sections.

New substrate
-------------

- ``tools/_block_store.py`` — SQLite WAL, per-row 1MB cap, 1h TTL,
  janitor (startup + opportunistic per-hour in register()),
  threading.Lock around the periodic-sweep gate.
- ``tools/_block_prompt.py`` — registration glue + ``## Available
  blocks`` Markdown table renderer.
- ``tools/_prefetch_section.py`` — single-source-of-truth section
  registry; collapses the duplicate ``_register_*_blocks`` helpers
  the reviewer and implementer previously kept in lockstep.
- ``tools/mcp_block_store_server.py`` — FastMCP wrapper exposing
  ``block_fetch`` / ``block_list`` / ``block_register`` /
  ``block_invalidate`` to agents. Uses the expanded ``_mcp_common``
  helpers.
- ``tools/_implementer_escalation_helpers.py`` — pure helpers
  extracted from ``dispatch_implementer.py`` (~180 lines off the
  3284-line file); takes ``claim_runtime`` as a kwarg for clean DI.

DRY refactors
-------------

- ``tools/_backoff.py`` — shared ``Backoff`` dataclass collapses the
  three near-identical exponential-backoff state machines in
  ``_pr_comments_cache``, ``_ci_logs``, ``_pr_classification_cache``.
- ``tools/_mcp_common.py`` — expanded with ``bootstrap_loader``,
  ``error_envelope``, ``make_main`` so each MCP server's prelude is
  three lines.
- ``tools/_pr_diff.build_diff_section_full`` — returns a 4-tuple
  including the raw diff body so the reviewer's block-store
  registration reuses the bytes instead of doing a second HTTP fetch.

Wiring
------

- ``_review_prompt.build_review_prompt`` builds a ``PrefetchSection``
  registry via ``_review_sections``, registers them, and renders the
  ``## Available blocks`` table at the end of the prompt.
- ``_implementer_prefetch._fetch_pr_context`` /
  ``fetch_new_issue_context`` build the equivalent registry via
  ``_implementer_sections`` and stamp ``result.block_refs`` for the
  prompt builder to read.
- ``_implementer_prompt`` builders include
  ``_build_available_blocks_section(result)`` in all three flows.
- ``dispatch_review.main`` + ``dispatch_implementer.main`` call
  ``_block_store.janitor()`` at startup; the per-call opportunistic
  janitor in ``register()`` keeps the file bounded between restarts.

Agent contract updates
----------------------

- ``.opencode/agents/task-implementor.md`` +
  ``.opencode/agents/pr-review-worker.md``:
  - ``block_store*`` permission
  - new "Block-store substrate" paragraph explaining
    ``block_fetch`` / ``block_list`` as the summarisation recovery
    path.

Tests
-----

- ``test_block_store.py`` — 48 tests pinning every public contract
  (register/fetch/list/invalidate/janitor, key whitelist, TTL,
  size cap, WAL durability).
- ``test_block_prompt`` — covered transitively via the e2e test.
- ``test_block_store_recovery_e2e.py`` — builds a real prompt via
  ``build_pr_fix_prompt``, applies a heading-bounded summariser stub
  (``_summarise_inline_sections``), asserts inline content is stripped
  yet block keys survive and ``block_fetch`` recovers original content.
  Plus a ``block_list`` fallback test for the worst case where the
  table itself was summarised away.
- ``test_mcp_block_store_server.py`` — 27 wrapper-contract tests.
- ``test_mcp_block_store_transport.py`` — spawns the actual server
  subprocess via ``mcp.client.stdio`` and exercises the JSON-RPC
  transport round-trip in ~1.5s. Catches FastMCP schema /
  serialisation bugs the in-process tests miss.
- ``test_backoff.py`` — 14 behaviour-focused tests of the shared
  ``Backoff`` curve.
- ``test_pr_comments_cache.py`` + ``test_ci_logs.py`` — deleted the
  now-redundant ``TestComputeNextAttemptAfter`` / ``TestBackoffActive``
  / ``TestBackoffHelpers`` classes; ``test_backoff`` covers them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:55:52 -04:00
drew 6e63073ad9 feat(auto-agents): retire implementation-worker wrapper (R2 wrapper-chain tax)
The depth-0 implementation-worker agent was a pure passthrough whose
only LLM-side responsibility was translating the dispatcher-built
prompt into tier-dispatcher's input format (top-level params +
``task_prompt:`` fenced body). Deterministic Python now does the
same translation in microseconds, removing one full LLM hop (and
its summarization-risk warning prose) from every implementer cycle.

What changed:

1. **New ``_wrap_for_tier_dispatcher`` helper** in
   ``tools/dispatch_implementer.py`` — emits the exact format
   documented in ``.opencode/agents/tier-dispatcher.md``: outer-level
   ``task_agent: \`task-implementor\``` + ``estimator_agent:
   \`estimator-implementation\``` + optional ``escalation_tier_hint``
   line, then the body verbatim inside a fenced ``task_prompt:``
   block.

2. **``_implementation_prompt_dispatch`` refactored** to compute
   the escalation_tier_hint upfront (start_tier vs estimator-flag
   logic preserved exactly) and wrap+return once. The three prior
   return paths (early-return / explicit-hint / final-extras) all
   funnel through the same wrap helper.

3. **WORK_GROUP defs swapped**: three ``worker_agent="implementation-
   worker"`` → ``"tier-dispatcher"`` so the dispatcher invokes
   tier-dispatcher directly as the top-level OpenCode session.

4. **``release_claim_on_exit`` removed** from the emitted prompt.
   It was an implementation-worker-only directive telling the
   wrapper to skip its own session-end release; tier-dispatcher
   has never read it. The dispatcher's ``finally`` block has
   always owned the actual claim lifecycle and is unchanged.

5. **Test updates**: three test files updated to match the new
   contract — assert absence of ``release_claim_on_exit`` and
   ``worker_agent == "tier-dispatcher"``. All other tests pass
   without modification (the wrapper's .md file remains as
   historical doc; can be deleted in a follow-up cleanup).

Audit confirmed no permission boundary lost (both agents are
``mode: all`` with equivalent bash/write allowlists), no agent-
name conditionals in ``_opencode_worker``, no archive-filename
grep filtering, tier-dispatcher input format is a 1:1 superset
of what the wrapper emits.

Live-cycle savings: removes the wrapper's ~1.5–3K tokens of
reasoning + tool overhead, removes one full session-creation +
session-archive cycle, removes the "verbatim forwarding"
summarization risk (deterministic Python can't lose sections).
Chain shrinks from 4 levels (impl-worker → tier-dispatcher →
estimator/tier-N → task-implementor) to 3 (tier-dispatcher →
estimator/tier-N → task-implementor).

Full auto_agents suite: 2056 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 16:10:01 -04:00
drew ea4a96aad6 feat(auto-agents): worktree hygiene — startup janitor + retry-on-failure
Live evidence on PR #29 (runs 15-18): 4 implementer worktrees
accumulated in ``/tmp/cleveragents-implementer-worktrees/``, one
with a corrupted ``.git`` link (``fatal: not a git repository``).
Root cause: every SIGTERM-induced dispatcher restart leaves the
in-flight cycle's worktree orphaned, and the mirror's
``worktrees/<name>/`` bookkeeping survives without the directory.
The next cycle's ``git worktree add`` against the same mirror can
then fail with "already exists" or unhelpful path collisions.

Two fixes that together close the loop:

1. **Startup janitor** (``_pr_clone.prune_orphan_worktrees``): scans
   the per-kind worktree base on dispatcher startup and removes
   any dir matching the canonical ``pr-{N}-{kind}-{hex-tag}``
   shape that is EITHER older than the OpenCode worker ceiling
   (default 30 min — longer than any possible in-flight cycle)
   OR has a missing / zero-byte ``.git`` link (definitionally
   corrupted). Removes the dir AND the mirror's ``worktree``
   bookkeeping. Idempotent. Skips operator scratch dirs that
   don't match the canonical name. Disable via
   ``DISPATCHER_WORKTREE_JANITOR_DISABLE=1``. Called once at the
   top of both ``dispatch_review.main`` and
   ``dispatch_implementer.main``.

2. **Retry-on-failure** in ``prepare_pr_worktree``: when
   ``git worktree add`` fails the first time, run
   ``git worktree prune`` to clear stale mirror bookkeeping, force-
   remove the target path if present, and retry exactly once.
   This rescues cycles whose janitor-min-age cushion missed a
   fresh orphan from a very-recent SIGTERM.

Coverage: 13 new tests in ``test_pr_clone_janitor.py`` (recent vs
stale removal, corruption detection regardless of age, non-canonical
name safety, idempotency, disable env, ``git worktree remove``
call count). Full auto_agents suite: 2059 passing (+13 vs prior
commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:42:06 -04:00
drew cf13de9b5a feat(auto-agents): per-repo linked-issue policy (strict / informational-on-not-found / disabled)
Adds a per-repo policy knob for ``Closes #N`` / ``ISSUES CLOSED: #N``
resolution during dispatcher pre-fetch. Three modes:

- ``strict`` (default, unchanged) — every reference is resolved; a
  ``not-found`` (4xx) result is rendered as a quality concern the
  reviewer is expected to surface; a ``fetch-error`` (5xx / runtime)
  trips the dispatcher's defensive APPROVED→COMMENT downgrade.
- ``informational-on-not-found`` — a ``not-found`` result is shown
  to the reviewer with an explicit policy note: "this signal is
  informational, not blocking; MUST NOT factor into your verdict."
  ``fetch-error`` semantics are unchanged. Typical setting for forks
  harvesting commits from an upstream with its own issue tracker.
- ``disabled`` — skip resolution entirely; section renders
  "linked-issue resolution disabled by repo policy" preamble. The
  worker is told it has no traceability check to perform.

Wired through ``DispatchConfig.linked_issue_policy`` (defaults to
``"strict"``) and ``{REVIEW,IMPLEMENTER}_DISPATCHER_LINKED_ISSUE_POLICY``
env vars. ``normalize_linked_issue_policy()`` validates the value at
``load_config`` time so a typo'd env var fails startup with a clear
error rather than silently defaulting.

Coverage: 15 new tests across ``test_review_fetch.py`` and
``test_review_views.py``. Touched-module suite: 250 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:55:46 -04:00
drew 363bcfad61 refactor(auto-agents): extract _classify_metadata_only as the first C2 step
dispatch_implementer.py is 3225 lines — well over the ~500-line per-
module budget the rest of tools/ honours. C2 is the long-overdue
decomposition; this commit starts the work with the smallest /
cleanest extraction available: the pure G1 metadata-only classifier
(no module state, single public surface) moves to a new sibling
_implementer_metadata_classifier.py, loaded through the existing
_loader.load_sibling machinery just like every other helper in this
file.

The leading-underscore alias dispatch_implementer._classify_metadata_only
re-exports the function so the G1 tests (and any other call sites)
keep working unchanged. The extraction is a pure refactor — the
full 1754-test auto-agents suite passes unchanged.

Subsequent C2 steps (extract _post_session_action_with_escalation —
the ~350-line nested loop the harvest plan singled out by name —
plus prompt-assembly + short-circuit blocks) are larger pieces that
warrant fresh-context attention. This commit establishes the
pattern (new module, _load_sibling line, leading-underscore alias)
for those follow-ups.

Refs: docs/development/final-working-harvest-plan.md (C2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:29:59 -04:00
drew 23ae44e432 feat(auto-agents): loud signal on degraded prompt assembly (W8, default-OFF)
Adds assess_prompt_completeness() to _implementer_prefetch.py — a
pure helper that translates the carrier's per-section completion
flags into a single {degraded, missing_sections, error_kinds} dict.
The aggregate data_complete flag already encoded the AND, but as a
single bool it lost the *which sections* signal an operator needs
to triage a degraded cycle. This helper keeps both together.

dispatch_implementer._prefetch_prompt now calls the helper after
prefetch and stashes the result on the per-item context as
prompt_completeness. When IMPLEMENTER_DEGRADED_PROMPT_LOG_ENABLED=1
(default OFF, per the dmpipeline safety contract) AND the cycle is
degraded, also emits a WARN log line naming the specific missing
sections so an operator tailing the dispatcher log sees the
silent-degradation signal without parsing telemetry JSONL.

The completeness signal is always stashed on the context regardless
of the flag, so future telemetry / status-comment paths can pick it
up without operators flipping any switch. The flag specifically
gates the LOG emission — the harvest plan's primary value
("converts a silent correctness risk into an observable one").

5 unit tests for the helper covering happy / single-fail / multi-
fail / diff-truncation / direct-data_complete-flip paths.

Refs: docs/development/final-working-harvest-plan.md (W8).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:27:19 -04:00
drew dc9c30f887 feat(auto-agents): metadata-only diversion classifier (G1, observability-only)
Adds _classify_metadata_only(item, prefetch) — a pure, deterministic
heuristic over the dispatcher's already-collected prefetch carrier
that returns True when a PR appears to need only label / milestone
work. Decision signals:

* Must be PR-shaped (new_issue work is always code work).
* CI overall state must NOT be failure/error.
* No per-check ci_detail row in failure/error state (defensive
  against Forgejo race windows where combined state lags).
* No active request_changes_reviews (conservatively treats every
  RC review as referencing source, per the harvest plan's "when
  in doubt classify as code work" tie-breaker).

Result is stamped on item["_dispatcher_implementer_context"]
["metadata_only_candidate"] so cycle telemetry and any future
diversion path can observe how often the heuristic fires on real
PRs before any behaviour changes. No production behaviour change
in this commit — the dispatcher still spawns the worker for every
cycle regardless of the classification. Actual diversion is
deferred until either a dedicated grooming driver consumes
groom_label_inference's rule engine (added by G7) or the
implementation-worker prompt is updated with a no-clone fast path
on the sentinel.

8 unit tests cover happy / failure / empty-input cases including
the conservative tie-breaker for ambiguous CI state and missing
prefetch carriers.

Refs: docs/development/final-working-harvest-plan.md (G1, A1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:52:08 -04:00
drew 1dba881a14 feat(auto-agents): re-enable estimator-driven adaptive tier selection (G11, default-OFF)
Three coordinated edits behind IMPLEMENTER_ESTIMATOR_ENABLED:

1. dispatch_implementer.py — on a TRUE first attempt (no prior
   auto/last-attempt-tier-N label AND no in-cycle escalation seeding
   start_tier > 0), OMIT the escalation_tier_hint line when the flag
   is ON. When the flag is OFF (default), explicitly emit
   `escalation_tier_hint: `0`` so the worker short-circuits to
   tier-0 byte-for-byte to the pre-G11 build (the worker no longer
   defaults to 0 on its own — see edit #2).

2. implementation-worker.md — change the "default to 0 when no hint
   in incoming prompt" fallback to OMIT the line entirely when absent.
   Follows the general "Only include a variable line if explicitly
   present" rule the worker already applies to every other variable.

3. estimator-implementation.md — remove the contradictory CRITICAL
   rule ("Always use tier 0 when the pull request is new ...
   Estimation should only be applied on escalation after the first
   attempt"). Without this removal, even with the Python flag ON the
   estimator would force is_confident: false on every first attempt,
   silently re-disabling the feature.

Net behaviour with flag ON: trivial PRs route to tier-min upfront,
confidently-hard PRs route to tier-1 / tier-2 upfront skipping the
wasted tier-0 attempt, normal/ambiguous PRs default to tier-0 via
is_confident: false (same as today). Cross-cycle resumption and
in-cycle escalation continue to short-circuit the estimator by
emitting their explicit hints — the estimator runs only on TRUE
first attempts.

7 dispatcher behaviour-matrix tests (full ESCALATION × ESTIMATOR ×
resumption-label cube) + 4 agent prompt-contract tests pinning the
load-bearing wording of both prompt edits.

Refs: docs/development/final-working-harvest-plan.md (G11).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:43:38 -04:00
drew 1eac4ea233 refactor(auto-agents): manifest-driven tier-model registry, slot-based naming
Replace the scattered per-agent .txt-file mapping (whose names embedded
model-family identity like tier-qwen-med and tier-kimi and went stale
the moment a model was swapped) with a single source-of-truth manifest
at .opencode/models/tiers.yaml. The four tier slots get model-agnostic
slot-based names (tier-min, tier-0, tier-1, tier-2); the model in each
slot is configured ONLY in the manifest.

Derived artifacts (per-agent .txt files and the mapping table block in
tier-dispatcher.md) are now generated by tools/sync_tier_models.py.
A drift-detection test in tests/auto_agents/test_tier_model_registry.py
fails CI if any derived file diverges from the manifest, if an agent
referenced by the manifest lacks an agent file, if the manifest cites
a provider not declared in opencode.json, or if opencode.json carries
a stale tier-* entry.

To swap a model in a slot: edit tiers.yaml -> run
python3 tools/sync_tier_models.py -> commit. The runtime dispatcher
re-reads the .txt files per cycle (no restart); the static OpenCode
config path needs a server restart.

Tier rename mapping (escalation_tier integers UNCHANGED):
  tier-qwen-small  -> tier-min  (slot -1)
  tier-qwen-med    -> tier-0    (slot  0, default first attempt)
  tier-qwen-large  -> tier-1    (slot  1)
  tier-kimi        -> tier-2    (slot  2)

Vestigial tier-* agents removed (declared but never in the active
mapping): tier-haiku, tier-sonnet, tier-opus, tier-codex,
tier-gpt5-mini, tier-gpt5-nano, tier-o4-mini.

estimator-implementation.md now reasons in capability descriptors
(cheapest / default / advanced / complex) instead of model-family
labels (qwen-small / qwen-med / qwen-large / kimi), so the estimator
stays correct across model swaps. The stale "default tier = gpt-5-mini"
docstring claim (already drifted to claude-haiku-4-5) is removed.

Companion prose updates across every consumer of tier names:
- Agent prompts: tier-dispatcher.md, implementation-worker.md,
  estimator-implementation.md
- Skills: implementer-pr-context, implementer-workspace
- Python: dispatch_implementer.py, _opencode_worker.py,
  _pr_context_sentinel.py, implementer_workspace.py,
  setup_auto_labels.py, _attempt_history.py
- Tests: test_worker_permissions.py (parametrize list + byte-identity
  test now covers 4 slot files instead of 3 family-named files),
  test_opencode_worker_models.py (synthetic-fixture names updated)
- Docs: .opencode/models/README.md, docs/development/models.md,
  docs/development/agent-system-specification.md,
  docs/development/auto-agents-tier-2-3-plan.md,
  docs/development/implementer-in-cycle-escalation-plan.md,
  docs/development/final-working-harvest-plan.md

Validation: 1625 tests pass (+6 net new from the tier-registry test
file), 3 skipped. python3 tools/sync_tier_models.py --check exits 0.
local_ci_gate.sh --gate lint PASS.

Files: 9 added, 22 deleted, 18 modified. The drift-detection test
ran green on every step of the refactor, catching one out-of-sync
.txt file before commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:49:31 -04:00
drew 24868a1a19 fix(auto-agents): perm-layer deny on pre-clone rm + broaden stale-lock cleanup + prompt-contract regression tests
Follow-up to f720e3003 closing three review items from a multi-
perspective code review of that commit:

#1 (audit) — pr-merge-worker.md inspected; doesn't have the bug
(always uses git-isolator-util, no pre-clone path). Confirmation
test ``TestMergeWorkerHasNoPreCloneDrift`` locks this contract so a
future change wiring a pre-clone into the merge worker fails here
loudly, prompting the same conditional-cleanup + permission-deny
treatment the implementer and reviewer already have.

#2 (prompt-contract regression test) —
tests/auto_agents/test_agent_prompt_contracts.py pins the
conditional cleanup language ("ONLY IF you created it", "DO NOT
delete the pre-clone") in task-implementor.md and pr-review-worker.md.
Without this, a future prompt edit that silently re-introduces the
unconditional ``rm -rf {repo_dir}`` would not fail any test. Also
asserts the deny rules (#3 below) come AFTER the broader
``rm -rf /tmp/**`` allow in each file — the OpenCode permission
engine is last-match-wins, so a deny placed before the allow would
be silently overridden.

#3 (permission-layer hardening) — adds
   "rm -rf /tmp/cleveragents-implementer-worktrees/*": deny
   "rm -rf /tmp/cleveragents-review-worktrees/*": deny
to the bash allowlists of both task-implementor.md and
pr-review-worker.md, AFTER the broader ``rm -rf /tmp/**`` allow.
The worker is now physically unable to delete a dispatcher-owned
worktree regardless of any future prompt drift. The chief-architect
critique called this out as the right structural fix — the prose
contract "dispatcher owns the pre-clone lifecycle" now has
permission-layer enforcement.

#4 (broaden stale-lock cleanup) —
``_reset_worktree_to_pinned_sha`` now removes every
``.git/*.lock`` file (HEAD.lock / MERGE_MSG.lock / ORIG_HEAD.lock
etc.), not just index.lock. A SIGKILL'd git op mid-stage typically
leaves index.lock + HEAD.lock together, and any one of them blocks
the next git operation. Two new tests in
test_worktree_reset_resilience.py: ``test_all_git_lock_files_removed``
(covers HEAD/MERGE_MSG/ORIG_HEAD) and ``test_non_lock_files_in_dotgit_untouched``
(verifies the glob doesn't touch HEAD/config/packed-refs/index).

Tests: 16 new/updated assertions across the two test files. Suite
1619 passed / 3 skipped (+8 over the f720e3003 baseline).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:16:44 -04:00
drew f720e30035 fix(auto-agents): worker no longer deletes dispatcher pre-clone + dispatcher resilient to missing worktree
Run-11 deep inspection (PR #30 cycle, 4 escalation tiers) traced the
"worktree reset to pinned SHA ... failed ... exit 128" warnings — and
the cycle's misleading ``rebase-failed`` outcome despite the worker's
own ``{"outcome": "resolved"}`` — to two bugs that compounded:

1. task-implementor.md instructed the worker to ``rm -rf {repo_dir}``
   after every ``pr_fix`` (rule #7 + procedure step 11). That rule
   was correct in the legacy ``git-isolator-util`` workflow where the
   worker created its own throwaway clone, but with the Phase 3
   pre-clone the worktree is owned by the dispatcher and reused
   across tier escalation. Deleting it stranded every subsequent
   tier with no workspace AND blew up the dispatcher's between-tier
   reset step.

2. ``_reset_worktree_to_pinned_sha`` shelled out to ``git -C <path>``
   without checking the path existed; the resulting
   ``CalledProcessError`` was logged with ``%s`` (just "exit 128"),
   so the real cause was invisible without py-spy.

Fixes:

- task-implementor.md: rule #7 + ``pr_fix`` step 11 now spell out
  the conditional — delete only if YOU created the clone via
  ``git-isolator-util``; leave the dispatcher's pre-clone alone.
  ``issue_impl`` step 10 keeps an unconditional ``rm -rf`` (no PR
  pre-clone exists for new-issue work) plus a one-line clarifying
  note.

- dispatch_implementer._reset_worktree_to_pinned_sha: detect missing
  worktree dir BEFORE shelling to git (returns False with a clear
  "disappeared before reset" warning naming the path + pinned SHA);
  remove a stale ``.git/index.lock`` if a SIGKILL'd previous-session
  git op left one behind; capture stderr from ``CalledProcessError``
  and include it (truncated) in the warning so future diagnosis
  doesn't require a session-archive deep dive. Fail-soft policy
  preserved — escalation still continues, the next session's
  ``implementer-workspace.py discover`` will fall through to
  ``git-isolator-util`` when the worktree is gone.

Tests: new tests/auto_agents/test_worktree_reset_resilience.py with
4 classes / 8 tests pinning the missing-dir detection, stale-lock
cleanup, stderr-surfaced-on-failure, happy-path, and degenerate-input
contracts. Suite: 1611 passed / 3 skipped.

Not fixed in this commit (deferred — root cause not clear from one
sample):

- PR #29's first session at run-11 timed out at the 1800s worker
  timeout with ``turn 3 task subagent input=0tok output=0tok
  wallclock=1756s`` — task subagent stuck with no token usage. Could
  be LLM-provider hang, tool loop, or queue stall. Needs another
  reproduction + session-archive event-stream inspection to localise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:21:38 -04:00
drew 7890af4a5e fix(auto-agents): run-6 deep-inspection findings + sidecar/launcher hardening
Several of these were silent failures the worker could not recover from.

- N2: OpenCode's permission resolver is LAST-match-wins (findLast), not
  first-match-wins. A prior reorder put `"*": deny` AFTER the /tmp allows
  in task-implementor.md's edit/write/external_directory blocks, so the
  leaf could not even read its own pre-cloned worktree. Reordered
  `"*": deny` first; added an ordering-pin test.
- N1: task-implementor.md never told the leaf to emit the terminal
  {"outcome","files_touched"} JSON, so a prose-only ending dropped the
  dispatcher into its UNKNOWN bucket. Added the Terminal output contract
  + a CRITICAL rule + a content pin.
- N3: piping the gate wrapper through head/tail is denied (neither is
  allowlisted). Documented "run it bare" in the quality-gates skill and
  the task-implementor bash comment.
- N5: the launcher reap loop respawned the live-writer sidecar (and
  dispatchers) during shutdown. Added a SHUTTING_DOWN guard.
- N6: documented the events.jsonl record schema in-repo (optional-field
  absence is intentional, not an inconsistency).
- Sidecar: _load_offset returned byte 0 on cold start, re-emitting the
  whole log backlog. Now returns EOF; resolution hoisted to __init__ so
  it is deterministic vs. thread startup.
- Auto-fix push: push_branch ran git without GIT_ASKPASS and failed
  rc=128. It now takes a credential-bearing git_env from the caller;
  added apply-phase logging before the push.

N4 assessed and not a bug: data_complete=False was caused by
pr_comments:partial + diff_truncated, not the 404 linked issue.

Full suite: 1559 passed, 3 skipped (+11 new regression tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 10:23:16 -04:00
drew 86c6c89bad fix(auto-agents): P8 compliance hallucination + 3 forensic fixes from run-2
Run-2 produced 8 implementer tier attempts, 7 telemetry rows, 0 pushes.
Deep inspection of session archives pinned three root causes; this
commit addresses them.

  * P8 — _render_compliance_pointer_stanza now blocks the
    emit-resolved directive when preflight reports related-to-diff
    failures, diverges_from_remote_ci, or its own timeout. Three of
    eight run-2 attempts produced disputed-resolved rows because the
    stanza said "compliance clean -> emit resolved" while preflight
    was reporting a failing related-to-diff scenario. New helper
    _compliance_preflight_block_reasons returns human-readable
    block reasons; the renderer joins them into an explicit
    "Do NOT emit resolved" branch.

  * _render_preflight_pointer_stanza surfaces
    diverges_from_remote_ci and remote-CI failure state in prose.
    The P3 sentinel field had no prose path to the leaf because
    intermediate tier agents summarise long sections away.

  * tier-{kimi,qwen-large,qwen-med}.md: {target_agent} placeholder
    hardcoded to task-implementor in all invocation instructions.
    PR #28 Tier 2 had Kimi delegate to 'general' (depth 3) because
    the 2-bit quantized model failed to substitute the placeholder.
    The parameter is still accepted for input validation, just
    not substituted.

  * Reviewer apiGet in list_prs.ts: AbortSignal.timeout 30s -> 60s
    plus a single retry on TimeoutError/AbortError. Absorbs the
    11 Forgejo slow-response failures observed across the run-2
    reviewer dispatcher. HTTP error responses still escape
    immediately (fast-fail).

Test coverage: 8 new tests under TestDeterministicStanzas covering
all five P8 predicate branches + the three preflight-surface
behaviours. Full auto_agents suite: 1492 passed, 3 skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 19:03:20 -04:00
drew 8ed4b96b1a feat(auto-agents): perf + observability + persistent comment cache
Folds B1-B4 + C2-C5 from the post-live-test plan into one commit:

B1 — npx tsx pre-warm in dispatchers-launcher.sh closes the cold-cache
30s AbortSignal timeout that killed both dispatchers' first cycle.

B2 — per-tier worker timeout
(IMPLEMENTER_DISPATCHER_WORKER_TIMEOUT_TIER_{N}_SECONDS) lets Tier 1
(qwen-large) and Tier 2 (kimi) get more wallclock than gpt-5-mini;
floor 60s.

B3 — _rebuild_prompt_from_cached_result skips the full prefetch on
tier transitions (worktree-reset puts everything back at the
prefetched head_sha, so the prefetch result + det_sections don't
change). Saves ~7 min per tier transition on comment-heavy PRs.

B4 — git-commit-util.md documents the FORBIDDEN naive recovery
pattern (git fetch && git reset --hard) that lost PR #30 attempt
3's real fix in the live test. Two correct paths now spelled out:
--force-with-lease=<branch>:<old-remote-sha> or stash+rebase+pop.

C2 — _pr_clone._refresh_mirror_with_retry adds one retry on git
fetch failure and force-reclones the bare mirror if both attempts
fail. Previously a single exit 128 logged WARN and continued with
stale data forever.

C3 — in-flight turn markers (asterisk suffix on input/output token
counts) in the per-turn log when completed=False. The archived
turn dict's completed field was already there; the log now surfaces
it. Sub-agent timeout archiving was already correct via
_archive_subagent_tree.

C4 — new module _recent_push_cache.py records per-PR push events
(head_sha + timestamp + cycle metadata). Prefetch surfaces in the
sentinel under recent_implementer_push (with --field accessor)
when the cached push matches the PR's current head_sha within
1h. Prevents the "dispatcher re-cycles right after pushing,
worker re-does the same compliance work" failure mode from
PR #28 cycle 2 in the live test.

C5 (replaces C1) — new module _pr_comments_cache.py wraps
_review_fetch.fetch_pr_comments with disk-backed delta-fetch
semantics. PR #30's 1340+ comment fetch (which previously took
~30s and hit the 20-page pagination cap) now becomes a 5-10 item
delta. Cache is per-PR, shared between reviewer + implementer
dispatchers, has 24h staleness bound, kill-switch via
IMPLEMENTER_DISPATCHER_COMMENT_CACHE_DISABLE=1.

Tests: 1484 passed, 3 skipped (+20 from 49a28b5c). New test files
test_pr_comments_cache.py (16 tests) and test_recent_push_cache.py
(7 tests); new TestPerTierWorkerTimeout class (4 tests).

ISSUES CLOSED: #30 #28

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 10:25:10 -04:00
drew 49a28b5cf5 feat(auto-agents): deterministic compliance short-circuit + push-substrate fixes
Three blocker classes surfaced by the 2026-05-13 4-hour live pipeline
test (PR #30 / #28 / #25), all fixed here:

1. `_pr_clone` left `remote.origin.mirror=true` on the bare mirror,
   making every `git push --force-with-lease origin <refspec>` fail
   with `fatal: --mirror can't be combined with refspecs`. Worktrees
   inherited the bad config; worker had to discover + work around it
   each cycle, and one cycle's fragile recovery LOST a real commit.
   `_disable_mirror_push_semantics` clears the flag at clone time
   and on every refresh.

2. Predicate treated `outcome=resolved + head_sha_advanced=False` as
   transport-class (UNKNOWN bucket → wasted same-tier retry). It's
   actually competence — the worker emitted a complete-looking JSON
   while delivering nothing. Now escalates immediately.

3. The dispatcher's own deterministic sections (compliance_gaps +
   gate_preflight) misled the worker into emitting `resolved`
   whenever both were clean — regardless of real remote CI state.
   New module `_implementer_compliance_apply` + dispatcher hook
   `_maybe_short_circuit` move trivial compliance fixes (CONTRIBUTORS
   line, CHANGELOG stub from PR title, ISSUES CLOSED footer from
   prefetched linked_issues) onto the dispatcher's deterministic
   side per the auto-agents.md policy: "deterministic Python owns
   orchestration; the LLM only handles the actual creative work."

Two short-circuit paths:
- P0 (always-on): compliance clean + preflight clean + remote CI =
  success → skip LLM, emit `no_changes_needed`. Eliminates the
  hallucination class observed live.
- A (opt-in via IMPLEMENTER_DISPATCHER_AUTO_FIX_COMPLIANCE=1):
  compliance has only fixable gaps + preflight clean + CI failing →
  dispatcher applies fixes, pushes, emits `resolved`. Worker
  reserved for code-bug PRs only.

Plus supporting changes:
- gate_preflight payload includes `remote_ci_state` + `diverges_from_remote_ci`
- Telemetry rows gain `outcome_disputed=True` when resolved+no-push
- `EscalationAction.SUCCESS` accepts `no_changes_needed` outcome
- task-implementor.md procedure re-ordered: read `--field ci` FIRST
- Runtime hook `_dispatch_runtime._maybe_read_short_circuit` consumes
  the dispatcher's `_short_circuit_result` stash and synthesizes a
  SessionResult instead of spawning the LLM session
- Cycle archive's post_session_result gains `auto_fix_report` +
  `short_circuit` fields

Tier B (perf) and Tier C (observability) deferred to follow-up.
Tests: 1464 passed, 3 skipped (+23 from 6315892e).

ISSUES CLOSED: #30 #28

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 07:50:00 -04:00
drew 6315892eb8 feat(auto-agents): sentinel-routed deterministic sections + masking hedge
Five rounds of fresh-eyes review against the b154d480 deterministic
worker-side improvements landed four substantive corrections:

- Sentinel handoff for compliance + preflight (workers read off
  disk via implementer_pr_context.py --field; survives tier-agent
  prompt summarisation)
- _GATE_STATUS_RE fixed to match real `## [unit_tests] PASS (12s)`
  output from local_ci_gate.sh (previously zero gates parsed)
- Preflight timeout returns explicit sentinel (-9999) and the
  orchestrator zeros counts so renderer can't show "Persistent
  failures: N" alongside the timeout warning
- outcome_synthesised plumbed per-attempt through phase 4 telemetry

Plus a fresh-eyes catch: check_worktree_clean / check_commit_has_issues_closed
return True on git rc != 0 (deliberate "don't conflate masking with
real gap"), but a fully-masked worktree coincidentally with valid
CHANGELOG + CONTRIBUTORS would have produced a confident "PR resolved"
verdict. New check_compliance_gaps_with_masking + masked_checks plumb
through the sentinel; both renderers (module-level + dispatcher's
pointer) hedge the verdict; worker docs instruct inspection of
masked_checks before exiting resolved.

Two new env flags, both default-ON kill-switches:
- IMPLEMENTER_OUTCOME_SYNTHESIS (=0 reverts to UNKNOWN-bucket)
- IMPLEMENTER_COMPLIANCE_GAPS_ENABLED (=0 disables compliance scan;
  AND-coupled with escalation since compliance is meaningless
  outside the gap-filling flow)

Byte-equivalence holds when all new flags are unset. Test suite
1432 passed, 3 skipped (+~70 new tests across compliance, masking,
synthesis-across-loop, sentinel round-trip, regex fixes, fixtures).

ISSUES CLOSED: #30

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:03:19 -04:00
drew b154d48027 feat(auto-agents): deterministic worker-side improvements
Four changes that move judgment off the LLM and into the dispatcher,
motivated by the live PR #30 escalation pilot (2026-05-12): Tier 0
spent 16 min and gave up on flaky-looking unrelated tests; Tier 2
spent 88 min discovering the PR was already correct and only needed
two compliance entries. Together these collapse the typical
"PR is correct, only needs compliance fixups" case from 88 min on
Kimi to ~5 min on gpt-5-mini at Tier 0.

- Outcome-JSON synthesis in dispatch_implementer
  (_synthesize_outcome_if_missing): synthesises a concrete outcome
  from terminal_state when the worker emits no contract JSON,
  routing the escalation predicate to ESCALATE instead of UNKNOWN.

- Diff-aware gate parser (_diff_aware_gate.py): pure-Python
  classifier that splits failing BDD scenarios into related vs.
  unrelated to the PR's changed files via a feature-stem heuristic.

- Flaky-test pre-flight (_implementer_gate_preflight.py): runs
  local_ci_gate.sh --fast twice and surfaces only the persistent
  failures. Off-by-default behind IMPLEMENTER_DISPATCHER_GATE_PREFLIGHT.

- Compliance gap detector (_implementer_compliance.py): deterministic
  check of CHANGELOG / CONTRIBUTORS / commit-footer / worktree-clean
  state. Result is embedded as a "Compliance gap report" stanza so
  the agent fills in known gaps instead of discovering them.

Both prompt stanzas are appended via _append_deterministic_stanzas,
flag-gated, skipped in dry-run, and skipped when no preclone exists.
Flag-off path is byte-equivalent to the pre-feature build.

Suite: 1382 passed, 3 skipped (+59 new tests over 4 modules).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:39:39 -04:00
drew d3d66f3726 feat(auto-agents): in-cycle implementer tier escalation 0→1→2
Adds a flag-gated escalation loop to the implementer dispatcher
(`IMPLEMENTER_ESCALATION_ENABLED=1`, default OFF). When the worker
fails in a way the predicate determines escalation can help, the
dispatcher holds the claim, resets the worktree to the prefetched
head_sha, refreshes the TTL via _claim_runtime.claim_pr, applies
the next-tier label, and re-runs the worker at the next tier — all
within the same cycle. Bounded by per-failure-class budgets in
_implementer_escalation.BUDGET_PER_FAILURE_CLASS.

Tier 2 (tier-kimi) is default-ON with a kill-switch flag
(IMPLEMENTER_ESCALATION_TIER2_ENABLED=0). Cross-cycle resumption:
the dispatcher reads auto/last-attempt-tier-N at cycle start and
seeds start_tier = min(N+1, max_tier) so crash recovery skips
known-failed tiers. Worker holds release across the cycle via the
new release_claim_on_exit: false directive — eliminates the
inter-tier claim-absent race window.

Behaviour preservation: flag=0 path is byte-equivalent to the
pre-feature build (worker prompt unchanged, Phase 4 row schema
unchanged, status-comment fingerprint unchanged). Issue work
(new_issue work group) always takes the legacy path even with
the flag on.

Supersedes the cross-cycle-only Phase 5c scheme in
auto-agents-tier-2-3-plan.md (now updated to point at the
new plan doc and the dual-role label semantics).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 12:15:39 -04:00
drew 0150c4fbc0 feat(auto-agents): Tier 1+2 follow-ups from 2026-05-11 post-mortem
R1: bake 20-min bash timeout into quality-gates skill recipes so
the first cold-cache --fast call no longer trips OpenCode's
120 s default (recovered ~2 min that was lost on the 2026-05-11
PR #30 cycle to timeout-and-retry).

R2: plumb subagent_max_depth from _archive_subagent_tree's BFS
walk through SessionResult -> SessionContext -> extract_phase4_
telemetry so the field stops landing as null on real multi-tier
cycles. Distinguishes None (walk failed / unknown) from 0
(measured-flat). +7 behavioural tests, 4 existing tests updated
to consume the new (paths, max_depth) tuple.

R3: throttled operator-visible "worker still in-flight" log
line every 120 s (configurable via
DISPATCHER_HEARTBEAT_LOG_INTERVAL_SECONDS) so a 20-min worker
turn emits ~10 status lines instead of going completely silent
between session-start and session-end. +5 tests covering
env-var override, defaults, garbage-rejection, end-to-end
emission, and short-cycle suppression.

R4: triaged 25 errored steps in fork-local unit_tests --
conclusive finding that they're caused by Rich Console
defaulting to 80-col width in non-TTY CliRunner mode,
truncating asserted column headers. Pre-existing on every
branch, unrelated to auto-agents. Documented in the Tier 2/3
plan so future operators don't re-spend the diagnostic time.

R5: short-circuit tier-dispatcher's estimator call on first
attempts via new optional escalation_tier_hint parameter;
implementation-worker now hard-codes hint=0. Saves ~30-60 s
wall-clock per cycle on the common case (estimator's
recommendation converged on Tier 0 in every observed cycle to
date; sample too small for a confidence interval). Future-proof:
the hint becomes dynamic when the auto/last-attempt-tier-N
label scheme lands. +2 static lint tests pin the contract.

Pre-commit polish (P0/P1/P2 from the consolidated critique):
- Renumber tier-dispatcher CRITICAL rules 6,9,7,8 -> 6,7,8,9
- End-to-end pin tests for the R2 closure-mutation chain
  (subagent_max_depth=1 and =None paths)
- Deterministic time.monotonic mock in heartbeat tests so they
  no longer depend on real-clock timing
- Reject bool from extract_phase4_telemetry's int check (bool
  is subclass of int in Python -- would slip True/False through
  as 1/0)
- Replace d.get("_subagent_depth") or 0 footgun with
  int(d.get(..., 0))
- Document DISPATCHER_HEARTBEAT_LOG_INTERVAL_SECONDS as a
  startup-only knob
- Calibrate the "~95% Tier 0" claim to "Tier 0 in every
  observed cycle, n=1"
- Document the reviewer-side R2 asymmetry (SessionContext
  carries the field; reviewer telemetry sink doesn't emit yet)
- Add R5 rollback procedure
- Trim duplicated R5 rationale prose in implementation-worker.md
- Add reviewer back-compat test for SessionContext with the
  new subagent_max_depth field

Tests: 1238 passed (+3 net-new), 3 skipped. Lint + typecheck
clean. Validated via the local_ci_gate.sh wrapper.
2026-05-12 08:37:01 -04:00
drew 17c59f3f08 fix(auto-agents): per-cycle worker-infra seed to /tmp/local_tools
The previous round (ba52135a) added a tools/local_ci_gate.sh wrapper
and a quality-gates skill whose recipes invoked it via the relative
path "bash tools/local_ci_gate.sh --fast". The recipes presumed the
wrapper would be present in the worker's cwd — but the auto-agents
fork's master branch does NOT carry tools/... scripts (they're pipeline
infra, not application code), so the worker's /tmp clone never had a
wrapper to invoke. The wrapper recipes were therefore dead code:
"command not found" on first invocation.

Three coordinated changes close the gap without polluting the fork
with infra it doesn't need:

1. Per-cycle worker-infra seed step. New _worker_infra_seed.py +
   _worker_infra_manifest.txt under tools/. The implementer
   dispatcher's _prefetch_prompt calls seed_worker_infra() once
   per cycle (non-dry-run only), which copies a 7-file manifest
   from the dmpipeline working tree into /tmp/local_tools/. The
   dest is OUTSIDE any git tree — the worker can never accidentally
   git-add the seeded files into a PR. WORKER_INFRA_DEST_DIR
   overrides for tests; WORKER_INFRA_DISABLE=1 short-circuits to a
   no-op SeedResult(skipped=True). Conftest sets the disable env var
   autouse so the suite stays hermetic.

2. tools/local_ci_gate.sh becomes cwd-aware. REPO_ROOT used to be
   derived from \${BASH_SOURCE[0]} (script-location-relative); under
   the seed model that would resolve to /tmp/local_tools (no
   noxfile.py, no source). Now REPO_ROOT defaults to \$(pwd) with a
   --repo-root <path> override, plus a pre-flight sanity check that
   verifies \${REPO_ROOT}/noxfile.py exists (exits 2 with a clear
   diagnostic naming both the resolved path and the two fixes). The
   .venv/bin/nox lookup now naturally targets the worker's clone's
   venv (typically absent, so falls through to uvx as expected).

3. Allow rules + recipes target /tmp/local_tools/. Every recipe
   in the quality-gates SKILL and in task-implementor.md's
   mid-session validation block, step 0a, step 0b, and step 5
   now uses absolute paths like
   bash /tmp/local_tools/tools/local_ci_gate.sh --fast --repo-root {repo_dir}
   The bash allow rules in task-implementor.md are pinned to the
   same prefix (bash /tmp/local_tools/tools/...,
   python3 /tmp/local_tools/tools/...). --repo-root {repo_dir} is
   mandatory in every gate-call recipe — without it the wrapper
   defaults to \$(pwd) (the OpenCode session cwd, NOT the worker's
   clone).

Also fixes the round-8 D1 bug: the quality-gates SKILL.md's
troubleshooting recipes for lint/unit_tests used
"bash -c \"cd /tmp/<...>/repo && \${NOX_CMD:-uvx --quiet nox} -e <gate>\""
which referenced an unexported shell variable and used a bash -c
shape outside the agent's allowlist. Rewritten to single
"bash /tmp/local_tools/tools/local_ci_gate.sh --gate <name> --repo-root {repo_dir}"
invocations that pass the permission engine and re-use the wrapper's
nox bootstrap.

Test coverage: 28 new tests in test_worker_infra_seed.py
(manifest parsing, seed mechanics, env-var-driven config,
production-manifest binding regression checks for both fork drift
directions) and 4 new tests in test_local_ci_gate.py
(noxfile sanity check fires before pre-flight, --repo-root
redirects venv lookup, nonexistent --repo-root exits 2,
--list does not require a noxfile). 5 existing
test_local_ci_gate.py tests updated to seed a noxfile.py into
their fake repos (required by the new sanity check).
Full auto_agents suite: 1203 passed, 3 skipped. Lint baseline
unchanged (4 pre-existing errors in dispatch_implementer.py,
none from this change).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 17:39:23 -04:00
drew ba52135a66 fix(auto-agents): quality-gate env bootstrap + filesystem-handoff hardening rounds 6-7
Two workstreams in one commit:

1. Quality-gate environment bootstrap + new quality-gates skill (round 8).
   The Tier 0 task-implementor was reporting "Failed — nox not available"
   on every PR because the worker's /tmp throwaway clone had no Python
   tooling. tools/local_ci_gate.sh now self-bootstraps nox via a three-
   step resolution chain (system PATH → project venv → uvx fallback)
   and exits 2 with an actionable diagnostic if none resolves. New
   .opencode/skills/quality-gates/SKILL.md gives the implementer worker
   a deterministic recipe + troubleshooting appendix. task-implementor.md
   adds the matching bash allow-rules (bash tools/local_ci_gate.sh *,
   uvx --quiet nox *) plus the skill on its allowlist; step 5 of the
   procedure now references the skill. 9 new unit tests in
   test_local_ci_gate.py pin the three-step resolution chain, the
   bad-shape error paths, and the "system-nox failure does not fall
   through to uvx" invariant. Verified end-to-end against a fresh /tmp
   clone with PATH restricted to system + uvx: both lint and typecheck
   gates ran cleanly via uvx fallback.

2. Filesystem-handoff hardening rounds 6 and 7 (P1/P2 follow-ups from
   the iterative critique loop). Round 6 propagated the round-5
   single-source-of-truth pattern to the sentinel writer
   (_to_dict overlays COMPLETION_FLAG_NAMES) and the worker reader
   (--field metadata introspects payload keys), added the bidirectional
   drift guard test (tuple ↔ dataclass set-equality), and corrected
   round-5 CHANGELOG wording. Round 7 added defence-in-depth: an
   assert-based schema-base collision guard in _to_dict, schema-lock
   tests pinning the writer's full output key set, writer-side typo
   guard, and a reader self-adapts test for unknown future flags.

Test results: 1,170 passing / 3 skipped (+10 vs the round-5 baseline,
+9 in this round). Lint: 11 pre-existing errors across the changed
files, unchanged baseline (corrects the round-6/7 entries' aspirational
"7 pre-existing" claim — empirically the _to_dict refactor cleaned up
zero lints).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 12:29:53 -04:00
drew 0b657cd0d9 fix(auto-agents): three-case contract, work_type dispatch, hardening for filesystem handoff
Post-commit review of d386ff4e surfaced two real bugs and several
rough edges. None changed the architecture — all changes harden the
existing dispatcher↔worker filesystem-handshake contract.

P0 bug fixes
- Three-case read contract for implementer_pr_context.py. The old
  ``or None`` projection conflated "field missing" with "field
  present but empty," forcing the worker to re-curl Forgejo every
  time the dispatcher had already confirmed a section was empty.
  New contract: empty stdout = "didn't try; fall through to legacy
  GET"; ``null\n`` = "tried and authoritatively empty; SKIP GET";
  any other content = use it.
- ``comments`` field dispatches on ``work_type`` instead of using
  the ``pr_comments or issue_comments`` chain. The old code would
  silently leak ``issue_comments`` from a stale issue context into
  a ``pr_fix`` worker's ``--field comments`` read.
- Every section's projection now honours its ``*_completed`` flag.
  A failed upstream fetch (transient API error) maps to empty
  stdout instead of authoritative empty data.

P1 hardening
- Dropped ``_resolve_branch_for_sha``. The pre-clone path was
  shelling out to ``git for-each-ref --points-at <sha>`` for data
  the dispatcher already had from ``pr_details.head.ref``. Now
  ``prepare_pr_worktree`` takes ``head_ref`` as a kwarg.
- Both writers (PR-context and workspace sentinels) clean up
  their ``.tmp`` orphan files on partial-write / serialisation
  failure.
- Removed the dead ``cleanup`` subcommand from
  tools/implementer_workspace.py — worktree cleanup is the
  dispatcher's job (WorktreeHandle.cleanup); the worker has no
  legitimate reason to rm -rf a worktree mid-session.
- Tightened bash allow-rules in task-implementor.md from
  ``<script> *`` to ``<script> <subcommand> *`` so future
  subcommands require explicit operator review.
- Retired the prompt-vs-sentinel "use either" softener in
  task-implementor.md and the implementer-pr-context SKILL.md.
  The scripts are now documented as the SINGLE SOURCE OF TRUTH.

Test additions
- 5 new dispatcher↔sentinel integration tests in
  test_dispatch_implementer.py: writer call site, new_issue
  work_type mapping, cleanup integration with and without a
  context dict, partial-fetch completion-flag propagation.
- 5 new contract tests in test_implementer_pr_context_cli.py:
  the three-case epic contract, work_type dispatch in both
  directions, failed-fetch fall-through.
- 2 new sentinel writer tests in test_pr_context_sentinel.py:
  ``.tmp`` orphan cleanup paths, real ImplementerPrefetchResult
  round-trip (defends against silent-attribute-miss when fields
  are added to the dataclass).
- ``test_workspace_handoff.py`` integration test now asserts NO
  ``git for-each-ref`` invocation (regression guard for the
  dropped helper).

Full auto_agents suite: 1,128 passed, 3 skipped (was 1,123 before).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 23:18:46 -04:00
drew d386ff4e8b feat(auto-agents): filesystem handoff for implementer pre-clone + pre-fetch
The 2026-05-10 default-ON flip of IMPLEMENTER_DISPATCHER_PREFETCH /
IMPLEMENTER_DISPATCHER_PRECLONE put rich PR context and a pre-cloned
worktree in the wrapper's prompt — but the deep `task` tool chain
(implementation-worker → tier-dispatcher → tier-qwen-med →
task-implementor) re-summarises the prompt at every level, so by the
time task-implementor sees its input only BEGIN_PR_DIFF survives. The
worker still called git-isolator-util (~82 s wasted) and re-issued
Forgejo GETs for data the dispatcher had already fetched.

This change introduces a filesystem-mediated handshake that is immune
to prompt summarisation. The dispatcher writes two sentinel JSON
files per cycle (workspace handoff next to the worktree; PR-context
handoff in /tmp/cleveragents-implementer-handoff/) and the worker
reads them via two new bash-allowed Python scripts. Missing /
malformed / stale sentinels map to empty stdout, which is the
worker's signal to fall through to the legacy GET / git-isolator-util.

New modules:
- tools/_pr_context_sentinel.py: dispatcher-side writer (atomic, with
  200 KB per-field truncation and idempotent delete).
- tools/implementer_workspace.py: worker-side reader CLI with
  `discover` and safety-checked `cleanup` subcommands.
- tools/implementer_pr_context.py: worker-side reader CLI with `read
  --field <name>` for every prefetch field.

Hooked into:
- tools/_pr_clone.py: prepare_pr_worktree writes the workspace
  sentinel after worktree-add succeeds; WorktreeHandle.cleanup
  removes both worktree and sentinel; new _resolve_branch_for_sha
  populates the sentinel's branch field.
- tools/dispatch_implementer.py: _prefetch_prompt writes the
  PR-context sentinel; _cleanup_clone_handle removes it.

Two new skills (.opencode/skills/implementer-workspace,
.opencode/skills/implementer-pr-context) and a rewrite of every
"Pre-fetched section" wording in .opencode/agents/task-implementor.md
to call the skills first and fall back to legacy GET only on empty
stdout.

Tests: 54 new (16 workspace CLI + 21 PR-context CLI + 7 sentinel
writer + 10 _pr_clone integration). Full auto_agents suite: 1,123
passed, 3 skipped (was 1,069 before).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 22:32:28 -04:00
drew 555a3469a7 feat(auto-agents): default-flip prefetch + preclone, teach task-implementor to use them (Phase 4)
Two complementary changes that should compound to ~260-400 s
wallclock reduction per implementer cycle, based on the live
PR #30 post-mortem.

1. tools/dispatch_implementer.py
   - _is_prefetch_enabled / _is_preclone_enabled now default
     to ON when the env var is unset or empty. Only the
     explicit falsy literals (0, false, no, off) opt out;
     this is the Phase 4 acceptance gate documented in
     docs/development/auto-agents-tier-2-3-plan.md § 5b.
   - New _env_falsy_explicit helper joins _env_truthy so the
     unset / empty-string / falsy-literal cases are handled
     symmetrically. Each predicate gained a docstring with
     the full behaviour matrix.

2. .opencode/agents/task-implementor.md
   - New "Pre-fetched context" table at the top of Main task
     listing every section the dispatcher may populate (PR
     description, diff, CI status, comments, reviews, linked
     issues, Epic, pre-cloned working copy) and which
     procedure step it short-circuits.
   - Each numbered step in pr_fix + issue_impl now leads with
     "If ## Pre-fetched ... is in your prompt, use it verbatim
     — skip the GET". The original GET / webfetch /
     git-isolator-util fallback is preserved for the explicit
     opt-out path AND for the transient-failure case where
     the dispatcher attempted the fetch but Forgejo returned
     500 (the section renders an "unavailable" placeholder).
   - This MD update is what makes the default-flip actually
     save wallclock — without it the worker would still
     curl for data already in its prompt.

Test updates:

- test_dispatch_implementer.py::TestPrefetchEnvFlag was
  reworked. The old test_legacy_prompt_when_flag_unset
  asserted the off-by-default behaviour we just inverted;
  it's replaced by two regression guards
  (test_prefetch_enabled_by_default_when_flag_unset +
  test_preclone_enabled_by_default_when_flag_unset) that
  pin the new default explicitly.
- test_falsy_values_disable_prefetch parametrize dropped
  "" (empty string falls through to default-ON, not falsy).
- New test_empty_string_falls_through_to_default pins the
  empty-string-equals-unset contract for both env vars.
- test_clone_section_no_handle_when_flag_disabled now sets
  IMPLEMENTER_DISPATCHER_PRECLONE=0 explicitly instead of
  relying on the unset-default that no longer means "off".
- test_implementer_prompt_snapshot.py autouse fixture
  switched from delenv to setenv("...", "0") so legacy-snapshot
  tests pin to the opt-out path. Tests that exercise the rich
  prompt continue to override to "1".

Full suite: 1062 passed, 3 skipped. Subagent archive walker
landed in dc968481 will capture the complete task-tool tree
from the next dispatcher cycle so we can measure the impact
end-to-end rather than from live-poll snapshots.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 21:30:22 -04:00
drew 355af84fb1 refactor(auto-agents): hard-switch supervisor decommission + implementer parity
Combines the 2026-05-09 hard-switch decommissioning of the LLM
implementation/pr-review supervisors with the Phase 2/3/4/5b
implementer parity work (prefetch + preclone + telemetry + operator-
status comments) and the third/fourth-round critique cleanup.

Removed
- .opencode/agents/implementation-supervisor.md (340 LoC)
- .opencode/agents/pr-review-supervisor.md (348 LoC)
- _dispatch_runtime.assert_no_legacy_supervisor +
  detect_legacy_supervisor_sessions and the SUPERVISOR_TAGS /
  SUPERVISOR_OVERRIDE_ENV plumbing in both dispatchers, along with
  the five supervisor-coexistence tests in test_dispatch_runtime.py
- _watchdog_helpers.parse_truthy_env + watchdog_check.py
  --check-env mode + their dedicated unit tests (the legacy
  DISPATCHERS_RUNNING gate had no callers after the watchdog
  rewrite became unconditional)

Added
- tools/_implementer_prefetch.py — pre-dispatch Forgejo fetches
  (PR/issue body, diff, CI status, comments, reviews, linked
  issues, Epic) per work group
- tools/_implementer_prompt.py — pure-function prompt assembly
  with UNTRUSTED CONTENT fences and shared
  PR_COMPLIANCE_CHECKLIST / OUTPUT_CONTRACT
- tools/_phase4_telemetry.py — extractor + JSONL sink for the
  Phase 4 plan metrics
- tools/_status_comments.py — per-fingerprint operator-status
  comment substrate, namespaced for reviewer + implementer
- _dispatch_runtime.SessionContext dataclass + SIGTERM/SIGINT
  cooperative claim release with synchronous handler
- TestSupervisorAgentsDecommissioned and
  TestAutoAgentsMdIsWatchdogOnly anti-regression lints (glob over
  *supervisor*.md in .opencode/agents/, plus body keyword bans
  and bash allow-list lint)
- pyproject.toml `slow` marker registration for the subprocess
  SIGTERM smoke test
- tests/auto_agents/fixtures/{phase4-acceptance.yaml,
  phase4-session-output-sample.txt}

Rewritten
- .opencode/agents/auto-agents.md from supervisor-fleet manager
  (~545 LoC) to dispatcher heartbeat watchdog (~184 LoC); host
  init system / process manager (systemd / runit / docker) is now
  the explicit restart authority instead of "host-level process
  supervisor"
- AGENTS.md production-launch story (Shells A-D) reflects the
  deterministic-Python orchestration boundary; the bot-identity
  fork-mode paragraph reads from FORGEJO_OWNER / FORGEJO_REPO
  env vars instead of the deleted hard-coded supervisor flags
- tools/launch_fork.sh header documents three host-level entry
  points (dispatchers-launcher.sh, opencode-builder.sh,
  merge_drive.py)
- worker self-descriptions (implementation-worker.md,
  pr-review-worker.md) refer to the dispatcher / merge driver
  instead of the deleted supervisors; session-health-quick-util.md
  and async-agent-util.md treat -SUP-suffixed sessions as
  flag-and-escalate signals

Tests: 1006 passed, 3 skipped, 0 failed under tests/auto_agents/.
Lint: zero new ruff errors on touched files; three pre-existing
errors in tools/_pr_diff.py at lines blamed to 2026-05-07.

Operator note: the only in-process rollback knob for prefetch
issues is IMPLEMENTER_DISPATCHER_PREFETCH=0 (and the matching
IMPLEMENTER_DISPATCHER_PRECLONE=0). Anything beyond that is git
revert of this commit. Residual doc surface in the
auto-agents-system and supervised-workers skill READMEs is
documentation-only; the agent files those READMEs reference no
longer exist.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 16:03:43 -04:00
drew 593d142f6f feat(auto-agents): Tier 2 deterministic review/implementer dispatchers
Replaces the long-running pr-review-supervisor / implementation-
supervisor LLM polling loops with host-level Python dispatchers that
own queueing, claim ownership, watchdogs, and SQLite telemetry. The
LLM workers retain sole responsibility for review judgment and code
generation; Python owns only orchestration. The hard merge invariant
is unaffected — these dispatchers do not touch master.

Driver surface
- _opencode_worker.run_session_blocking — outcome-agnostic OpenCode
  session lifecycle (completed / timeout / transport-error). Used
  directly by reviewer / implementer dispatchers whose workers do
  not emit the conflict-driver JSON exit schema.
  run_worker_blocking is now a thin wrapper that adds the
  conflict-specific JSON-outcome classification.
- _dispatch_runtime — shared Python runtime (work-group polling,
  claim helpers, dispatch loop, telemetry). Pre-checks issue labels
  before claiming and refuses when any auto/claimed-* is already
  present, distinguishing already-claimed vs labels-fetch-failed
  vs claim-failed terminal states. Frozen DispatchConfig.
- dispatch_review.py / dispatch_implementer.py — per-pipeline work
  groups, prompts, and CLIs. Each refuses startup when a competing
  AUTO-REV-SUP / AUTO-IMP-SUP legacy supervisor is live on the
  same OpenCode server (override:
  {REVIEW,IMPLEMENTER}_DISPATCHER_ALLOW_SUPERVISOR_COEXIST=1).
- _loader.py — shared sibling-module loader; replaces the three
  duplicated copies in the dispatcher entry points.

Operational hardening
- run_outer_loop tracks consecutive cycle exceptions against
  cycle_failure_budget (default 5, env-tunable per driver) and
  exits 2 on exhaustion for supervisor-driven restart.
- _sanitize_release_detail strips control bytes and neutralises
  triple-backtick fences before quoting worker raw_response in
  Forgejo claim-release comments.
- scripts/opencode-builder.sh: OPENCODE_BUILDER_SERVER_ONLY=1 keeps
  only the OpenCode HTTP API up so the Python dispatchers own
  queue orchestration without auto-agents running concurrently.

Telemetry
- _forgejo_cache.py schema v4: dispatch_review_cycles,
  dispatch_implementer_cycles. One row per cycle with cycle_id,
  driver, candidates_count, claims_acquired, swept_count,
  processed_count, terminal_state, worker_outcome, session_id,
  worker_wallclock_seconds, raw.
- .opencode/telemetry/server.py wires the new tables into
  /api/cycles?driver=dispatch_review|dispatch_implementer and
  surfaces a composite terminal_state/worker_outcome 24h breakdown
  so dashboards can distinguish session-level vs work-level
  outcomes.

Tests
- 31 new tests in tests/auto_agents/test_dispatch_runtime.py
  covering: candidate priority/dedup, claim/release labels,
  foreign-claim refusal, same-kind-claim refusal,
  labels-fetch-failed terminal state, sanitization, supervisor
  coexistence guard (pass/refuse/override/unreachable-server),
  session timeout / transport-error propagation,
  JSON-vs-no-JSON worker exits, cycle failure budget exit and
  reset, heartbeat cadence, end-to-end --once --dry-run /
  --status CLI smoke, and full prompt-snapshot tests for
  _review_prompt and _implementation_prompt (PR-fix + issue-impl).
- test_telemetry_schema.py asserts schema v4 and the presence of
  the two new dispatcher cycle tables.

338 auto_agents tests pass (was 322 before Tier 2). Conflict driver
regression suite unchanged. Dispatchers run cleanly under
--status / --once --dry-run with no Forgejo or OpenCode HTTP traffic.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-06 23:12:09 -04:00