Commit Graph

9 Commits

Author SHA1 Message Date
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 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 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 0eb79c881d fix(auto-agents): move review submission into dispatcher (#2 full + safe glob)
Restructures the reviewer pipeline so every Forgejo curl GET / POST that
``pr-review-worker`` used to issue is now handled deterministically by
``dispatch_review.py``. The worker becomes "read pre-fetched data, emit
structured JSON, exit" — no HTTP, no claim_pr.ts, no skill loads beyond
the contributing checklist.

Why: the first end-to-end ``dispatch_review --once`` run against PR #30
spent the whole 30-min budget retrying permission-denied curl POSTs
(multi-line ``-d`` continuations don't match the bash glob, nested
``/tmp`` writes for review bodies don't match ``/tmp/*``, ``python3 -c
"..."`` heredocs don't match anything we allow). Loosening the bash
permissions to fix this would expand the worker's blast radius into
arbitrary code execution; moving the submission into the dispatcher
removes the operations that triggered the denials in the first place.

Changes:

- New module ``tools/_review_pipeline.py`` (six fetchers + linked-issue
  parser + brace-balanced JSON parser + three posters + finalize_review
  orchestrator). All fetchers swallow their own failures so a transient
  Forgejo blip degrades to "Pre-fetched X unavailable" rather than
  burning the cycle-failure budget.
- ``tools/dispatch_review.py`` rewired: ``_fetch_review_context`` runs
  every fetcher in one pre-dispatch pass; ``_review_prompt`` now embeds
  the diff + 5 new pre-fetched data fences + an ``Output contract``
  block describing the JSON the worker must emit; each work group binds
  a ``_build_post_session_action`` closure so ``dispatch_one`` POSTs
  the worker's verdict on its behalf.
- ``WorkGroup.post_session_action`` hook in ``_dispatch_runtime.py``
  (optional, defaults to None — implementer / merge-driver dispatchers
  are unchanged). Exceptions in the action are caught and recorded so
  a buggy hook can't orphan the claim release.
- Defensive Tier 1F enforcement: dispatcher re-fetches the
  REQUEST_CHANGES count after the session and overrides the worker's
  outcome to ``tier_1f_escalation`` when count >= 5, attaching
  ``auto/needs-implementer`` per the tier-dispatcher contract.
- ``pr-review-worker.md`` rewritten to drop every curl GET / POST step,
  drop claim_pr.ts (dispatcher owns the claim lifecycle), drop the
  ``forgejo-api`` and ``auto-agents-system`` skills, switch
  ``webfetch`` from allow to deny, and document the JSON output
  contract.
- ``/tmp/*`` -> ``/tmp/**`` glob fix on the worker's ``external_directory``
  / ``edit`` / ``write`` permissions plus the matching bash patterns
  (``git -C /tmp/**``, ``mkdir /tmp/**``, ``rm -rf /tmp/**``). Strict
  superset of the previous surface; un-breaks nested writes the model
  organically tries.

Tests: +49 unit tests in ``test_review_pipeline.py`` (every fetcher,
the brace-balanced parser, every poster, the orchestrator's terminal-
state gating + dispatcher RC override + parse-failure recovery + poster-
exception swallowing), +5 integration tests in ``test_dispatch_runtime.py``
covering the new ``post_session_action`` hook (default None, fires on
timeout, doesn't fire on already-claimed, exception is caught and
release still runs). Existing prompt tests in ``test_dispatch_review.py``
silence the new fetchers via a stub helper so they remain network-
isolated. Full-prompt snapshot test in ``test_dispatch_runtime.py``
switched from a 4 KB byte-equivalent comparison to structural assertions
(header byte-equivalent + every section in documented order + every
outcome named in the output contract + ASCII-only) — same regression
coverage with far less maintenance cost.

435 passed, 1 skipped in tests/auto_agents/.

Skipped from the parallel proposal: adding ``python3 *`` to the
worker's bash allow. Python is Turing-complete and inherits the
OpenCode server env, so allowing it would expand the worker's
surface from "scoped writes" to "arbitrary code + outbound network
+ home-dir read". That is a containerisation problem, not a
permission-glob fix.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 08:31:05 -04:00
drew 2cbe62a70c fix(auto-agents): F1+F2+F3 telemetry liveness, reviewer perf (A+C), bash rules
Bundles a long-overdue set of fixes that surfaced while watching the
single-PR pipeline test against PR #30 the morning of 2026-05-07.

# F1 — long-worker liveness contract
The dispatcher heartbeat was only refreshed *between* worker sessions.
On a 30-minute review the heartbeat file went stale, and any
heartbeat-watchdog (dispatchers-launcher.sh /
cleveragents-dispatchers.service) would SIGTERM a perfectly-healthy
worker mid-cycle, orphaning the OpenCode session and the
auto/claimed-* lock. ``_opencode_worker.run_session_blocking`` now
accepts an ``on_poll`` callback fired once per status-poll iteration;
``_dispatch_runtime.dispatch_one`` and ``conflict_drive.py`` wire it to
``write_heartbeat(cfg.heartbeat_path)``. Callback exceptions are
logged and swallowed so a transient EROFS on the heartbeat path can
never mask a successful worker completion.

# F2 — in-flight cycle visibility (schema v5)
The ``dispatch_*_cycles`` tables previously only recorded a row at
cycle *end*. While a worker was running, the operator's only signal
was the heartbeat file — and even that became stale (see F1). Schema
bumped to v5: ``ended_at`` is now nullable and ``cycle_id`` carries a
UNIQUE index. ``begin_cycle`` writes the in-flight row at start;
``finish_cycle`` updates it at end. ``run_one_cycle``'s try/finally
guarantees ``finish_cycle`` runs even when ``collect_candidates`` /
``dispatch_one`` raises, so an orphan ``ended_at IS NULL`` can no
longer be stuck forever after a crash.

The v4→v5 migration is now defined in ONE place — a set of helpers in
``_forgejo_cache.py`` (``DISPATCH_CYCLE_TABLES``,
``_dispatch_cycle_create_sql``, ``_dispatch_cycle_index_sqls``,
``migrate_dispatch_cycle_table_to_v5``,
``ensure_dispatch_cycle_schema``). Both
``ForgejoCache._migrate_to_v5_in_flight_rows`` and
``_dispatch_runtime.ensure_cycle_table`` import from there, eliminating
the drift risk of the previous duplicated DDL. Pre-existing rows are
preserved verbatim across the migration.

# F3 — telemetry surface for the new state
``/api/health`` now returns ``in_flight_cycle: {cycle_id, started_at,
session_id, candidates_count, elapsed_s}`` per dispatcher daemon and a
``running_long_worker: bool`` flag (heartbeat older than 600s AND a
matching pid alive — should never fire under healthy F1 operation, so
when it does it points at a real bug). The Drivers and Overview tabs
in ``.opencode/telemetry/{index.html,app.js,style.css}`` render
in-flight rows with a tinted background + "in flight" pill, daemon
tiles get a dashed border for the long-worker state, and each tile
shows the running cycle's id + elapsed time inline.

# Fix A — reasoningEffort high → medium for pr-review-worker
On its own that change alone would not have been enough, but combined
with Fix C below it dropped a representative cycle from "timed out at
30:00" to a target ~2-3min. Pure config change in
``.opencode/agents/pr-review-worker.md``; no code path touched.

# Fix C — pre-fetch PR diff in dispatch_review and embed in prompt
The reviewer used to spawn a ``git-isolator-util`` subagent, which
shelled out to ``git clone``, ``git fetch``, and ``git diff
master...HEAD``. That subagent burned 90+ seconds and several token
budgets per cycle. ``dispatch_review.py`` now fetches the unified diff
via the Forgejo ``/pulls/{n}.diff`` endpoint and embeds it into the
worker prompt under an ``UNTRUSTED CONTENT`` fence with explicit
BEGIN_PR_DIFF / END_PR_DIFF markers, head_sha pinning, character-count
metadata, and END marker redaction to defeat patch-text injection. The
worker is instructed to use the embedded diff and skip the isolator
subagent entirely when it is present. Falls back to the old path on
fetch failure or via the ``REVIEW_DISPATCHER_EMBED_DIFF=0`` env switch.

# Cross-cutting bash rules
The ``pr-review-worker``'s shell tool calls kept hitting
``permission denied`` because OpenCode's permission engine matches
the *raw, unexpanded* command string against allow-globs. Chained
commands (``&&``, ``||``, ``;``, ``|``), command substitution
(``$(...)``), bare variable assignments, multi-line continuations
(``\\\n``), heredocs, and inline ``python3 -c "..."`` strings all
contain characters the permission glob cannot span, and were silently
denied. Added ``.opencode/instructions/bash-commands.md`` (wired into
``opencode.json`` via the ``instructions`` array so it appends to
EVERY agent's system prompt globally), with hard rules + recovery
recipes (``printf > /tmp/file`` instead of heredocs;
``printf > /tmp/script.py`` + ``python3 /tmp/script.py`` instead of
``python3 -c``; ``curl -d @/tmp/body.json`` instead of multi-line
``-d '{...}'``).

# Pre-commit polish (architect/dev/test review)
Surfaced during a chief-architect / principal-developer /
senior-test-engineer code review of the uncommitted change:

- Schema DDL deduplication (described above under F2).
- ``finish_cycle`` INSERT-fallback now preserves ``started_at`` /
  ``driver_name`` when caller provides them; otherwise stamps a
  ``synthetic_started_at: true`` flag in the raw blob so cycle-time
  analytics can exclude rows whose duration was synthesised.
- ``bytes=`` → ``chars=`` in the embedded-diff header. The value is
  ``len(diff_text)`` after ``decode("utf-8")`` — a UTF-8 character
  count, not a byte count. Off-by-multibyte for non-ASCII patches.
- ``scripts/opencode-builder.sh`` mode 644 → 755.
- ``.gitignore`` entries for ``.parked-prs.json`` (runtime state for
  ``tools/park_other_prs.py --restore``) and ``.dispatcher-logs/``
  (append-only local pipeline log directory).

# Tests (381 passed, 1 skipped)
- ``test_opencode_worker.py``: 3 new tests for ``on_poll`` cadence,
  error swallowing, and backwards-compatible default.
- ``test_dispatch_runtime.py``: 7 new tests for ``begin_cycle`` /
  ``finish_cycle`` semantics, the v4→v5 migration with row
  preservation, the crash-safe try/finally path, the
  ``dispatch_one`` → ``run_session_blocking`` ``on_poll`` wiring, and
  the new ``started_at`` / ``driver_name`` plumbing through the
  INSERT-fallback branch.
- ``test_telemetry_server.py``: 4 new tests for ``in_flight_cycle``
  in ``/api/health``, the elapsed-seconds computation, and the
  ``running_long_worker`` flag.
- ``test_telemetry_schema.py``: assertion bumped from v4 → v5 and a
  new test confirming the cycle tables now allow ``ended_at IS NULL``.
- ``test_dispatch_review.py`` (new file): 14 tests for diff fetch
  (happy path, truncation, HTTP/URL errors, END_PR_DIFF redaction,
  Forgejo auth scheme), ``_build_diff_section`` (dry-run, env
  toggle, embedding, fallback), and end-to-end prompt embedding.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 07:12:45 -04:00
drew f89d275650 chore(dispatchers): apply pre-push critique fixes + ship production launcher
Layered polish on top of 593d142f (Tier 2 deterministic dispatchers)
plus the systemd-supervised launcher that realises the SystemExit(2)
restart contract in production.

Code:
- Promote `_opencode_worker.list_sessions(server_url)` to public so the
  coexistence guard no longer reaches into private `_request`.
- `assert_no_legacy_supervisor` truncates with `(showing first N)` and
  takes `Iterable[str]` for supervisor_tags.
- Collapse `_sanitize_release_detail` to a single `str.replace()`
  (substitute has no backticks; second pass was always a no-op).
- Telemetry `_api_cycles` routes through a single `_DISPATCH_TABLES`
  dict so the rows query and breakdown query can never drift onto
  different tables.
- `dispatch_one` docstring now lists `labels-fetch-failed` alongside
  the other terminal states.
- `_loader.load_sibling` raises `ImportError` up front for a missing
  file (was bubbling `FileNotFoundError` from `exec_module`).

Operations:
- `scripts/dispatchers-launcher.sh` supervises both dispatchers in one
  process, restarts on non-zero exit with backoff, enforces a
  per-child crash-loop budget, and forwards SIGTERM cleanly.
- `contrib/systemd/cleveragents-dispatchers.service` wires that into a
  systemd unit with hardening defaults and journalctl visibility.

Tests (350 pass, 1 skipped):
- New `tests/auto_agents/test_loader.py` (4 cases).
- New `tests/auto_agents/test_telemetry_server.py` (5 synthetic-row
  cases covering both tables, composite breakdown, unknown driver,
  table isolation).
- Extended `test_dispatch_runtime.py` with truncation-suffix and
  `list_sessions` direct tests; refit existing supervisor-guard tests
  to monkeypatch the public helper.

Docs:
- AGENTS.md cross-links the launcher / systemd unit.
- CHANGELOG.md entry under [Unreleased] dated 2026-05-07.

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

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

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

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

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

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

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