Commit Graph

343 Commits

Author SHA1 Message Date
drew 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 62d4e8f07).

No code changes — pure configuration. Revert is one-line if the
observed cost profile favours the OpenAI defaults.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 00:18:18 -04:00
drew 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>
2026-05-18 00:18:04 -04:00
drew 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>
2026-05-18 00:05:47 -04:00
drew 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>
2026-05-17 17:00:02 -04:00
drew 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>
2026-05-17 16:47:54 -04:00
drew 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>
2026-05-17 15:53:25 -04:00
drew ee120011c8 feat(auto-agents): R3.2 — estimator cache invalidation on failure + polish
Addresses the P0/P1 + P2 items from the post-R3.1 review pass.

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

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

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

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

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

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

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

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

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

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

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

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

Tests
-----

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Tests
-----

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:03:30 -04:00
drew 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 6e63073ad).
The cutover commit will swap the dispatcher to invoke these variants
directly; this commit adds the variants without behavior change.

What changed:

1. **sync_tier_models.py extended** to generate
   ``task-implementor-tier-{slot}.md`` + matching
   ``.opencode/models/task-implementor-tier-{slot}.txt`` for every
   tier in ``tiers.yaml``. The ``.md`` body is a byte copy of
   ``task-implementor.md`` (the source of truth) prefixed with an
   HTML-comment generation header. The ``.txt`` carries the slot's
   model, identical to the matching tier-N selector's ``.txt``.

2. **opencode.json agent block** gets four new entries (one per
   variant) wiring each ``task-implementor-tier-{slot}`` to its
   ``.opencode/models/<name>.txt`` via the same ``{file:...}``
   interpolation the existing tier-N selectors use. This is the
   one-time refactor edit the generator's docstring already
   documents — operators add/remove ``opencode.json`` agent block
   entries when tiers are added or removed.

3. **Drift-detection tests** in test_tier_model_registry.py for the
   new variants: existence (md + txt), byte-copy invariant against
   the source, model match against the manifest, and opencode.json
   wiring.

The per-tier variants exist because OpenCode resolves a session's
model from ``agent.<name>.model`` at startup — there is no
per-session model override (documented C3 footgun at 1635229828).
Each tier needs a distinct agent name to hit a distinct model slot;
the variants give the dispatcher that handle without going through a
tier-N pass-through agent.

Tests: 2268 auto_agents passing (was 2268 before; +5 new variant
drift tests offset by no regressions). The variants are not yet
invoked by any code path — that's the cutover commit.

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

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

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

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

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

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

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

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

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

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

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

Wiring
------

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

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

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

Tests
-----

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:55:52 -04:00
drew 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>
2026-05-16 15:36:30 -04:00
drew 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>
2026-05-15 20:21:49 -04:00
drew 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>
2026-05-15 20:20:42 -04:00
drew 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>
2026-05-15 20:15:10 -04:00
drew 1dba881a14 feat(auto-agents): re-enable estimator-driven adaptive tier selection (G11, default-OFF)
Three coordinated edits behind IMPLEMENTER_ESTIMATOR_ENABLED:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fixes:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ISSUES CLOSED: #30 #28

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

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

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

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

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

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

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

ISSUES CLOSED: #30 #28

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

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

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

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

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

ISSUES CLOSED: #30

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Test updates:

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 21:30:22 -04:00
drew 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>
2026-05-10 21:21:01 -04:00
drew 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>
2026-05-10 20:53:42 -04:00
drew 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>
2026-05-10 20:30:39 -04:00
drew 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>
2026-05-10 20:24:55 -04:00
drew 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>
2026-05-10 20:07:49 -04:00
drew 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>
2026-05-10 17:23:06 -04:00
drew 355af84fb1 refactor(auto-agents): hard-switch supervisor decommission + implementer parity
Combines the 2026-05-09 hard-switch decommissioning of the LLM
implementation/pr-review supervisors with the Phase 2/3/4/5b
implementer parity work (prefetch + preclone + telemetry + operator-
status comments) and the third/fourth-round critique cleanup.

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

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

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

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 16:03:43 -04:00
drew 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>
2026-05-09 11:48:12 -04:00
drew 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>
2026-05-08 01:33:26 -04:00
drew 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>
2026-05-08 00:54:51 -04:00
drew 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>
2026-05-08 00:36:43 -04:00