Until now the doc-gardener example was a single monolithic
orchestrator binary writing synthetic messages directly to SQLite.
That's now obsolete: the feature runs as a true multi-agent flow
where the SynapBus reactor fires subprocess runs for every DM, each
agent is its own reactive subprocess invocation, and follow-up DMs
go through the real MessagingService.Send → dispatcher path so the
reactor picks them up.
Changes:
- cmd/synapbus/main.go: gate the three legacy background workers
(expiry, retention, stalemate) behind SYNAPBUS_DISABLE_*_WORKER env
flags. These workers manage the legacy channel task-auction /
message retention features the doc-gardener demo doesn't use, but
they held the single-connection write pool long enough to wedge
the whole server for interactive sessions. All three are disabled
in the example's start.sh.
- cmd/docgardener/agent.go (new): the per-agent subprocess entry the
reactor harness invokes for every reactive trigger. Reads
message.json from the workdir, routes by SYNAPBUS_AGENT to either
coordinator-kickoff, coordinator-completion, or specialist-work
logic. Writes prompt.txt + response.txt for harness capture. Uses
the admin socket (`synapbus messages send`) for follow-up DMs so
the real MessagingService.Send path fires the dispatcher.
- cmd/docgardener/main.go: adds `docgardener agent` subcommand, plus
helpers freshAPIKey / bcryptHash / absPath / selfPath used by the
spawn flow.
- examples/doc-gardener/start.sh: provisions user + coordinator
agent + algis human agent + approvals/requests channels; the
coordinator is created with trigger_mode=reactive,
harness_name=subprocess, local_command pointing to docgardener
agent, and harness_config_json.env carrying SYNAPBUS_AGENT,
SYNAPBUS_BIN, SYNAPBUS_SOCKET. Specialists are spawned
dynamically by the coordinator at runtime (not pre-registered),
so the demo exercises dynamic agent spawning end-to-end.
- examples/doc-gardener/run_task.sh: collapsed to a 3-line kickoff
that just DMs the coordinator and polls algis's inbox for the
coordinator's FINAL: reply. Everything else happens via the
reactor.
Verified end-to-end in Chrome on a fresh instance:
- 4 agents registered (coordinator + 3 specialists dynamically
spawned by the coordinator on receipt of the first DM)
- 7 reactive_runs + 6 harness_runs across the goal lifecycle:
algis → coordinator (kickoff, 624ms, builds goal+tree+spawns)
coordinator → docs-scanner (claim task 2)
coordinator → cli-verifier (claim task 3)
coordinator → drift-reporter (claim task 4)
docs-scanner → coordinator (DONE task=2)
cli-verifier → coordinator (DONE task=3)
drift-reporter → coordinator (DONE task=4, coalesced)
- Web UI Agent Runs page shows all 7 runs with the real
"DM from X" trigger lines and correct sender/receiver chain
- Goal ends at status=completed with all 3 leaf tasks at status=done
- Each specialist run posts a real subprocess artifact to the
goal channel (#finding, #verified, #summary) and appends a real
reputation_evidence row keyed by config_hash.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
doc-gardener
End-to-end demo of the dynamic agent spawning feature (spec 018-dynamic-agent-spawning).
A human owner defines a high-level goal ("verify docs.mcpproxy.app against the mcpproxy source code"). A pre-built coordinator meta-agent decomposes the goal into a task tree, proposes spawning specialist sub-agents with capped autonomy, the specialists claim tasks and produce artifacts, and a rich HTML report is generated from the run.
This example exercises the feature's data primitives end-to-end: goal creation with a backing channel, task-tree materialization with denormalized ancestry, config_hash-rooted trust, delegation-cap enforcement, atomic task claim, append-only reputation ledger, cost rollup, HTML rendering from DB state.
Status of the MVP demo
| Piece | Status |
|---|---|
| Goal creation + backing channel | ✅ real |
| Task tree materialization (ancestry snapshots) | ✅ real |
| Atomic optimistic-lock task claim | ✅ real (covered by 50-goroutine race test in internal/goaltasks/) |
| Config-hash computation (deterministic, sensitive to capability changes) | ✅ real (tested in internal/trust/) |
| Delegation cap enforcement (child ≤ parent) | ✅ real (tested in internal/trust/) |
| Append-only reputation ledger with 70 %-of-parent seed and exponential decay | ✅ real (tested in internal/trust/) |
| Cost rollup via recursive CTE | ✅ real (tested in internal/goaltasks/) |
| Rich HTML report (goal / tree / agents / costs / timeline) | ✅ real |
| Secret encryption + scoped env injection | ✅ real (internal/secrets/, tested) |
| Coordinator driven by a real LLM | ❌ deferred — the demo's coordinator logic lives in Go (cmd/docgardener/flow.go); the LLM-in-the-loop path needs MCP tool wiring + reactor integration |
| Specialist subprocess runs via the harness | ❌ deferred — the demo produces synthetic artifacts |
Full MCP tool surface (create_goal, propose_task_tree, propose_agent, claim_task, verify_task, request_resource, list_resources) |
❌ contracts live in specs/018-dynamic-agent-spawning/contracts/mcp-tools.md; wiring is deferred |
Svelte /goals UI |
❌ deferred |
See specs/018-dynamic-agent-spawning/tasks.md for the full phase breakdown and what remains.
Prereqs
- Go 1.25+
sqlite3,curlon$PATH- A free TCP port (default
18089)
Run it
./start.sh # build + launch synapbus on port 18089
./run_task.sh # execute the demo flow
./report.sh # render report.html
./stop.sh # shut down synapbus
run_task.sh can be re-run any number of times against a running instance — each invocation creates a new goal + task tree + reputation evidence, all appended to the ledger.
What happens under the hood
./run_task.sh invokes ./bin/docgardener run which:
- Bootstraps: creates user
algis(passwordalgis-demo-pw), creates theapprovalsandrequestschannels, and materializes the pre-built coordinator agent (doc-gardener-coordinator) with itsconfig_hashcomputed from its system prompt and tool scope. - Creates a goal via
goals.Service.CreateGoal— slugkeep-docs-mcpproxy-app-accurate-against-source, budget$50.00,max_spawn_depth=3. Auto-creates the#goal-...backing channel. - Decomposes the goal into a 4-node task tree (root +
scan-docs+verify-cli+drift-reportleaves) viagoaltasks.Service.CreateTree, which denormalizes the full ancestry onto each child task in a single transaction. - Spawns specialists — three agents (
docs-scanner,cli-verifier,drift-reporter), each one running throughtrust.DelegationCap()to verify its proposed grant does not exceed the coordinator's, then computing a deterministictrust.ConfigHash(...)and seeding its reputation ledger at 70 % of the parent's rolling score viatrust.Ledger.SeedFromParent(). - Atomically claims tasks — each specialist invokes
goaltasks.Service.Claim()which runs the optimistic-lockUPDATE ... WHERE assignee_agent_id IS NULL AND status='approved'pattern. A concurrent-claim race test ininternal/goaltasks/service_test.goverifies exactly-one-winner over 50 goroutine rounds. - Runs specialists — simulated for the v1 demo. Each task:
- transitions
claimed → in_progress → awaiting_verification → done - increments leaf spend (
tokens,dollars_cents) - posts an artifact message (
#finding,#verified,#summary) to the goal channel withmetadata.kind="artifact" - appends a positive evidence row to the reputation ledger with
score_delta=+0.15(auto verifier) or+0.2(command verifier)
- transitions
- Marks the goal completed.
./report.sh then invokes ./bin/docgardener report, which:
- reads the goal id from
.last_goal_id - queries all tasks, agents, reputation, messages, billing codes for that goal
- computes rolling reputation via
trust.Ledger.RollingScore()(exponential decay,half_life_days=30) - builds a recursive task tree + a chronological timeline
- renders
report.html.tmplintoreport.html - opens it in the default browser
Inspect during / after the run
- Web UI: http://localhost:18089 — log in as
algis/algis-demo-pw. The existing channels, messages, and agents views all work on the new data. - DB shell:
sqlite3 ./data/synapbus.db -header -column " SELECT id, title, status, spent_dollars_cents, assignee_agent_id FROM goal_tasks; " - Trust ledger:
sqlite3 ./data/synapbus.db -header -column " SELECT substr(config_hash,1,12) AS hash, score_delta, evidence_ref, created_at FROM reputation_evidence ORDER BY created_at; " - Cost rollup:
sqlite3 ./data/synapbus.db -header -column " SELECT COALESCE(billing_code,''), SUM(spent_tokens), SUM(spent_dollars_cents) FROM goal_tasks GROUP BY billing_code; "
Expected HTML report
report.html contains six sections:
- Header — goal title, status, budget, owner, backing channel
- Spend metrics — total dollars / tokens / agents spawned
- Goal description
- Task tree — recursive, collapsible, status badges, per-task spend, verifier kind
- Spawned agents — each with name,
config_hash(first 12 chars), parent agent, spawn depth, autonomy tier, rolling reputation bar, tool-scope chips, truncated system prompt - Cost breakdown by billing code — per-code task count, tokens, dollars
- Artifacts posted by specialists — the raw
#finding,#verified,#summarymessages - Timeline — every message in the goal channel, chronologically, annotated with actor and kind
Screenshot-equivalent output (minus images):
Doc-gardener run — Keep docs.mcpproxy.app accurate against source
Goal #4 · slug keep-docs-... · owner algis · backing channel #goal-... · [completed]
Spend Tokens Agents spawned
$1.05 6000 4
Task tree
├─ Verify docs.mcpproxy.app against source [approved]
│ ├─ Scan docs for CLI flags [done] $0.45 · 1500 tok · auto
│ ├─ Verify flags exist in mcpproxy binary [done] $0.25 · 2000 tok · auto
│ └─ Produce drift report [done] $0.35 · 2500 tok · command
Spawned agents
• Doc-gardener Coordinator config_hash 70a9a06e9595… root · assisted · rep 80%
• Docs Scanner config_hash a0b5c6538b2d… parent=coordinator · depth 1 · assisted · rep 58%
• CLI Verifier config_hash 47c6839eed73… parent=coordinator · depth 1 · assisted · rep 58%
• Drift Reporter config_hash ceaa7816aa42… parent=coordinator · depth 1 · assisted · rep 59%
Cost breakdown
doc-gardener 1 task 0 tok $0.00
doc-gardener/report 1 task 2500 tok $0.35
doc-gardener/scan 1 task 1500 tok $0.45
doc-gardener/verify 1 task 2000 tok $0.25
Tests for the primitives
The feature ships with passing test suites for every critical invariant:
go test ./internal/goals/... ./internal/goaltasks/... ./internal/trust/... ./internal/secrets/...
internal/goaltasks/service_test.goTestCreateTree_AncestryAndDepth— recursive tree build with correct depth + ancestryTestCreateTree_AncestryOverflow— 16 KB cap enforcementTestClaimAtomic_Race— 50 rounds × 2 racing goroutines, exactly one winner per roundTestRollupCosts— recursive CTE over 4-level treeTestTransition_StateMachine— legal and illegal transitions
internal/trust/config_hash_test.go— determinism under shuffled inputs, sensitivity to capability changesinternal/trust/delegation_test.go— full tier-matrix + tool-scope subset enforcementinternal/trust/ledger_test.go— exponential decay, 70%-of-parent seed, clampinginternal/secrets/store_test.go— NaCl roundtrip, scope precedence, name sanitization
Troubleshooting
| Symptom | Fix |
|---|---|
./start.sh fails at "admin socket never appeared" |
Another instance on port 18089 — set SYNAPBUS_PORT=18090 ./start.sh |
./run_task.sh fails with "DB not found" |
./start.sh hasn't run — run it first |
| Report page is empty or missing sections | .last_goal_id is stale — rerun ./run_task.sh then ./report.sh |
| Stale binary | rm -rf bin && ./start.sh — forces rebuild |
Next steps (out of scope for this MVP)
The spec at specs/018-dynamic-agent-spawning/ lays out what comes after this demo, including the full MCP tool surface, reactor integration for real subprocess runs, the Svelte /goals page, the resource-request protocol, quarantine on low reputation, and the LLM-driven coordinator. This example establishes that the foundational primitives work; the follow-up work layers on top.