ae940f45644971d5937b755d2459eceffeb6500a
343 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
91073ef104 |
chore(auto-agents): rebind default + tier-0 to local-claude/claude-haiku-4-5
Both ``.opencode/models/default.txt`` (the agent default model when
no tier slot is specified) and the tier-0 entry in
``.opencode/models/tiers.yaml`` were pointing at OpenAI's GPT-5
family (gpt-5-mini and gpt-5-nano respectively). Rebinds to
``local-claude/claude-haiku-4-5`` to keep the default workload on
the local proxy at the per-million rate:
$1 in / $5 out / $0.10 cached (Haiku 4.5)
versus
$0.25 in / $2.00 out / $0.025 cached (gpt-5-mini)
$0.05 in / $0.40 out / $0.005 cached (gpt-5-nano)
Haiku-4-5 is more expensive per-token but Anthropic's prompt
caching (90% off cached input) typically wins for workloads with
large repeated system prompts — which the auto-agents pipeline
absolutely has. The actual cost/PR comparison will be visible
once a few cycles have run through the now-functional cost
dashboard (telemetry commit
|
||
|
|
b80f5f7c3a |
fix(auto-agents): reviewer data-handling — five live-observed regressions
Five independent bug fixes around the reviewer's data pipeline, all live-observed on 2026-05-17 run-1 (PR #35 / PR-shaped traffic generally). Bundled because they share the same data-correctness intent and one fixture-update covers two of them. 1) **PR diff: distinguish "fetch failed" from "PR has no changes."** ``tools/_pr_diff.py``. The old code conflated both cases into ``unavailable=True``, which forced ``data_complete=False`` and blocked APPROVED verdicts. Live-observed on PR #35 created by the new_issue worker without any code changes — Forgejo returns HTTP 200 + empty body for a head==base PR, and the reviewer was incorrectly told the diff was unavailable. 2) **PR-state cache: refresh body + labels on the unchanged- ``updated_at`` branch.** ``tools/_pr_state_cache.py``. Forgejo label add/remove mutations do NOT bump ``updated_at``, so the warmer's cached PR object would carry stale labels for as long as the PR sat idle. Downstream consumers (cycle-cap, claim sweeps, filter exclusions) would never see them. Cheap fix — same row, two extra columns refreshed. 3) **Comments cache: URL-encode the ``since=`` cursor.** ``tools/_pr_comments_cache.py`` + matching test update. The ``+`` in ``+00:00`` decodes to a space on Forgejo's query- string parser, producing 422 errors. Live-observed on PR #35 run-1: 6 consecutive 422s on the same clean ``+00:00`` cursor before the cache backed off for 30 min. ``urllib.parse.quote`` with ``safe=''`` quotes every non-alphanumeric so ``+`` → ``%2B``, ``:`` → ``%3A``. Test updated to ``unquote`` the captured path before substring-matching. 4) **Reviewer prompt: surface the clone fallback.** ``tools/_review_prompt.py``. Adds an inline note in the pre-fetched diff section explaining the two diff sources (inline-truncated vs pre-cloned worktree) and the ``REVIEW_DISPATCHER_DIFF_MAX_BYTES`` cap. Closes a reviewer- side confusion where the model didn't know it could read source files from disk when the inline diff was truncated. 5) **Reviewer agent contract: truncated diff + clone IS data-complete.** ``.opencode/agents/pr-review-worker.md``. The prior wording said ``truncated=True`` forced ``data_complete= False`` and blocked APPROVED — but with the pre-cloned worktree available, the reviewer DOES have full code access and APPROVED should remain valid. Updated guidance now distinguishes "truncated but clone present" (APPROVED OK) from "truncated AND no clone" (COMMENT / REQUEST_CHANGES only). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a6610a022c |
feat(auto-agents): launcher sidecars for conflict-drive + merge-drive
Two new long-lived sidecars folded into the dispatcher launcher, each gated by an env var for safe enable/disable: **Conflict driver** (``DISPATCHERS_DISABLE_CONFLICT_DRIVE``, default enabled). Watches Forgejo for ``auto/needs-conflict-resolution`` labels, dispatches ``conflict-resolver-worker`` (the new agent; bound to ``local-claude/claude-opus-4-6`` for high-quality rebases) to rebase + resolve conflicts, force-with-leases the result, and clears the label. Decoupled from approval — keeps PRs mergeable so the merge driver doesn't have to wait at approval time. **Merge driver** (``DISPATCHERS_DISABLE_MERGE_DRIVE``, default DISABLED in this test launcher to avoid accidental merges during validation runs). Watches Forgejo for APPROVED PRs without blocking labels, rebases against current master, runs the local CI gate, merges via squash with force-with-lease semantics. Terminal stage of the pipeline. Both sidecars follow the existing warmer pattern: best-effort respawn on failure, no contribution to the dispatcher crash-loop budget, clean shutdown on launcher exit. PRs that get ``auto/needs-conflict-resolution`` would sit forever without the conflict-driver sidecar — the implementer and reviewer don't act on that label. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3f12e4140c |
fix(auto-agents): R3.4 — cycle-cap signature + implementer sees full reviewer record
Two related fixes surfaced by the 2026-05-17 run-5 live observation: 1. **Cycle-cap signature now includes total_reviews count** so the reviewer's COMMENT-only path actually moves the signature. Before: the cap signature was ``sha + (approvals + has_active_RC + has_unaddressed_RC)`` — all three boolean axes ignore COMMENT reviews entirely. After: ``+ total_reviews`` term bumps on EVERY review submission. The ``data_complete=False → COMMENT downgrade`` path the reviewer takes in low-context cycles now reflects in the signature, so the cap stops firing falsely. Without this, run-5 observed 4 fresh PRs hitting count=5 in ~10 minutes — the reviewer was successfully posting reviews every cycle but the cap saw "no change" because COMMENT-only reviews don't bump approvals_count, has_active_RC, or has_unaddressed_RC. 2. **Implementer now sees ALL reviewer feedback**, not just active REQUEST_CHANGES. Before: ``fetch_pr_fix_context`` passed ``include_active_reviews=False`` so failing-CI PRs reached the implementer with zero reviewer data. ``fetch_request_changes_pr_context`` only included active RC reviews — COMMENT-only feedback was invisible in both code paths. After: ``fetch_pr_fix_context`` also includes reviews, AND the fetcher now partitions reviews into TWO buckets — the existing ``request_changes_reviews`` (active blocking RC, unchanged semantic) and a new ``comment_reviews`` field carrying every non-dismissed non-RC review (COMMENT / APPROVE). Both are persisted in the PR-context sentinel and exposed via ``implementer_pr_context.py``'s ``comment_reviews`` field. New prompt section ``## Pre-fetched reviewer comments and approvals`` renders the comment_reviews bucket with author / event / commit / body / inline comments + a postscript marking them as ADVISORY (not blocking, unlike the existing RC section). This closes the architectural gap where the reviewer and implementer pools could work in silos on the same PR — the reviewer's substantive prose feedback now reaches the implementer regardless of which work-group routed it. Files touched: - tools/_pr_classification_cache.py — total_reviews in classify_pr + reactivity composite; docstring updated. - tools/_implementer_prefetch.py — new comment_reviews + comment_reviews_completed fields; fetcher partitions reviews once; fetch_pr_fix_context now includes reviews. - tools/_implementer_prompt.py — _build_comment_reviews_section; wired into prompt assembly between RC and PR-comments sections. - tools/_pr_context_sentinel.py — comment_reviews in _to_dict. - tools/implementer_pr_context.py — comment_reviews accessor for the worker's handoff read path. - tests/auto_agents/test_pr_context_sentinel.py — expected_value_keys + fixture + round-trip test updated. - .opencode/agents/estimator-implementation.md — restored canonical section header levels (####) for downstream test compatibility after R3.1 rewrite. Full auto_agents suite: 2303 passing (+12 from this and adjacent work, none broken). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
62d4e8f07d |
fix(telemetry): cost dashboard now computes real USD totals
Three intertwined bugs caused every Cost-tab row to display \$0 even after the scraper started writing real token data: 1. **Lookup key mismatch.** ``_cost_usd`` looked up bare ``model`` but ``_DEFAULT_PRICES`` was keyed by ``provider/model`` — every priced model silently missed. Fixed by adding ``_price_key`` and a fallback chain: ``provider/model`` → bare ``model`` → ``_unknown``. 2. **SQL grouped by model only.** Same modelID served by two providers (e.g. ``claude-opus-4-6`` via Anthropic direct vs a local proxy) at different rates was conflated into one row. Fixed: ``GROUP BY model, provider`` in ``_api_cost`` + ``provider`` returned in each row. 3. **Math convention mismatch.** ``_cost_usd`` did ``(tokens_in - cached) * in_rate`` assuming ``tokens_in`` was total input. But the scraper records ``tokens_in`` as OpenCode's ``info.tokens.input`` (fresh, non-cached), so ``tokens_in - cached`` went negative whenever cache reads exceeded fresh input — which is the common case with Anthropic prompt caching. Fixed: no subtraction; the three populations bill at their three rates. Pricing seeded for the 8 models the scraper has actually observed (``_DEFAULT_PRICES`` corrected from stale Opus-3 numbers + new entries for the Haiku 4.5 / GPT-5 family / CleverThis HF endpoints): | Provider | Model | in | out | cached_in | |--------------|---------------------------|-------|-------|-----------| | local-claude | claude-opus-4-6 | 5.00 | 25.00 | 0.50 | | local-claude | claude-sonnet-4-6 | 3.00 | 15.00 | 0.30 | | local-claude | claude-haiku-4-5 | 1.00 | 5.00 | 0.10 | | openai | gpt-5 / gpt-5-codex | 1.25 | 10.00 | 0.125 | | openai | gpt-5-mini | 0.25 | 2.00 | 0.025 | | openai | gpt-5-nano | 0.05 | 0.40 | 0.005 | | CleverThis-* | (HF endpoints, advisory) | 0.50 | 1.00 | — | Operators can override without touching code via ``.opencode/telemetry/prices.json`` (added; same keying convention). ``_load_prices`` now skips ``_comment`` / ``_last_updated`` / ``_sources`` metadata keys so docs in the JSON don't pollute the table. Smoke against current ``llm_activity`` (3743 turns, 450 archives): total USD over the lifetime window is now \$118.06, with all 8 models showing as priced. Tests pin all three regressions: provider-qualified lookup, bare-model fallback, no-subtraction math, GROUP BY (model, provider), and the metadata-key filter in ``_load_prices``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
88b9373fa9 |
feat(auto-agents): LLM activity scraper + cost dashboard wiring
Closes the cost-tracking instrumentation gap: the telemetry console's Cost tab read from an empty ``llm_activity`` table because nothing in production wrote to it. The scraper walks the OpenCode session archives that ``_opencode_worker`` already writes (including subagent trees via the BFS-walked ``parentID`` chain) and emits one row per assistant turn. Folded into the existing PR-State Warmer loop so it runs on the same 30s cadence without spinning a new sidecar. Schema (v6): - ``llm_activity`` grows ``session_id`` / ``message_id`` / ``provider`` / ``parent_session_id`` / ``subagent_depth`` columns - Partial UNIQUE INDEX on ``message_id`` makes re-scrapes idempotent - v5→v6 migration ALTER-gated on column existence (safe to re-run) Scraper (``tools/llm_activity_scraper.py``): - Reads ``.dispatcher-logs/sessions/*.json``, one row per assistant turn - Folds reasoning tokens into ``tokens_out`` and cache-write into ``tokens_in`` (preserves raw breakdown in ``raw`` JSON for future cost-calc refinements) - Normalises ``subagent_depth=0`` at top level so dashboards can filter ``WHERE subagent_depth > 0`` cleanly - Batch INSERT OR IGNORE via new ``PipelineCache.upsert_llm_activity_batch`` — one fsync per archive, not per turn Warmer integration: - First tick: full backfill of the archive directory - Subsequent ticks: 1h lookback via ``since=`` filter - Scraper failures are logged and swallowed — PR-state job stays load-bearing and unaffected - ``LLM_ACTIVITY_SCRAPER_DISABLE=1`` env kill switch Renames (mechanical, atomic): - ``tools/_forgejo_cache.py`` → ``tools/_pipeline_cache.py`` - ``ForgejoCache`` class → ``PipelineCache`` - Both reflect the module's broader scope (Forgejo data + pipeline telemetry tables); on-disk filename ``forgejo.sqlite`` and ``FORGEJO_*`` env vars are kept for compatibility Verified end-to-end on real archives: 435 archives → 3595 turns ingested (2873 from subagents) across 8 models / 5 providers / 9 PRs. Re-runs insert 0, dedup 3595. 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 (
|
||
|
|
b8c1e49032 |
feat(auto-agents): R3 prep — generate per-tier task-implementor variants
Prep step for retiring the tier-dispatcher + tier-N wrapper chain (R3, follow-up to R2's implementation-worker retirement at |
||
|
|
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>
|
||
|
|
3d30358333 |
feat(auto-agents): pre-fetch failing-CI job log tails (information-starvation fix)
Workers (reviewer + implementer) historically saw `CI / lint: failure` in the prompt but had no idea WHY. Three implementer cycles on PR #28 (runs 18-20, 2026-05-16) burned their full 30-min worker budget largely on: - ~14 file reads + ~14 bash calls inferring the failure from the diff - `bash curl … /actions/runs/N/jobs` attempts blocked by the bash allowlist - `webfetch` against the same Actions URL — also errored - one ~10-min `ci_run_local_gate` run (a full `nox -s coverage_report`) This adds a single source-of-truth cache that pre-fetches the LAST N chars of every failing job's raw CI log: - **`tools/_ci_logs.py`** — per-(head_sha) on-disk cache; parses Forgejo Actions `target_url` (both `/runs/N/jobs/M` and `/runs/N`-only shapes) to resolve job_ids; fetches `/repos/{owner}/{repo}/actions/jobs/{id}/logs` and keeps the tail. SHAs are immutable so `completed=True` cache entries are good forever; partial fetches stamp exponential backoff (same shape as `_pr_comments_cache`). - Dispatcher pre-fetch (reviewer + implementer): each dispatcher's prefetch coordinator (`_review_prompt.fetch_review_context` + `_implementer_prefetch.prefetch_for_pr_fix`) now calls `_ci_logs.fetch_pr_failure_logs` when CI overall != success. Cache exceptions WARN and serve None (prompt builds with empty section; worker still has `target_url` to follow). - Prompt sections: new `## Pre-fetched CI failure logs` section in both reviewer (`_review_views.build_ci_failure_logs_section` — JSON payload) and implementer (`_implementer_prompt._build_ci_failure_logs_section` — text blocks). The reviewer's `data_complete` aggregate gates on it. - Agent prompts updated: `pr-review-worker.md` line 414 (which already predicted this feature in prose) and `task-implementor.md` step 1 ("Read the CI failure picture FIRST") both now point at the new section and explicitly tell the worker NOT to use `curl` / `webfetch` / `ci_run_local_gate` for log content. Coverage: 26 new tests in `test_ci_logs.py` (parse_run_job_ids, the two URL shapes, cache miss/hit/backoff, log truncation, run-only URL job-id resolution, disabled-mode bypass) + 1 test update in `test_pr_context_sentinel.py` to include the new completion flag. Full auto_agents suite: 2046 passing (+36 vs prior commit, all green). NOTE: the matching `ci_fetch_pr_failure_logs(pr)` MCP wrapper for agents to call ad-hoc is implemented in `tools/mcp_ci_server.py` + covered by `tests/auto_agents/test_mcp_ci_fetch_pr_failure_logs.py`, but both files are currently untracked (Phase 1 work). They'll ride with Phase 1's commit. The dispatcher pre-fetch path here is self-contained and doesn't depend on the MCP wrapper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d52b85f063 |
docs(auto-agents): reviewer-identity separation policy (A5)
Documents the FORGEJO_REVIEWER_PAT / FORGEJO_REVIEWER_USERNAME convention as a load-bearing policy in the auto-agents-system skill. Pairs with the existing assert_reviewer_identity runtime enforcement in dispatch_review.py::load_config and the G5 startup PAT probe in _dispatch_runtime — the convention was always enforced at runtime, but the policy was undocumented and a fresh operator deploying the bot for the first time had no obvious place to learn WHY the reviewer pipeline needs a separate identity. The no-self-approval invariant matters because Forgejo branch protection rejects merges when the author is also the only approver. Misconfiguring the secrets so both pipelines share a PAT silently breaks the merge driver (HTTP 422 from the merge endpoint) in a way that surfaces only at human triage. Refs: docs/development/final-working-harvest-plan.md (A5). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1635229828 |
docs(auto-agents): document model-override-needs-restart footgun (C3)
Replaces the README's optimistic "Changes to a .txt file take effect on the next dispatched session — no OpenCode restart needed" with the actual behaviour: BOTH paths require an OpenCode restart for a .txt edit to change what model OpenCode generates with. The dispatcher passes the resolved model on every POST /session (observability / consistency check), but OpenCode itself re-resolves agent.<name>.model from its startup-cached opencode.json on every generation. Without a restart, the dispatcher logs say one model ran and OpenCode actually ran another — a silent regression invisible from the dispatcher side. This is the same footgun the long comment in _opencode_worker.run_session_blocking documents inline; the README was the missing place where an operator naturally looks before editing a model file. Cross-links the consequence for G11 (estimator-driven adaptive tier selection) and the in-cycle escalation plan — both silently misbehave if a model swap lands without a restart (a Tier 1 escalation would run on the cached Tier 0 model). Refs: docs/development/final-working-harvest-plan.md (C3). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d758ca3529 |
feat(auto-agents): reviewer CI-failure triage procedure (G8)
Adds a deterministic CI-failure triage subsection to pr-review-worker.md. The reviewer's verdict outcome rules already distinguished "CI failing with issues introduced by this PR" (→ REQUEST_CHANGES) from "CI issues already known and not introduced by this PR" (→ may APPROVE), but the prompt gave the worker no procedure for making that determination. Net result: the call was inconsistent across reviewer cycles — different sessions on the same PR could land on different verdicts. The new section gives the worker a five-step method: 1. Identify the failing check's context. 2. Map the context to a code area (lint covers source diffs; e2e covers src/+features/; nightly/flaky covers nothing). 3. Cross-reference against the prefetched diff to determine whether the failure plausibly maps to changed files. 4. When in doubt, REQUEST_CHANGES — the same cost-asymmetry tie- breaker that applies elsewhere in the prompt. 5. Never APPROVE when overall CI is failing AND every per-check failure plausibly maps. The optional Python-side log-tail prefetch is documented as a future enhancement but not built in this commit (the deterministic mapping above is the immediate value). Contract test in test_agent_prompt_contracts.py pins the load- bearing section heading + the four signal phrases so a future prompt simplification cannot silently drop the procedure. Refs: docs/development/final-working-harvest-plan.md (G8). 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> |
||
|
|
f70187ba04 |
feat(auto-agents): forbid "pre-existing / out of scope" punt (G2)
Adds CRITICAL Rule 13 to task-implementor.md, ported from
agents/final-working's Rule 12 and reconciled with dmpipeline's
existing gate_preflight.unrelated guidance. The preflight wording
("unrelated failures are environmental, do NOT bail") could be
misread as licensing a worker to leave a failing gate broken; the
new rule pins the contract explicitly — "do not abort" never means
"leave broken" — and gives the worker three named escalation paths
(fix / dependency issue / outcome: unresolved + reason in attempt
comment) so it has a compliant exit when a failure is genuinely
out of its reach this cycle.
Contract-tested in test_agent_prompt_contracts.py so a future
prompt simplification cannot quietly drop the rule, the
preflight-reconciliation anchor, the three escalation paths, or
the forbidden-phrase list.
Refs: docs/development/final-working-harvest-plan.md (G2).
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> |
||
|
|
42a5dc965e |
chore(auto-agents): task-implementor prompt — cut ~70 lines of bloat + add anti-hallucination guard
Body went 570 → 500 lines (closer to final-working's 476). The cuts
preserve every contract; the addition targets the specific failure
mode runs 9–12 made visible.
Cuts:
- Mid-session self-validation: prose+code-fence walk-through → 4-row
table. Same info density, third the tokens.
- Terminal output (REQUIRED): drop the run-6/N1 postmortem narrative
and the "explanation belongs in the attempt comment" repetition.
Schema + outcomes table + "always emit JSON" rule kept.
- "How to read an environment variable" (19 lines): deleted. Basic
bash trivia — workers running on Haiku/Sonnet/Opus already know
`printenv`. The denied-alternatives list collapsed to a one-line
parenthetical in the env-var precedence note.
- Three-case contract repetition inside pr_fix steps 1–5: hoisted
to a single preamble paragraph above the procedure ("Empty stdout
/ `null\n` / populated JSON" applies to every read step). Step
bodies stop restating the contract per call.
- Example prompt: 31-line fully-spelled-out interpolation → 15-line
skeleton showing the two-level shape.
Add:
- Anti-hallucination rule at the TOP of the `### Main task` section,
explicitly tying `outcome: resolved` to a verifiable push:
"verify with `git log master..HEAD --oneline` before emitting
`resolved`". Run-12 Tier-0 sessions on PR #28 + PR #27 both emitted
`resolved` without pushing; the dispatcher's P8 downgrade caught it,
but the tier budget was already burned. The rule is also baked into
the `resolved` row of the outcomes table.
Test: loosen the prose-locking assertion in test_worker_permissions.py's
`test_terminal_output_contract_documented`. The previous assertion
locked the EXACT phrase "NEVER substitute a prose explanation for the
JSON"; the new one checks the structural invariant ('prose' is named
as the anti-pattern + the consequence — UNKNOWN / always-emit /
no-JSON — appears). Phrasing can evolve; the contract holds.
Out of scope: a pre-existing test failure in
test_opencode_worker_models.py::test_every_md_agent_is_wired_or_in_inheritor_allowlist
is unrelated to this commit (in-flight tier rename from tier-qwen-*
to tier-N has new opencode.json entries but old tier-*.md files still
on disk). Confirmed by stashing my edits — fails identically on HEAD.
Leaving for the operator handling the rename.
Suite: 1635 passed / 3 skipped / 1 pre-existing failure.
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> |
||
|
|
e710963c2a |
fix(auto-agents): Forgejo retry classifier + worker pipe-filter allowlist
Run-7 inspection follow-ups. - N7: list_prs.ts's apiGet retried only on timeout/abort, so a bare HTTP 503 from Forgejo's /pulls endpoint failed a whole review cycle. The classifier now retries transient HTTP failures (5xx + 429) with exponential backoff, matching the Python dispatcher's _claim_runtime.get policy; 4xx still escapes immediately. All 13 list_prs_*.ts scripts share this apiGet. The 2-attempt cap is deliberate — two 60s fetch budgets must fit the 120s subprocess timeout. - N3: the leaf kept piping file inspection through head/tail/sort/uniq and hitting permission denials (none were allowlisted), re-running bare and burning turns. Added them to the worker bash allowlists, bare + `<tool> *` forms (the engine matches each pipeline node independently). task-implementor gets all four — it proved it needs `grep|sort|uniq` and already carries curl */`* /tmp/*`; pr-review-worker gets only head/tail (pure read→stdout), since sort -o / uniq OUTPUT can write a file and the reviewer is read-only by role. - Updated the N3 guidance in task-implementor.md and the quality-gates skill: head/tail are now allowlisted, so "run the gate wrapper bare" is justified by output truncation hiding the failing-gate name, not by denial. Tested: test_worker_permissions.py + test_prompt_heredoc_lint.py pass (93); list_prs.ts retry path verified against live Forgejo. 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>
|
||
|
|
447de90f95 |
docs(auto-agents): explain task-implementor's global read permission
task-implementor.md locks edit/write/external_directory to /tmp/** but leaves read as "*": allow. The asymmetry is deliberate — the worker only mutates its /tmp worktree, but legitimately reads outside it (/tmp/local_tools seed, skill bodies, repo context OpenCode resolves from the agent install path, absolute paths from sentinels). Read is not a mutation risk; constraining it has historically just locked the worker out of its own context. Added a comment so the next reader does not "fix" the asymmetry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
179b420270 |
feat(telemetry): live dispatcher-run observability subsystem
A sidecar + web UI that turns a running 4-hour dispatcher test from
"tail three logs and guess" into a real-time status grid.
tools/live_log_writer.py — sidecar. Tails the implementer + review
dispatcher logs, watches OpenCode session archives + Phase-4
telemetry JSONL, polls host process state, and emits two artifacts:
<run_dir>/events.jsonl — append-only event stream, closed event set
<run_dir>/snapshot.json — rolling status grid, atomically replaced
Restart-safe: replays events.jsonl to rebuild counters, resumes each
tailer from a per-tailer offset file. Offsets are saved BEFORE the
line is handled, so an unclean crash can drop <=1 event but never
double-count.
.opencode/telemetry/{server.py,app.js,index.html,style.css} — the
"Live" tab. server.py serves /runs + /snapshot + /events (the last
with a byte-offset cache so a 3s UI poll is O(new events), not
O(file size)); app.js renders the process/service grids, the
implementer/reviewer cards, and a bounded event feed.
scripts/dispatchers-launcher.sh — wires the sidecar in as a
best-effort child: respawned outside the dispatcher crash-loop
budget, its exit never propagates to the launcher's, with a
rapid-crash backoff floor so a writer that crashes on boot can't
tight-loop.
Hardened through three review rounds (23 fixes) before landing:
partial-read race in the dir watchers, a shared offset-temp-file
race, replay/offset double-count, dead event types, the /events
O(n)-per-poll scan, the app.js↔snapshot field-contract drift, a
respawn tight-loop, and more.
Tests: tests/auto_agents/test_live_log_writer.py — 46 tests covering
every log-pattern handler, replay↔handler counter agreement, atomic
snapshot write, idempotent restart, partial-read resilience, the
/events offset cache, a cross-language app.js↔snapshot contract test,
a schema-doc consistency test, and two end-to-end smoke tests that
run the actual sidecar process. Full tests/auto_agents/ suite: 1548
passed, 3 skipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
706178d54b |
fix(auto-agents): tier-selector pass-through lockdown + local-claude provider
Three coupled fixes from the run-3/4/5 forensic sequence, plus the
local-claude model routing they depend on.
PROVIDER (validated in run-5):
* opencode.json: local-claude provider uses @ai-sdk/anthropic (was
@ai-sdk/openai-compatible). The OpenAI-compat adapter dropped
streaming tool-call args on ~58% of responses — OpenCode saw
bash({}) schema-errors. The native Anthropic SDK parses the
proxy's input_json_delta stream correctly. Run-5 confirmed: tool
args now flow intact (full {command,description} fields observed).
* apiKey reads {env:LOCAL_ANTHROPIC_API_KEY} (not ANTHROPIC_API_KEY)
so a shell with a real Anthropic key set cannot leak it to the
local proxy.
* .opencode/models/tier-{qwen-med,qwen-large,kimi}.txt point at
local-claude/claude-{haiku,sonnet,opus}-4-x. These REQUIRE the
provider block above — committed together to avoid a non-bootable
intermediate state.
TIER-SELECTOR LOCKDOWN (applied, unvalidated — see below):
* tier-{qwen-med,qwen-large,kimi}.md: every tool except `task` is
now denied at the permission-engine level. Run-5 showed an agentic
model (claude-haiku-4-5) given Read/Grep/Edit/Bash will do the
implementer work *inside the tier selector*, bypassing
task-implementor and thrashing against the selector's restrictive
perms (20+ denials in one Tier 0 cycle; task-implementor never
spawned). gpt-5-mini honoured the "pure pass-through" prompt;
Claude-family models do not. The fix makes pass-through
structural, not prompt-dependent. The 60-line alphabet-kludge
edit block is also gone.
TASK-IMPLEMENTOR PERMISSION ORDERING (applied, unvalidated):
* task-implementor.md: edit/write/external_directory rules reordered
so specific allows precede `*: deny` — the OpenCode permission
engine evaluates path-perms first-match-wins (confirmed empirically
from run-4's denial dump; no OpenCode docs exist for this). Path
globs widened /tmp/* -> /tmp/** since the worktree path is
multi-segment under /tmp/. This is what was blocking edit/write on
the dispatcher's pre-cloned worktree.
VALIDATION STATUS: the provider swap is confirmed by run-5 observation.
The two permission changes are UNVALIDATED — run-5 never reached
task-implementor because the tier selector consumed the whole cycle.
The tier-selector lockdown is precisely what unblocks reaching
task-implementor, so the next run validates both at once.
TEST: tests/auto_agents/test_worker_permissions.py gains
TestTierSelectorPassThroughPermissions — 10 tests pinning that all
work-tools are denied, only `task: task-*` is allowed, the bash block
has zero allow rules, and the three selector files stay byte-identical
(they are maintained as a unit; copy-paste drift is the failure mode).
Full suite: 25/25 in test_worker_permissions.py, 1499 passing in
tests/auto_agents/ (3 pre-existing failures in test_pr_comments_cache.py
are a date-rollover time-bomb unrelated to this change).
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
|
||
|
|
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. |
||
|
|
66aace7cd0 |
feat(auto-agents): local_ci_gate.sh pass-through + uvx nox fallback
Closes the gap discovered in the 2026-05-11 implementer cycle: after
the wrapper-driven --fast / --gate paths PASS lint+typecheck and FAIL
unit_tests, the worker fell off the wrapper to a bare `nox -s ...`
invocation that failed at runtime ("nox: command not found") because
the worker's fresh /tmp clone has no nox on PATH.
Three coordinated changes:
1. Wrapper -- POSARGS pass-through. tools/local_ci_gate.sh now
accepts an optional `--` separator after the gate flags; the
rest is forwarded to nox as `nox -e <gate> -- "$@"` and lands
in session.posargs. Required precondition: --gate <name> MUST
be set (the wrapper rejects --fast/no-mode + posargs at
arg-parse time with exit 2 and a corrective diagnostic). Empty
pass-through is a deliberate no-op.
2. Noxfile predicate widened. The unit_tests and coverage_report
sessions used arg.endswith(".feature") which missed the
features/X.feature:LINE scenario-specifier form — behave would
then receive BOTH the targeted scenario AND the default
features/ tree, defeating the bisection. Extracted as a
module-level _has_feature_files() helper accepting either
shape, wired into both call sites, and pinned with 11 cases in
the new tests/auto_agents/test_noxfile_predicates.py (9
behavioural + 2 AST-based wiring guards that fail if the old
inline predicate resurfaces).
3. uvx nox fallback documented. .opencode/skills/quality-gates/
SKILL.md adds a "Targeted-debug fallback" recipe for the
non-gate edge cases the wrapper doesn't model; the agent's
allow list adds `uvx nox *` (without --quiet) so the worker
can invoke uvx-nox directly when needed.
Test infrastructure: _stub_executable now records argv with
per-arg angle-bracketing (`printf '<%s>' "$@"`) so the log
preserves argument boundaries — a previous shape using `"$*"`
space-joined the argv and could not distinguish `-k 'login and
not slow'` (one arg) from `-k login and not slow` (four args).
Test count: 1220 passed, 3 skipped (+17 net-new: 6 wrapper
pass-through cases in test_local_ci_gate.py, 11 cases in
test_noxfile_predicates.py — 9 behavioural + 2 static wiring
guards). Lint baseline on the touched files is clean.
Validated end-to-end by a 2026-05-11 implementer cycle on PR
#30: worker used /tmp/local_tools/tools/local_ci_gate.sh --fast
via uvx fallback, lint+typecheck PASSed, unit_tests reached
the gate (terminated by bash-tool's 120s timeout on cold-cache
behave-parallel; worker self-corrected to 20-min timeout on
retry and the gate completed normally with cached venvs).
Worker correctly diagnosed the failures as unrelated to the PR
change and refused to push.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
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
|
||
|
|
dc96848174 |
feat(auto-agents): archive entire task-tool subagent tree before DELETE root
Before this change the dispatcher archived only the top-level
wrapper session. The entire ``task``-tool subagent chain
(tier-dispatcher → estimator-implementation / tier-qwen-med →
task-implementor → git-isolator-util) was opaque the moment
the dispatcher's DELETE /session/{id} fired, so post-mortem
analysis of an implementer run was limited to whatever
live-API polling we'd done DURING the run. That's how the
recent optimization round had to work from two cherry-picked
live snapshots of task-implementor and git-isolator-util —
unreliable, only what happened to be active when polled.
Three changes:
1. tools/_opencode_worker.py
- New helpers: _walk_subagent_descendants (BFS over
GET /session keyed on parentID), _extract_subagent_agent_name
(parses OpenCode's "(@<agent> subagent)" title convention),
_ms_to_iso (epoch-ms to ISO-8601), and _archive_subagent_tree
(best-effort walk + fetch + write driver; never raises).
- _archive_session / _build_archive_payload gain optional
parent_session_id / subagent_title / subagent_depth kwargs.
When set, the filename includes a ``sub<depth>`` infix
(e.g. 2026-...__sub01__AUTO-IMP-PR-30__tier-dispatcher__ses_*.json)
so a directory listing groups every session from one
dispatcher cycle and reads top-down in BFS order.
- Archive schema bumped from v1 → v2. New fields are nullable;
v1 readers (the existing telemetry-console endpoints) treat
them as missing and remain forward-compatible.
- run_session_blocking's finally block calls
_archive_subagent_tree after the root archive write and
before the root DELETE. Both calls are wrapped in
try/except so a subagent-walk failure can never mask the
worker outcome or stop the dispatcher from cleaning up.
- The dispatcher's existing redact_values list (the Forgejo
PAT) propagates into every subagent archive too, so a
``git clone https://${PAT}@...`` in git-isolator-util's
bash history is masked the same way the wrapper's prompt is.
2. .opencode/telemetry/server.py
- _api_archived_sessions listing endpoint now surfaces the
three v2 fields (schema_version, parent_session_id,
subagent_title, subagent_depth) in each row payload so a
future UI render can nest subagents under their wrapper.
Additive — existing row keys are preserved.
3. tests/auto_agents/test_opencode_worker_observability.py
- 17 new tests across four classes:
- TestSubagentTitleExtraction (5): title parser edge cases
- TestWalkSubagentDescendants (6): BFS order, depth
annotation, transport-error / malformed-payload paths,
cycle safety
- TestArchiveSubagentTree (5): end-to-end orchestration
including a redaction-propagation test that asserts a
PAT inside a subagent's bash tool input is replaced
with <REDACTED>
- TestEndToEndSubagentArchive (1): drives the full
run_session_blocking lifecycle with a wired subagent
descendant and asserts BOTH archives land on disk
- Existing schema-version assertion updated to v2 + three
new ``None``-on-top-level field assertions.
- Two manually-wired archive tests (transport-error,
timeout) now wire GET /session so the walker doesn't emit
a spurious warning.
Total auto_agents suite: 1061 passed, 3 skipped (up from 1044).
This is the prerequisite for trustworthy quantification of the
upcoming default-flip of IMPLEMENTER_DISPATCHER_PREFETCH=1 and
IMPLEMENTER_DISPATCHER_PRECLONE=1. With the walker in place,
every cycle now leaves a complete trace on disk that a human
can read bottom-up months later.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
8af5924db2 |
fix(auto-agents): workspace-isolate skill — bare /tmp paths to avoid permission denial
First live run of workspace-isolate showed git-isolator-util
loading the skill correctly but burning one wasted turn on
`mkdir -p "/tmp/task-implementor-pr10909"` (quoted) — the
agent's allow rule is `mkdir -p /tmp/*` and the OpenCode AST
matcher includes the literal quote characters in the matched
text, so the quoted form doesn't match the bare glob.
Three changes to eliminate the recurrence:
1. workspace-isolate SKILL.md: every operation now spells out
the bare-path rule for the `/tmp/...` argument. Added a
WRONG / RIGHT example pair on the mkdir step ("CRITICAL —
do NOT add surrounding double quotes to the /tmp/... path"),
plus follow-up notes on the clone destination, git -C path,
and rm -rf path. Removed the `${RANDOM}` form from the
recommended pattern — prompt-derived suffixes
({agent_name}-{pr_number}) are deterministic across all
four steps without needing to read back the expanded path.
2. git-isolator-util.md: added a "most common cause of wasted
turns" callout in the summary block right above the
operation table — concentrating the bare-path rule so it's
visible without loading the skill text in full.
3. CHANGELOG entry under [Unreleased] § Changed documenting
both this fix and the earlier "tighten implementation-worker
skill allowlist" change. Honest reporting: the
implementation-worker tightening (force implementer-cycle
over auto-agents-system) yields a determinism win but not
a measurable wallclock win — turn 1 input tokens are
dominated by the system prompt + agent definition, not by
the skill body. Future optimisation should focus on the
tier-dispatcher subagent which accounts for >80% of
wallclock.
Tests: 1044 passed, 3 skipped (no test additions — this is a
skill / prompt text change only).
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
2d6d8afb0e |
refactor(auto-agents): force implementation-worker to use implementer-cycle skill only
Post-P0 rerun showed the wrapper still loading auto-agents-system (29 KB, ~75 s on turn 1) when implementer-cycle (4 KB) would have done. Three changes to push the model to the smaller skill: 1. Remove auto-agents-system from the wrapper's skill allowlist. If the model attempts to load it, the permission engine denies the call and the model falls back to implementer-cycle (or to the inline commands, which are self-contained). 2. New "Step 0" at the very top of the Startup section: load exactly one skill, implementer-cycle. Explicit do-NOT list for auto-agents-system, cleveragents-contributing, forgejo-api. The disambiguation is critical: claim_pr.ts lives under .opencode/skills/auto-agents-system/scripts/ as a file path, but `npx tsx` runs it directly from disk — the wrapper does NOT need to load the skill of the same name. 3. Update step 2 (env-fallback) to call out the new prompt credentials section explicitly: if the prompt has the "## Worker credentials" block, use those values verbatim and skip printenv entirely. The instruction was previously buried in the Fallback table further down the file. Also collapse the multi-line backslash-continued claim_pr.ts examples to single physical lines — bash-commands.md § Hard rules rule 1 documents that backslash-newline inside a single command's arg list is denied. The previous formatting (multi-line example in the .md, model writing single-line in practice) was inconsistent and confusing. Expected next-run improvement: ~70 s saved on turn 1 (smaller skill load: implementer-cycle 4 KB vs auto-agents-system 29 KB), plus token-cost reduction on every subsequent turn that referenced the loaded skill content. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
90d5c74993 |
feat(auto-agents): workspace-isolate skill — deterministic clone/branch recipe for git-isolator-util
P0-3 follow-up. The post-P0 implementer rerun (PR #30, 569s vs 985s — 42% faster) revealed a residual wasted-turn pattern in git-isolator-util: the agent burnt two turns trying chained bash variants (`WORK_DIR="..." && mkdir -p ... && git clone ...` denied because the inline `$(date +%s)` extracts a `date` command node that is NOT in the agent's allowlist; `mkdir && git clone` denied for the same chain-extraction reason). The existing inline bash blocks in git-isolator-util.md were the seed — they used $(date +%s) and chained statements that look sensible but trip the OpenCode permission engine. This commit replaces the inline bash blocks with a pointer to a new skill at .opencode/skills/workspace-isolate/SKILL.md that provides: - One bash call per step, never chained. - Deterministic suffix pattern (`/tmp/{agent_name}-{pr_number}`) so the same repo_dir threads through every step without re-querying the shell. No $(date +%s), no ${RANDOM} drift across steps. - The exact recipe for `isolate` (4 calls), `setup_branch` (existing branch: 3 calls; new branch: 4 calls), and `cleanup` (1 call). - Hard rules against $(...) command substitution, multi-line continuations, and heredocs — with explicit reasoning so a future agent that tries to "improve" the recipe immediately sees why it must not. git-isolator-util.md now references the skill, drops its inline bash blocks, and adds `skill.workspace-isolate: allow`. The expected next-run improvement: -30s shaved off git-isolator-util (two failed bash attempts eliminated). Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
f27cf1e017 |
feat(auto-agents): prefetch worker credentials in prompt + redact PAT in archives + implementer-cycle skill (P0-1, P0-3)
P0-1: the implementer dispatcher now embeds forgejo_pat / git_user_name / git_user_email directly into the worker prompt under a new "## Worker credentials (use these instead of env vars)" block. Live-test post-mortem of the 2026-05-10 implementer run showed the worker burning 245 s across 3 turns probing for env vars (printf denied -> printenv ... || true denied -> printenv ... succeeded); with values inline those turns disappear entirely. The dispatcher additionally passes redact_values=[cfg.token] to run_session_blocking so every occurrence of the PAT is replaced with <REDACTED> in the on-disk session archive (prompt body, tool input.command, any nested error string). Minimum redact-length floor of 12 chars prevents accidental archive mangling when a caller passes too-short credentials. Both reviewer and implementer pipelines benefit. P0-3: new .opencode/skills/implementer-cycle/SKILL.md is a 130-line cheat sheet that replaces the heavier auto-agents-system skill load on the implementation-worker's claim/dispatch/release path. The worker .md inlines the full `npx --yes tsx ... claim_pr.ts ...` one-liners so the skill load is informational, not load-bearing. 15 new tests: 10 cover redaction unit/integration paths (multi-occurrence, multi-secret, short-value warning, negative control, prompt+tool-input end-to-end); 5 cover the credentials section (presence/absence/partial/empty/canonical-order). 1043 auto_agents tests pass / 3 skipped. Forward-looking expectation: ~245 s/4 min saved per implementer cycle + measurable input-token reduction. Will be re-measured against the next live dispatch_implementer --once run. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
132a5a2269 |
feat(auto-agents): centralise model registry under .opencode/models with session-create model stamp
Every agent's model assignment now lives in a single-line text file at
.opencode/models/<name>.txt; default.txt is the 27-agent catch-all.
opencode.json's agent.<name>.model uses
{file:./.opencode/models/<name>.txt} interpolation, and
tools/_opencode_worker.py reads the same files at session-create to
stamp the resolved model on the session record (observability + drift
sentinel; OpenCode does NOT propagate session-level model to
prompt_async — schema for that is undocumented and deferred to Stage
2). 39 .md frontmatter `model:` lines stripped; the two intentional
inheritors (task-implementor, agent-evolution-pool-supervisor) keep
their model-less frontmatter.
Operator workflow for swapping a model is now: edit
.opencode/models/<role>.txt, restart OpenCode so opencode.json's
{file:...} re-resolves, run the dispatcher. Live-swap without restart
was attempted (override on prompt_async); OpenCode 0.x silently
dropped those requests (200 OK, no assistant message) and the
prompt_async override was reverted. The session-create override
remains for observability + drift detection.
End-to-end validation (2026-05-10): dispatch_review.py on PR #25 with
default.txt=openai/gpt-5-mini produced a clean REQUEST_CHANGES review
in 26 s for ~$0.016; dispatch_implementer.py on PR #30 with
tier-qwen-* files remapped to openai/{gpt-5-nano, gpt-5-mini,
gpt-5-codex} ran the full implementation-worker → tier-dispatcher →
estimator-implementation → tier-qwen-med → task-implementor →
git-isolator-util chain in 16 min with model=gpt-5-mini end-to-end.
Also documents the printenv VAR form as the only allowed env-read in
implementation-worker.md and task-implementor.md (live testing
showed the worker burning 2–4 turns on permission-denied
trial-and-error trying printf and echo variants).
Tests: 21 in tests/auto_agents/test_opencode_worker_models.py
(resolver semantics with caplog assertions on every malformed-input
path; session-create body shape; prompt_async body never carries
model; three repo-level invariants — every {file:...} reference
resolves, every .md is wired or in the inheritor allowlist, no .md
has a model: frontmatter). 1027 auto_agents tests pass / 3 skipped.
Note: .opencode/models/default.txt and the three tier-qwen-*.txt
files are committed with their OpenAI swaps in place (gpt-5-mini,
gpt-5-nano, gpt-5-mini, gpt-5-codex respectively) because the
CleverThis HuggingFace endpoints are paused. Revert with `git diff
HEAD~1 -- .opencode/models/*.txt | git apply -R` if/when they come
back.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
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>
|
||
|
|
2f1be34d12 |
feat(auto-agents): implementer parity — verify-invariant verifiers, implementer-helpers skill, watchdog gate, _opencode_worker audit
Closes the four open items in `docs/development/auto-agents-tier-2-3-plan.md` § "Revised remaining scope (2026-05-08)" plus three rounds of fresh-eyes critique fold-in (rounds 3, 5, and post-round-5 polish). Highlights: - New continuous invariant verifiers on a shared `_verify_common.py` substrate: `verify_review_invariant.py` (R1: approval-without-CI) and `verify_implementer_invariant.py` (I1: head-commit fails commit-lint, I2: PR description missing Epic reference). Strictly additive cron-job- shaped scripts that open idempotent `auto/invariant-violation` issues; safe to run every 15 minutes in production. - New `implementer-helpers` skill at `.opencode/skills/implementer-helpers/SKILL.md` + CLI at `tools/implementer_validate.py` (4 subcommands: validate-commit-message, validate-pr-compliance, validate-file-budget, validate-changelog). Mirrors the reviewer side; `tools/_commit_lint.py` is shared so a future change to commit policy updates one place. - `auto-agents.md` watchdog gate: `DISPATCHERS_RUNNING=1` puts the primary orchestrator into watchdog-only mode. Heartbeat resolution + age computation factored into `tools/_watchdog_helpers.py` + the CLI `tools/watchdog_check.py` so the agent only needs `python3 tools/watchdog_check.py *` and `sleep *` bash permissions. The reader honours the env-var override first, then falls back to a freshest-mtime scan across `/var/run` / `$XDG_RUNTIME_DIR` / `/tmp` (deliberately diverging from the dispatcher's first-existing fallback to guard against stale heartbeats from previous root-owned sessions masking healthy user-mode heartbeats). - `_opencode_worker.py` audit: structured `error_kind` classification at every transport-error / timeout return site, plumbed through `_dispatch_runtime.py` into the cycle-log; new `_request_read` retry helper (3 × 0.5s linear backoff, transport-only) wrapping every idempotent read in a worker session so a single transient flap on a polling GET cannot trash a 10-minute worker session. - Static heredoc lint at `tests/auto_agents/test_prompt_heredoc_lint.py` glob-walks every agent prompt and skill recipe markdown, rejecting any heredoc bash recipe in a fenced code block (per `bash-commands.md` rule 2 — heredocs fail at OpenCode's permission-engine parse time). - `bash-commands.md` rule 2 + its fix-it advice both lead with apostrophe-safe `printf "%s" "<body>"` (double-quoted) form; single-quoted form documented as the fragile JSON-only fallback. - `CHANGELOG.md` carries the full multi-round narrative (round 3 CRITICAL/HIGH/MEDIUM/LOW fold-in, round 5 docstring drift + telemetry refactor + broader heredoc lint scope, post-round-5 doc-drift polish). Net delta: +911 passing tests / 3 skipped (was 825 / 3); ruff clean on every new file; pre-existing lint debt in `_dispatch_runtime.py`, `_opencode_worker.py`, `conftest.py`, `_commit_lint.py` unchanged and out of scope for this commit. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
2bc51bfe4c |
chore(reviewer): post-merge polish for review-helpers CLI
Round-3 critique follow-ups: addresses every Should-fix and
Nice-to-have item from the principal-developer / chief-architect /
senior-test-engineer review of the review-helpers CLI.
A1 — Bot identity is now env-overridable via REVIEW_BOT_COMMITTER_EMAIL
so forks / test deployments don't have to monkey-patch
BOT_COMMITTER_EMAIL. Resolution at use site keeps per-test
monkeypatch.setenv flowing through.
A2 — --base-ref argparse default is now None instead of
'origin/master', so an explicit --base-ref origin/master on a
main-default deployment is honoured (previously the env-var
fallback chain silently substituted origin/main).
D1, D2, D3, D5 — dead-code removal: unreachable
worktree/sha check in cmd_lint_commit (argparse already enforces
it); assert -> RuntimeError so it survives python -O; dead
raw_draft='' default; stale 'no worktree AND no diff file'
docstring claim.
D4, D7 — _GitRepo.__iter__ shim and the late dataclass import
deleted alongside the test split (T1 + T3).
D6 — _DIFF_FILE_HEADER_RE / _DIFF_NEW_FILE_RE now accept git's
C-style quoted file headers ('a/path with space'). Covered by
test_validate_inline_handles_quoted_paths_with_spaces.
D8 — _bot_committer_email always returns .lower(); both sides of
the lint_commit_message comparison are lowercased so a future
operator setting the env var to mixed case continues to work.
T1, T3 — 970-line test_review_validate.py split into 744-line
unit suite + 319-line integration suite. GitRepoFixture and
real_git_repo hoisted to conftest along with cli_review_validate
so a future test module can reuse them.
T2 — orphaned T5/T7 comment markers removed.
T4 — test_cli_smoke_validate_draft_rejects locks in the
failure-emit JSON shape via subprocess (catches print routing
regressions the in-process tests miss).
T5, T6, T8 — new tests for git's rc!=0 branch, the
TimeoutExpired bytes-stderr decode branch, and 5 parametrized
degenerate-diff inputs that must produce structured
path-not-in-diff rejections instead of crashing.
Test count: 45 (35 unit + 10 integration), up from 35. Source
line counts under 500-line budget (review_validate.py 458,
_review_validate_helpers.py 499). Full tests/auto_agents suite:
553 passed, 1 skipped (no regressions).
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
ee3142b87a |
refactor(reviewer): round-2 follow-ups for review-helpers CLI
A1 — Typed git-failure surface for diff_from_worktree: DiffResult discriminates empty-output (path genuinely absent → structured path-not-in-diff rejection, exit 0) from three classes of internal git failure (git-timeout / git-not-found / git-error → exit code 2 with error_kind + verbatim git stderr). The worker no longer silently drops inline comments because of a slow disk, missing binary, or typo'd --base-ref. A2 — Env-var fallbacks for --base-ref. _resolve_base_ref consults (in priority order): --base-ref flag, REVIEW_VALIDATE_BASE_REF, FORGEJO_DEFAULT_BRANCH (auto-prefixed with origin/), then origin/master. main-default deployments now work with zero CLI flags because FORGEJO_DEFAULT_BRANCH is already exported. P1–P5 polish: --worktree/--diff-file are now an add_mutually_exclusive_group(required=True); cmd_validate_inline_comment docstring updated to remove the stale origin/master reference; _excerpt_for_field anchors on the literal JSON '\"<field>\":\"<value>\"' form so commit_id excerpts can't collide with bare SHAs in the body; --worktree help text states the validator-internal-failure contract; the _commit_from_worktree indirection's rationale + the deliberate asymmetry with the diff path is documented in its docstring. T1, T2, T3, T4, T5, T7 — test polish: - real_git_repo inherits PATH from os.environ (macOS portability) - real_git_repo docstring documents git ≥ 2.28 requirement - real_git_repo's run helper accepts env_override for per-commit identity changes (e.g. bot-author exemption tests) - factory dict + parametrize replaced with three dedicated smoke tests, one per subcommand - validate-draft smoke test no longer invokes the (unused) real_git_repo fixture (~50ms savings) - new monkeypatched timeout/missing-git/empty-output tests for diff_from_worktree - new real-git-repo bot-author exemption integration test Test count: 35 (up from 25). Full suite: 543 passed, 1 skipped (no regressions). Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
235c07b04b |
feat(reviewer): add review-helpers skill + review_validate CLI
Three host-side Python validators the pr-review-worker invokes via bash mid-session to catch malformed JSON, mis-anchored inline comments, and commit-message lint violations BEFORE emitting its final review verdict. Reduces generative drift on wire-format invariants the LLM was previously left to satisfy from prompt instructions alone. - validate-draft: strict-parses the draft via the dispatcher's post-session parser plus a commit_id == head_sha check Forgejo enforces with HTTP 422. - validate-inline-comment: walks the unified diff to confirm (path, new_position) lands on a +-line. Reads the diff from the pre-clone worktree via configurable --base-ref, falls back to a saved diff file. Unified rejection vocabulary across both diff sources (path-not-in-diff / line-not-added / line-out-of-range). - lint-commit: conventional-commit subject + ISSUES CLOSED footer (head only) + bot-author exemption (advisory; the dispatcher does not enforce these post-session today). Wired into pr-review-worker.md via bash allow-list entry and review-helpers skill allow. Tests cover all three subcommands plus four real-git-repo integration tests against the production --worktree path and three parameterised end-to-end subprocess smoke runs. Co-authored-by: Cursor <cursoragent@cursor.com> |