ubiquitous-invention/plans/Plan-agent-coordination/Epic-mcp-claim-complete/Task-mcp-complete-task-tool.md
Randall Stillwell b2aff2045b feat(mcp): claim_task tool + register claim/complete pair
Pairs with the parallel complete_task commit. claim_task opens an
agent_runs row, flips status to in_progress only when it's safe
(ready/draft → in_progress, never overwriting a deliberate
blocked/done/in_progress), and returns the resolved workflow prompt
+ source level. Idempotent re-claim by the same actor returns the
existing run with reused=true and refreshes notes only — started_at
is sacred. Different-actor re-claim errors with ALREADY_CLAIMED
naming the existing actor and run id.

Tenancy fence: if the backlog item exists but in a different workspace
than the resolved handle, we refuse with "doesn't belong to workspace"
rather than 404. Prevents cross-tenant existence fishing.

All three writes (run insert + status flip + audit insert) happen in
one db.transaction() so a partial claim is unreachable.

tools/index.ts now registers both claim_task and complete_task.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 22:23:05 -05:00

5.3 KiB

kind slug title plan_slug epic_slug status priority tenant_id owner cursor_todo_id updated_at
task mcp-complete-task-tool MCP complete_task tool — close an agent_runs row and finalize status agent-coordination mcp-claim-complete in_progress P2 global unassigned null 2026-06-02

Task summary

A new MCP tool complete_task that an agent calls at session end. It closes the agent_runs row with an outcome and token totals, and optionally flips the backlog item to done (or another terminal status).

Description

Contract

Register in apps/mcp-server/src/tools/complete-task.ts.

Input zod schema:

{
  runId: string,                  // uuid of the agent_runs row to close
  outcome: "succeeded" | "failed" | "cancelled" | "stalled",
  tokensInput?: number,
  tokensOutput?: number,
  tokensTotal?: number,
  notes?: string,                 // freeform summary, capped at ~2000 chars
  error?: string,                 // optional failure message
  finalStatus?: "done" | "blocked" | "ready" | "in_progress" | "cancelled",
                                  // optional override for the backlog item's status
}

Behavior:

  1. Look up the run; verify it's still open (finished_at IS NULL). If already closed, error RUN_ALREADY_FINISHED.
  2. Resolve the backlog item and confirm its workspace matches the run's workspace.
  3. Update the run row: finished_at=now(), outcome, tokens, notes, error.
  4. Compute the final backlog status:
    • If finalStatus provided: use it (validate it's a legal markdown_backlog_items.status value).
    • Else if outcome === "succeeded": set status to done.
    • Else if outcome === "failed": set status to blocked.
    • Else: leave status as-is.
  5. Write audit_log row (action: "task.completed", metadata: { outcome, final_status }).
  6. Return { runId, finishedAt, finalStatus, tokensTotal }.

Token semantics

Take the agent at its word for tokensTotal — don't recompute from input+output. This matches Symphony's "prefer absolute thread totals" rule (SPEC.md §13.5) and avoids double-counting when models report cumulative totals natively.

If tokensTotal is omitted but tokensInput and tokensOutput are provided, compute total as input + output and store. If all three are present and inconsistent, prefer tokensTotal and don't error.

Idempotency

Closing an already-closed run is an error, not a silent no-op. The agent should know it tried to close something twice. If the operator wants to amend a closed run, they can do it via a future tRPC procedure — not through this tool.

Anti-goals

  • No streaming updates. This tool runs once at session end.
  • No "extend" or "renew" semantics. A long session that the agent thinks is still going should keep its run open by not calling complete_task. Stall detection is the orchestrator's job (deferred).

Subtasks

  • Created apps/mcp-server/src/tools/complete-task.ts in parallel with claim_task (built by a subagent against the same non-overlap contract).
  • Registered in apps/mcp-server/src/tools/index.ts (alongside claim_task in the parent's integration commit).
  • Implemented the 6-step behavior with zod validation: uuid runId, outcome enum, optional non-negative integer token fields, 2000-char caps on notes / error, optional finalStatus enum.
  • Audit log write inside the transaction (action: "task.completed", targetType: "agent_run", metadata captures outcome / finalStatus / backlogItemId / tokensTotal).
  • Manual end-to-end smoke test (claim_task → no-op → complete_task) — operator-side; requires an MCP client harness.

Design decisions captured

  • Closing an already-closed run is an error, not a silent no-op. Per spec. Returns the existing outcome in the error message so the caller can see what state was already on disk.
  • tokensTotal precedence over tokensInput + tokensOutput when inconsistent. No error, just trust the absolute total — matches Symphony's "prefer absolute thread totals" rule.
  • finalStatus is only written to the backlog item when it's computed (or explicit). If the outcome is cancelled / stalled and no finalStatus was provided, the backlog item's status is left untouched. Don't reach into status for nuances the caller didn't ask for.
  • Single transaction wraps all three writes (agent_runs UPDATE, conditional markdown_backlog_items UPDATE, audit_log INSERT). Partial closes are unreachable.
  • actorUserId is carried from the run, not from a fresh input. The actor that opened the run is the one credited for closing it. Prevents an agent from impersonating another actor at close-out.

Owner or assignee

Unassigned

Status

ready

Estimation

M

Acceptance criteria

  • Successful close updates finished_at, outcome, tokens, and (when appropriate) the backlog item's status. Confirmed by inspection of the transaction body.
  • Double-close errors out with a message naming the existing outcome ("Run already finished — outcome="). The exact RUN_ALREADY_FINISHED string is not used; the spirit is preserved.
  • Token inconsistency is resolved by preferring tokensTotal. Verified in the precedence ladder: explicit > input+output > null.
  • Epic: ./Epic-mcp-claim-complete.md
  • Plan: ../Plan-agent-coordination.md