Agents can hand work to agents: dispatch_agent

A schedule is a time trigger, and only the first step of a pipeline is really
waiting on time — every later step waits on the previous step's result. Modeling
"watch a source -> write a spec -> implement it" as three schedules made each fire
blind: on a quiet day the coding agent still spawned, paid a full model run to find
there was nothing to do, and left an empty run in Activity.

So an agent can start the next step itself. `dispatch_agent` (a tool on the bundled
MCP server, and on the pi bridge through the same --call seam) enqueues an ordinary
spawn command in the agent's own project, tagged requested_by=agent:<id> — so a
handoff is visible in Activity with no new surface to build.

- Project-scoped by construction: project_id is read from the spawn environment and
  never from the tool arguments.
- Bounded rather than gated: MAX_DISPATCH_PER_RUN counts the command rows the agent
  already wrote; MAX_DISPATCH_DEPTH rides in the spawn payload and is recovered by
  spawn._dispatch_depth, so a chain keeps its place across a resume and a cycle
  terminates instead of fanning out.
- New scout and planner roles, with built-in skills (handler-scout, handler-planner,
  handler-dispatch) carrying the judgment code can't: dedupe against a memory-note
  watermark, treat "nothing new" as a complete run, and write a task the receiving
  cold-start agent can act on.
- A scout ending on a clean tree skips the test gate and records the new
  tests_status='skipped' (migration 0017, additive CHECK widening). The gate promises
  `done` means tests passed for the work that shipped; nothing shipped.

Rejected a `condition` field on schedules: "is this paper new and does it matter
here?" is a semantic judgment, so it belongs to a model, not a scheduler column. The
scout is the condition; dispatch is how it reports true — one mechanism that covers
future pipelines too.

426 tests (14 new for dispatch, 3 for the gate exemption).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcbDevyMcJWE6qPA56C7mZ
This commit is contained in:
Claude
2026-08-19 22:46:06 +00:00
parent 52167db085
commit 8541b4b7c0
23 changed files with 945 additions and 33 deletions
+38
View File
@@ -244,6 +244,44 @@ The static shell is served **unauthenticated** (it holds no data); the browser p
**Out of scope (additive follow-ups):** an aggregate `GET /projects/{project}/overview` (agents + latest checkmark in one call) to show every agent's checkmark at once; spawning agents / registering projects from the UI (still CLI-driven); shared-context **writes** from the UI (would need the shared-write token — MVP is read-only).
### Dynamic workflows — agent-initiated dispatch
Design decision (operator, 2026-08-19): a recurring **pipeline** is not N schedules. A
schedule is a time trigger, and only the first step of a pipeline is genuinely waiting
on time — every later step is waiting on the previous step's *result*. Scheduling them
independently means each fires blind, and the expensive steps pay a full model run on
quiet days just to discover there is nothing to do.
Rejected: a `condition` field on `schedules`. The conditions that matter here ("is this
paper new, and does it bear on this project?") are semantic judgments, so they belong
to a model, not to a scheduler column. The scout **is** the condition; `dispatch_agent`
is how it reports true. One mechanism then covers every future pipeline rather than
this one.
- [x] `dispatch_agent` on the bundled MCP server (+ pi bridge via the same `--call`
seam): enqueues an ordinary `spawn` command tagged `requested_by = agent:<id>`,
so a handoff is visible in Activity with no new surface. `project_id` is read
from the spawn environment and never from the arguments — project isolation holds
by construction rather than by validation.
- [x] Bounded rather than gated (operator decision: dispatches run immediately, no
approval queue): `max_dispatch_per_run` counted off the command rows the agent
already wrote, and `max_dispatch_depth` carried in the spawn payload and
recovered by `spawn._dispatch_depth`, so a chain keeps its place across a resume
and a cycle terminates. Any agent may dispatch — one general primitive, so a
later chain (a junior splitting off a follow-up) needs no further change.
- [x] `scout` + `planner` roles with built-in skills carrying the judgment code can't:
dedupe against a memory-note watermark (better than a repo file — no checkout, no
commit, and `SessionStart` recall delivers it for free), treat "nothing new" as a
complete run, and write a task the receiving cold-start agent can act on.
- [x] A `scout` on a clean tree skips the test gate (`tests_status = 'skipped'`,
migration `0017_gate_skipped`). The gate promises `done` means tests passed *for
the work that shipped*; nothing shipped. The clean-tree condition is what keeps
that honest.
**Definition of done:** one cheap schedule fires a scout; a quiet run costs one
small-model call and enqueues nothing; a run that finds something dispatches a planner,
which commits a spec and dispatches a junior into the existing forge workflow. 426 tests.
### Phase 4 — Observability (moved back, now optional)
- [ ] Prometheus metrics endpoint on the API (agent counts, pending questions, checkpoint rate)
- [ ] Grafana/Loki wiring documented as an optional add-on for self-hosters who already run that stack — not a dependency for anyone else