All docs

Task comments & amendments

Task comments & amendments

Migration: 0010_task_comments_amendments.sql Core module: packages/core/src/task/comments.ts MCP tools: apps/mcp/src/tools/comments/

Why typed, not free-form

Drobek's single-write-path invariant forbids unstructured commentary — otherwise people would "fix" tasks via Slack-style comments instead of an MCP call. Instead, agents pick a kind:

Open blocker comments hide a task from get_next_actionable regardless of its state. The claim_task tool consults the same flag.

Rate limit

Max 3 open blocker comments per agent per task. Enforced inside addComment before insert (a quick count(*) keyed by the partial index task_comments_open_blockers_idx). Over the cap → task.invariant_violated.

Shallow threading

parent_comment_id is allowed only when the parent is itself top-level (max 1 reply depth). Deeper threads = signal the discussion belongs in an amendment.

Amendments

task_amendments is a separate workflow: an agent proposes a structured patch (title / description / contracts / priority), older proposed amendments for the same task become superseded, a human (only) approves. On approve:

  1. Snapshot tasks row into applied_snapshot (rollback artifact).
  2. Apply patch fields with an UPDATE.
  3. If the patch touches a contract and the task is claimed, the claim is

released so the agent stops work. The pub/sub event carries claimDropped: true.

Rejection requires a decision_note. Rejected/superseded states are read-only audit rows.

MCP tools (scopes)

Approvals (approveAmendment / rejectAmendment) are user-only and currently exposed only via the core helpers — the UI flow lands in task 38.

Pub/sub events

All events fire on pubsub:org:{orgId}:tasks after commit:

  • task.comment.added{ taskId, commentId, kind, authorKind, authorId }
  • task.comment.resolved{ taskId, commentId, decision }
  • task.amendment.proposed{ taskId, amendmentId, patchKeys }
  • task.amendment.applied{ taskId, amendmentId, claimDropped }
  • task.amendment.rejected{ taskId, amendmentId }

Decision tree (when to use which kind)

Is the task spec broken / impossible?         → `blocker` (with rationale)
Should the task not exist at all?             → `nack`
You don't understand a requirement?           → `question`
You found something useful while doing it?    → `note`
Want to link to another task / RFC / knowledge? → `reference`
Need to actually CHANGE the spec?             → `propose_task_amendment`

If you're tempted to write more than two sentences in a body, the answer is probably an amendment, not a comment.