Files
synapbus/examples/doc-gardener/start.sh
T
Algis DumbrisandClaude Opus 4.6 3b94fab226 feat(018): real reactor-driven multi-agent doc-gardener flow
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>
2026-04-14 17:09:08 +03:00

152 lines
6.2 KiB
Bash
Executable File

#!/bin/bash
# start.sh — launch an isolated synapbus instance and build the
# docgardener demo driver. Mirrors cold-topic-explainer layout.
#
# Exit codes:
# 0 everything came up
# 1 synapbus failed to start
# 2 admin socket never appeared
# 3 preflight failed
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
PORT="${SYNAPBUS_PORT:-18089}"
DATA_DIR="$SCRIPT_DIR/data"
BIN_DIR="$SCRIPT_DIR/bin"
BIN="$BIN_DIR/synapbus"
DOCGARDENER="$BIN_DIR/docgardener"
SOCKET="$DATA_DIR/synapbus.sock"
PID_FILE="$SCRIPT_DIR/.synapbus.pid"
LOG_FILE="$SCRIPT_DIR/synapbus.log"
cd "$SCRIPT_DIR"
say() { printf '\033[1;36m[start]\033[0m %s\n' "$*"; }
die() { printf '\033[1;31m[start][FAIL]\033[0m %s\n' "$*" >&2; exit "${2:-1}"; }
# --- preflight ---------------------------------------------------------
for cmd in go sqlite3 curl; do
command -v "$cmd" >/dev/null || die "missing required CLI: $cmd" 3
done
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
die "synapbus already running (pid $(cat "$PID_FILE")); run ./stop.sh first"
fi
# --- build -------------------------------------------------------------
say "building synapbus + docgardener binaries..."
mkdir -p "$BIN_DIR"
(cd "$REPO_ROOT" && CGO_ENABLED=0 go build -o "$BIN" ./cmd/synapbus)
(cd "$REPO_ROOT" && CGO_ENABLED=0 go build -o "$DOCGARDENER" ./cmd/docgardener)
# --- fresh data dir ----------------------------------------------------
say "wiping data dir $DATA_DIR"
rm -rf "$DATA_DIR"
mkdir -p "$DATA_DIR"
# --- launch synapbus ---------------------------------------------------
say "starting synapbus on port $PORT"
# Disable the legacy background workers that hold the single-connection
# write pool long enough to wedge interactive sessions. They manage
# features (task-auction expiry, message retention, stalemate reminders)
# that the doc-gardener demo doesn't use, so skipping them here is safe.
export SYNAPBUS_DISABLE_EXPIRY_WORKER=1
export SYNAPBUS_DISABLE_RETENTION_WORKER=1
export SYNAPBUS_DISABLE_STALEMATE_WORKER=1
nohup "$BIN" serve \
--port "$PORT" \
--data "$DATA_DIR" \
> "$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
say "pid $(cat "$PID_FILE") → $LOG_FILE"
# Wait for the admin socket + HTTP to appear.
for i in $(seq 1 100); do
if [ -S "$SOCKET" ]; then break; fi
if ! kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
die "synapbus crashed during boot — see $LOG_FILE" 1
fi
sleep 0.1
done
if [ ! -S "$SOCKET" ]; then
die "admin socket $SOCKET never appeared after 10s" 2
fi
for i in $(seq 1 100); do
if curl -fsS "http://localhost:$PORT/health" >/dev/null 2>&1; then break; fi
sleep 0.1
done
say "synapbus is up"
# --- shorthand for admin calls -----------------------------------------
admin() { "$BIN" --socket "$SOCKET" "$@"; }
# --- user + human agent ------------------------------------------------
say "creating user algis / algis-demo-pw"
admin user create --username algis --password 'algis-demo-pw' --display-name Algis >/dev/null 2>&1 || true
OWNER_ID=$(sqlite3 "$DATA_DIR/synapbus.db" "SELECT id FROM users WHERE username='algis'")
if [ -z "$OWNER_ID" ] || [ "$OWNER_ID" = "1" ]; then
die "failed to resolve algis user id (got '$OWNER_ID')" 3
fi
say "algis user id = $OWNER_ID"
admin agent create --name algis --display-name "Algis (human)" --type human --owner "$OWNER_ID" >/dev/null 2>&1 || true
# --- coordinator agent -------------------------------------------------
# The coordinator is reactive and runs via the subprocess harness as
# `docgardener agent`. The reactor sets up the workdir with message.json
# and env vars; docgardener reads SYNAPBUS_AGENT to dispatch.
say "creating coordinator agent doc-gardener-coordinator"
admin agent create \
--name doc-gardener-coordinator \
--display-name "Doc-gardener Coordinator" \
--type ai \
--owner "$OWNER_ID" >/dev/null 2>&1 || true
# Configure reactive trigger mode, harness_name, local_command,
# harness_config_json, and feature-018 trust columns (config_hash,
# system_prompt, autonomy_tier, tool_scope_json) via sqlite3 — the
# admin CLI doesn't expose the new columns yet.
DG_ABS="$(cd "$SCRIPT_DIR" && pwd)/bin/docgardener"
DB_ABS="$DATA_DIR/synapbus.db"
LOCAL_CMD_JSON="[\"$DG_ABS\",\"agent\",\"--db\",\"$DB_ABS\"]"
# SYNAPBUS_BIN + SYNAPBUS_SOCKET let the coordinator shell out to the
# admin CLI for follow-up DMs (the real MessagingService.Send path
# that fires the reactor dispatcher). SYNAPBUS_AGENT tells docgardener
# which role it's running as.
HARNESS_CFG_JSON="{\"env\":{\"SYNAPBUS_AGENT\":\"doc-gardener-coordinator\",\"SYNAPBUS_BIN\":\"$BIN\",\"SYNAPBUS_SOCKET\":\"$SOCKET\"}}"
COORD_PROMPT='You are the doc-gardener coordinator. Your job is to decompose a high-level goal ("keep docs accurate against the source code") into a tree of sub-tasks, propose specialist agents to carry out the leaf tasks, monitor progress via the goal channel, and iterate. You never act on leaf tasks directly. You communicate via SynapBus DMs.'
COORD_TOOL_SCOPE='["messages:read","messages:send","channels:read","reactions:add","goals:create","tasks:propose_tree","agents:propose"]'
sqlite3 "$DATA_DIR/synapbus.db" <<SQL
UPDATE agents SET
trigger_mode = 'reactive',
cooldown_seconds = 0,
daily_trigger_budget = 50,
max_trigger_depth = 8,
harness_name = 'subprocess',
local_command = '$LOCAL_CMD_JSON',
harness_config_json = '$HARNESS_CFG_JSON',
system_prompt = '$COORD_PROMPT',
autonomy_tier = 'assisted',
tool_scope_json = '$COORD_TOOL_SCOPE',
config_hash = '70a9a06e9595ade4edc527a792e857792d17af9819c80850cf53bea7ff3887ef'
WHERE name = 'doc-gardener-coordinator';
SQL
# --- base channels -----------------------------------------------------
say "ensuring approvals and requests channels"
admin channels create --name approvals --type blackboard --description 'Approval queue' >/dev/null 2>&1 || true
admin channels create --name requests --type blackboard --description 'Resource requests' >/dev/null 2>&1 || true
echo
echo " Web UI: http://localhost:$PORT (login: algis / algis-demo-pw)"
echo " Log: tail -f $LOG_FILE"
echo " Admin socket: $SOCKET"
echo
echo "Next: ./run_task.sh"