Files
Algis DumbrisandClaude Opus 4.6 42f8256df6 feat(goals): complete_goal MCP tool + draft→active auto-transition
Three improvements that turn the doc-gardener demo from "runs but
stays in 'draft' forever" into a goal that properly transitions
through its lifecycle and renders a completion summary on /goals/<id>.

### 1. complete_goal MCP tool (#59, #62)

New tool surface: complete_goal(goal_id, status, summary, completion_message_id?)

The critic calls this from inside the sandbox after it sends its FINAL:
DM. Records the one-paragraph human-readable summary on the goal row
plus a pointer to the message that carried the FINAL text, so the Web
UI /goals/<id> page has both the verdict and a deep link to the full
findings JSON.

Status parameter accepts completed | stuck | cancelled. Idempotent
when called with the current status. Rejects callers owned by a
different human than the goal owner.

Plumbing:
- New migration 026_goals_completion_summary.sql adds two columns
  to goals: completion_summary TEXT, completion_message_id INTEGER
  (FK messages.id, ON DELETE SET NULL).
- internal/goals/types.go: new CompletionSummary + CompletionMessageID
  fields on Goal struct.
- internal/goals/store.go: Get/List Scan both new columns;
  SetCompletion(goalID, status, summary, messageID) helper that
  updates status+summary+message_id atomically and populates
  completed_at for terminal states.
- internal/goals/service.go: Complete(ctx, goalID, status, summary,
  messageID) wraps the store method with legalTransition gating.
  legalTransition expanded so draft can jump straight to completed
  (no mandatory "active" hop required).
- internal/mcp/goals_tools.go: completeGoalTool definition +
  handleCompleteGoal handler. Tool count 6 → 7.
- internal/api/goals_handler.go: surfaces completion_summary,
  completion_message_id, and completed_at on both list and detail
  endpoints so the Svelte /goals UI can render them.

### 2. Draft → active auto-transition in propose_task_tree (#60)

handleProposeTaskTree now flips the goal from draft to active at the
end. Previously the coordinator would call create_goal +
propose_task_tree and dispatch inspector, but the goal stayed in
draft forever because nothing transitioned it. Now the mere fact
of having a task tree means the goal is active.

Safe: the transition is best-effort and ignores the legal-transition
error when the goal is already beyond draft.

### 3. REVISE round cap (#61)

Two-layer enforcement:

- Server-side: examples/doc-gardener/start.sh drops max_trigger_depth
  from 8 to 4. Each REVISE round costs 2 hops (critic→inspector +
  inspector→critic), so depth=4 caps the loop at roughly 2 rounds
  before the reactor refuses further dispatches.

- Prompt-side: inspector now includes revision_round (starting at
  0, incremented when it sees a REVISE: input) in its findings JSON.
  Critic reads revision_round and force-FINALs when >= 1. Prompt
  explicitly tells the critic to call complete_goal after sending
  FINAL, so the goal row gets a proper completion_summary.

### 4. run_task.sh terminal-state detection

Rewrote the poll loop to watch goals.status/completion_summary as
the definitive "done" signal rather than parsing DM bodies. Keeps
a message-based fallback for TRIVIAL/CANNOT paths that don't create
a goal. Treats "Received system trigger..." and "Coalesced
trigger..." as informational (they're `__coalesced__` reactor
synthetic events leaking through the coordinator reply, not real
user-facing output). Bare coordinator replies are terminal only
when no goal was created.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 18:52:44 +03:00

123 lines
5.3 KiB
Bash
Executable File

