Skip to content

subagent-dispatch is declared absent on OpenCode for a reason that cannot be the blocker — OpenCode has a dispatch primitive, and we measured 13 dispatches through it #493

Closed

Description

@YiannisDermitzakis

Claim under challenge

parity.yaml:

- id: subagent-dispatch
  summary: The pipeline's ability to hand a phase to a subagent for isolated execution.
  harnesses:
    opencode:
      state: absent
      scope_note: no isolation-argument dispatch primitive on OpenCode —
                  fr-goal §5's documented fallback runs phases inline;
                  correct behaviour, not a gap to fill

and plugins/super-fr/skills/fr-goal/SKILL.md:91:

OpenCode has no dispatch primitive of its own — phases run inline, which is
correct

Both look wrong, in different ways.

1. OpenCode does have a dispatch primitive, and we have measured it

OpenCode ships built-in subagents (general, explore, scout), invocable
two ways: by @mention, or programmatically through the Task tool
(gated by permission.task).

This is not a docs-reading claim. Our own recorded experiment measured it. From
docs/presentation/version-1/experiment/run-metrics.csv, arm A, harness
opencode --auto (see that experiment's runbook — all arms were OpenCode):

arm sessions subagents cost root minutes
A — fr skills 14 13 $7.59 56.2
B — vanilla 1 0 $1.07 77.6
C — vanilla + plan 1 0 $0.97 105.2

Thirteen child sessions, each with parent_id set and agent = general, titled
(@general subagent). So fr-goal on OpenCode did dispatch subagents, and
the rows are still in the repo.

So SKILL.md:91's "no dispatch primitive of its own" is false as written.

2. The parity scope_note's reason is self-refuting

The note blames the absence of an isolation-argument dispatch primitive. That
cannot be the blocker for this surface, for two independent reasons.

Hermes has no isolation argument either, and is declared enforced.
plugins/super-fr/rules/fr-isolation-required.md:149 says so in as many words:

Harnesses without an isolation argument on their dispatch tool — Hermes'
delegate_task, OpenCode — cannot express the poisoned shape and need no hook.

Both named together as lacking it. One is enforced, the other absent. The
stated discriminator does not discriminate.

The carve-out forbids an isolation argument here anyway. The same rule's
fr-phase-executor section is emphatic that phase executors must not be
dispatched with isolation: "worktree" — fr-goal §6 runs them serially inside
the workspace that already exists, and fr-phase-executor-guard.sh refuses the
combination outright. So this surface requires the absence of an isolation
argument. Citing that absence as the reason it cannot work is backwards.

3. There is a real limitation — just not the stated one

Worth stating precisely, because it is the part that may justify a partial:

  • Custom named subagents appear not to be reliably invocable on OpenCode
    (upstream: anomalyco/opencode#29616 — @name routing does not fire and the
    Task tool's subagent_type exposes only built-ins). So mirroring
    fr-phase-executor by name, with its tool restrictions, may genuinely not
    work today.
  • And we ship nothing to find out: plugins/super-fr/agents/ contains only
    fr-phase-executor.md for Claude Code, and this repo's opencode.json has no
    agent key at all.

Dispatch to the built-in general subagent demonstrably works, which is
what arm A did 13 times. That is context isolation per phase — the thing the
surface is for — minus the tool restrictions.

4. If the real reason is cost, the declaration should say cost

Arm A's 13 subagents cost $7.59 against roughly $1 inline — about 7×. That
is a defensible reason to run phases inline by default, and it may well be the
actual motivation for the 4.5.x change.

But note arm A was also faster in wall clock (56.2 min, vs 77.6 and 105.2 for
the single-session arms), so it is a cost-versus-speed-and-context-isolation
tradeoff, not a plain loss.

Either way: "no primitive exists" and "the primitive is too expensive to use by
default" are very different statements, and only one of them is true. A parity
row is a claim about capability; encoding a cost decision as an absence is how
a capability stops being reconsidered.

What I think closing this looks like

  • Correct SKILL.md:91 — OpenCode has a dispatch primitive (Task tool /
    @mention, built-in subagents).
  • Re-state the subagent-dispatch / opencode row. partial with an
    accurate note looks right: dispatch to a built-in subagent works; a
    named custom agent with tool restrictions does not.
  • Decide explicitly whether inline-by-default is a cost policy. If it
    is, say so in the note and ideally make it selectable, rather than
    describing a capability that exists as one that does not.
  • Reconcile with Hermes, which is enforced while sharing the stated
    blocker. Whichever way this lands, the two should not disagree for a
    reason they have in common.

Why this surfaced

Planning a recorded demo on opencode --auto, the absent row meant the
Extensibility beat could not appear on camera at all. The operator challenged
it, and the challenge holds up: the measured evidence for OpenCode dispatching
subagents is sitting in this repo.

Activity

  1. YiannisDermitzakis commented on Sep 19, 2026

    @YiannisDermitzakis
    ContributorAuthor

    Tested live: custom subagents work. My "narrower real limitation" above is wrong too.

    I wrote that custom named subagents "may not be reliably invocable on OpenCode"
    per upstream anomalyco/opencode#29616. That issue is stale — filed
    2026-05-27, and the behaviour it describes is fixed in the version on this
    machine (opencode 1.18.31).

    Definitions are read — all three documented forms

    Created one agent in each form in a scratch project:

    $ opencode agent list
    build (primary)
    compaction (primary)
    explore (subagent)
    general (subagent)
    plan (primary)
    summary (primary)
    title (primary)
    fr-phase-exec-json (subagent)        # opencode.json  "agent" key
    fr-phase-exec-plural (subagent)      # .opencode/agents/*.md
    fr-phase-exec-singular (subagent)    # .opencode/agent/*.md
    

    And they dispatch — with a real child session

    $ opencode run --model github-copilot/gpt-5.6-terra \
        "Use the task tool to delegate to the fr-phase-exec-plural subagent..."
    • Request acknowledgement  Fr-Phase-Exec-Plural Agent
    ✓ Request acknowledgement  Fr-Phase-Exec-Plural Agent
    The subagent replied: `ACKNOWLEDGED`
    

    Output alone could be the primary agent role-playing, so — the session table:

    id parent agent cost tok in / out
    ses_…b802 <root> build 0.0357 12842 / 66
    ses_…a51d ses_…b802 fr-phase-exec-plural 0.0239 9510 / 8

    A genuine child session, parented, with its own cost and token accounting, and
    the custom agent name recorded in the agent column. Structurally identical
    to the 13 @general rows in arm A, but with a named custom agent.

    So the row is simply wrong

    subagent-dispatch / opencode: absent does not describe this harness. Both
    halves of the surface work today: definition and dispatch. And OpenCode agents
    accept a model: field, so the per-phase model tiering that fr models
    exists for is reachable here too — not just context isolation.

    What implementing it looks like

    Small, and it rides machinery that already exists:

    1. Mirror plugins/super-fr/agents/fr-phase-executor.md into an OpenCode agent
      file, the way scripts/sync-opencode.py already mirrors skills and rules.
      The frontmatters are close but not identical and need a translation:

      Claude OpenCode
      name: the filename
      description: description:
      tools: Read, Edit, Write, Bash, Grep, Glob permission: {edit: allow, bash: allow, …}
      — mode: subagent (required)
      (tier → model via fr models) model:
    2. Add it to REPO_LOCAL_ONLY_*/mirror lists plus a sync tripwire, matching how
      skills and rules are already guarded against drift.

    3. Correct fr-goal SKILL.md:91, which currently tells the model OpenCode has
      no dispatch primitive — so even with an agent defined, the skill would still
      instruct it to run phases inline.

    4. Decide the default explicitly. Dispatching costs more (arm A: $7.59 for 13
      subagents vs ~$1 inline) and buys context isolation plus per-phase model
      tiering, and arm A was also the fastest arm. That is a policy choice worth
      stating as one.

    Point 3 is the one that would otherwise bite: the definition alone changes
    nothing while the prose still says the capability does not exist.

    Verification note

    Every claim above is from the installed binary, not documentation — the same
    posture #486 established. Anyone re-checking should run opencode agent list
    and inspect session.parent_id rather than trusting this comment.

  2. YiannisDermitzakis commented on Sep 21, 2026

    @YiannisDermitzakis
    ContributorAuthor

    Closing — both claims this issue challenged have since been corrected, and the correction predates the issue's own triage.

    Verified on main at 089fb51:

    1. The parity row no longer says absent.

    - id: subagent-dispatch
      harnesses:
        claude-code: {state: enforced}
        opencode: {state: enforced}
        hermes: {state: enforced}

    The self-refuting scope_note — which blamed the lack of an isolation-argument dispatch primitive, a discriminator that did not discriminate because Hermes lacks one too and was declared enforced — is gone entirely. The summary now points at observable state in the run cursor rather than at skill prose being followed:

    Every dispatch is now recorded fr-side per unit — fr run advance opens a DispatchRecord carrying the agent type, the resolved model and the harness that resolved it, fr run claim --agent <id> names the holder in that harness's own id vocabulary, fr run resolve closes it.

    2. The SKILL.md line is gone. plugins/super-fr/skills/fr-goal/SKILL.md no longer contains "no dispatch primitive of its own" or a claim that OpenCode phases run inline.

    Changed by a5cd114 (#495, gh-494), "Ship fr-phase-executor as an OpenCode subagent: the agent, the prose that forbade it, and the row that agreed" — which is this issue's argument, implemented.

    What this issue got right, recorded so it is not lost: the reasoning stands as a worked example of the failure mode fr harness parity exists to catch — a declaration whose stated reason cannot be the real blocker, kept plausible because nothing checked it against the wiring. The measured counter-evidence it cited (13 recorded OpenCode subagent dispatches in the repo's own experiment CSV) was the decisive part.

    What the issue did not cover, and is still open: #530 — on OpenCode the holder of a dispatched unit is unnameable while it holds it, because the task tool blocks and returns the child session id only with the result. That is declared honestly as dispatch-holder-identity: opencode: partial rather than folded into this row, which is the distinction this issue argued for.

    Found during a backlog triage sweep, not by new work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions