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>