#!/bin/bash
# run_task.sh — send a doc-verification goal DM from algis to
# doc-coordinator and wait for the FINAL: reply that flows back from
# docs-critic. The whole flow is driven by MCP tool calls inside three
# Docker-isolated agent containers — nothing here writes to the DB
# directly.
#
# Usage:
# ./run_task.sh # default doc-gardener brief
# ./run_task.sh "your custom goal here"
#
# The default brief asks the inspector to verify mcpproxy CLI flag
# documentation against the actual binary. Override with any free-form
# brief — the coordinator triages it.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BIN="$SCRIPT_DIR/bin/synapbus"
SOCKET="$SCRIPT_DIR/data/synapbus.sock"
DEFAULT_GOAL='Verify the CLI commands listed on https://docs.mcpproxy.app/cli/command-reference still exist in the current mcpproxy binary. Install mcpproxy in the sandbox first (releases at https://github.com/smart-mcp-proxy/mcpproxy-go/releases — pick the linux-arm64 or linux-amd64 variant matching `uname -m`). For each documented command, check whether `mcpproxy --help` and `mcpproxy <command> --help` show it; flag any drift, missing commands, or doc claims that no longer match. Produce a patch suggestion list.'
GOAL="${1:-$DEFAULT_GOAL}"
say() { printf '\033[1;36m[run]\033[0m %s\n' "$*"; }
die() { printf '\033[1;31m[run][FAIL]\033[0m %s\n' "$*" >&2; exit 1; }
[ -x "$BIN" ] || die "synapbus binary not found at $BIN — run ./start.sh first"
[ -S "$SOCKET" ] || die "admin socket missing — is synapbus running?"
cd "$SCRIPT_DIR"
DB="$SCRIPT_DIR/data/synapbus.db"
# Snapshot the current max message id so we only look at replies from
# THIS run, not stale replies left from previous invocations.
BASELINE=$(sqlite3 "$DB" "SELECT COALESCE(MAX(id), 0) FROM messages" 2>/dev/null || echo 0)
say "sending goal DM: algis → doc-coordinator (baseline msg_id=$BASELINE)"
printf '%s' "$GOAL" | "$BIN" --socket "$SOCKET" messages send \
--from algis \
--to doc-coordinator \
--priority 8 >&2
say "waiting for goal completion or FINAL:/CANNOT: reply to algis (up to 600s)..."
deadline=$(( $(date +%s) + 600 ))
last_seen_id=$BASELINE
while [ "$(date +%s)" -lt "$deadline" ]; do
# Goal completion check (definitive signal — set by complete_goal MCP).
# A goal in 'completed'/'stuck'/'cancelled' state with a
# completion_summary means the critic finalized the verdict.
COMPLETED=$(sqlite3 "$DB" "
SELECT id FROM goals
WHERE status IN ('completed','stuck','cancelled')
AND completion_summary IS NOT NULL
ORDER BY id DESC LIMIT 1
" 2>/dev/null || true)
if [ -n "$COMPLETED" ]; then
say "goal $COMPLETED reached terminal state"
GOAL_SUMMARY=$(sqlite3 "$DB" "SELECT status || ': ' || COALESCE(completion_summary,'') FROM goals WHERE id = $COMPLETED" 2>/dev/null)
say "$GOAL_SUMMARY"
echo "$COMPLETED" > "$SCRIPT_DIR/.last_goal_id"
say "goal id = $COMPLETED — render with ./report.sh"
exit 0
fi
# Message-based fallback (for TRIVIAL/CANNOT paths that skip the
# task tree and never call complete_goal).
NEW_LINES=$(sqlite3 -separator '|' "$DB" "
SELECT id, from_agent, replace(substr(body, 1, 280), char(10), ' ')
FROM messages
WHERE to_agent = 'algis'
AND from_agent != 'algis'
AND id > $last_seen_id
ORDER BY id ASC
" 2>/dev/null || true)
if [ -n "$NEW_LINES" ]; then
while IFS='|' read -r id from body; do
[ -z "$id" ] && continue
say "← [$from #$id] $body"
last_seen_id=$id
case "$body" in
DELEGATED:*|REVISING:*|Received\ system\ trigger*|Coalesced\ trigger*)
;; # informational, keep waiting
FINAL:*|CANNOT:*)
say "terminal response received"
GOAL_ID=$(sqlite3 "$DB" 'SELECT id FROM goals ORDER BY id DESC LIMIT 1' 2>/dev/null || echo)
if [ -n "$GOAL_ID" ]; then
echo "$GOAL_ID" > "$SCRIPT_DIR/.last_goal_id"
say "goal id = $GOAL_ID — render with ./report.sh"
fi
exit 0
;;
*)
# A bare reply from doc-coordinator with no status
# prefix is a TRIVIAL-triage direct answer. Count
# it as terminal only if no goal was created (i.e.
# the coordinator didn't start a pipeline).
if [ "$from" = "doc-coordinator" ]; then
HAS_GOAL=$(sqlite3 "$DB" 'SELECT COUNT(*) FROM goals' 2>/dev/null || echo 0)
if [ "$HAS_GOAL" = "0" ]; then
say "direct (trivial) response received"
exit 0
fi
# Otherwise keep waiting — the coordinator
# already delegated and will finalize via
# complete_goal once the critic runs.
fi
;;
esac
done <<EOF
$NEW_LINES
EOF
fi
sleep 1
done
say "timed out waiting for terminal response"
say "check http://localhost:18089/runs and http://localhost:18089/goals"
exit 2