a21466add232d59cdec1604e09d58ca05659a623
34 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
12286b8a3f |
feat(auto-agents): R3.1 — estimator wrapper, cache, prose refinements
Three follow-ups to the R3 wrapper-chain retirement (
|
||
|
|
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 (
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
24868a1a19 |
fix(auto-agents): perm-layer deny on pre-clone rm + broaden stale-lock cleanup + prompt-contract regression tests
Follow-up to |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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
|
||
|
|
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
|
||
|
|
6315892eb8 |
feat(auto-agents): sentinel-routed deterministic sections + masking hedge
Five rounds of fresh-eyes review against the
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
17c59f3f08 |
fix(auto-agents): per-cycle worker-infra seed to /tmp/local_tools
The previous round (
|
||
|
|
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> |
||
|
|
0b657cd0d9 |
fix(auto-agents): three-case contract, work_type dispatch, hardening for filesystem handoff
Post-commit review of
|
||
|
|
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> |
||
|
|
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
|
||
|
|
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>
|
||
|
|
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>
|