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>
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>
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>
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>