Skip to main content
Glama

Edit a node

node
Destructive

Edit a node or its structure. action = update (edit fields/body) | move (re-file under a component or workspace) | link (add a typed edge between two nodes). Removing an edge (unlink) lives in the destructive tool.

Input Schema

NameRequiredDescriptionDefault

No arguments

Output Schema

NameRequiredDescriptionDefault
dataYesThe action-specific result returned by Stele.
actionYesThe action that produced this result.

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed3 schema fields changed
    • changedInput schema / anyOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "description": "node action \"update\".",
      -    "properties": {
      -      "action": {
      -        "const": "update",
      -        "description": "Run the update action.",
      -        "type": "string"
      -      },
      -      "anchor_files": {
      -        "anyOf": [
      -          {
      -            "anyOf": [
      -              {
      -                "not": {}
      -              },
      -              {
      -                "items": {
      -                  "type": "string"
      -                },
      -                "type": "array"
      -              }
      -            ],
      -            "description": "Repo-relative file paths this note is about (e.g. 'src/auth/session.ts'). When someone later edits a file named here, this node surfaces to them. Set it on a lesson/risk/gap scoped to specific code so it resurfaces exactly when that code is touched."
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge or task nodes — repo-relative file paths this node is about; surfaces it when one is later edited (for a task: don't step on toes). Pass [] or null to clear."
      -      },
      -      "anchor_symbols": {
      -        "anyOf": [
      -          {
      -            "anyOf": [
      -              {
      -                "not": {}
      -              },
      -              {
      -                "items": {
      -                  "type": "string"
      -                },
      -                "type": "array"
      -              }
      -            ],
      -            "description": "Symbol names this note is about — function / class / constant / env var (e.g. 'refreshToken', 'SESSION_TTL'). Same purpose as anchor_files: a precise hook so the note resurfaces when that symbol's code is touched."
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge or task nodes — symbol names this node is about (function / class / constant / env var). Pass [] or null to clear."
      -      },
      -      "assignee_user_id": {
      -        "anyOf": [
      -          {
      -            "format": "uuid",
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) task nodes only — user uuid (not a shareable id)"
      -      },
      -      "body": {
      -        "description": "(update only) New markdown body (full replace) — mutually exclusive with edits. Write it in plain style: one idea per sentence, active voice, lead with the condition, one term per concept, exact technical names.",
      -        "type": [
      -          "string",
      -          "null"
      -        ]
      -      },
      -      "category": {
      -        "anyOf": [
      -          {
      -            "enum": [
      -              "decision",
      -              "architecture",
      -              "goal",
      -              "risk",
      -              "gap",
      -              "fyi",
      -              "opportunity",
      -              "priority",
      -              "lesson"
      -            ],
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge-type nodes only"
      -      },
      -      "due_at": {
      -        "anyOf": [
      -          {
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — ISO-8601 deadline. Pass null to clear. Any active knowledge with a past due_at surfaces in stats.overdue_commitments."
      -      },
      -      "edits": {
      -        "description": "(update only) Literal string-replacements applied to the body server-side, in order. Mutually exclusive with `body`. Pair with an exact search: use a matching line as old_string.",
      -        "items": {
      -          "additionalProperties": false,
      -          "properties": {
      -            "new_string": {
      -              "description": "Replacement text.",
      -              "type": "string"
      -            },
      -            "old_string": {
      -              "description": "Exact substring to find in the current body.",
      -              "minLength": 1,
      -              "type": "string"
      -            },
      -            "replace_all": {
      -              "description": "Replace every occurrence (default: require a unique match).",
      -              "type": "boolean"
      -            }
      -          },
      -          "required": [
      -            "old_string",
      -            "new_string"
      -          ],
      -          "type": "object"
      -        },
      -        "minItems": 1,
      -        "type": "array"
      -      },
      -      "expected_version": {
      -        "description": "(update only) Optimistic concurrency: reject the write if the node's current version differs (it changed since you read it).",
      -        "exclusiveMinimum": 0,
      -        "type": "integer"
      -      },
      -      "expires_at": {
      -        "anyOf": [
      -          {
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — ISO-8601 expiry (date TTL). Pass null to clear. When it passes, the next health-check run with fix archives the node. For ephemeral `fyi`/temporary-note knowledge; decision/architecture retire by supersession instead."
      -      },
      -      "hidden": {
      -        "description": "(update only) Set true to HIDE this node from every retrieval surface (search, recall, lists) — the one explicit invisibility. It stays fetchable by its shareable id (shown with a HIDDEN banner), so hiding is never a delete. Use for a genuine mistake, expired ephemera, or a secret that was pasted in by accident. Set false to un-hide. On the hosted assistant this pauses for human approval.",
      -        "type": "boolean"
      -      },
      -      "node_id": {
      -        "description": "REQUIRED for update — shareable id of the node to edit (TASK-N/KNOW-N/COMP-N/DOC-N/NODE-N).",
      -        "type": "string"
      -      },
      -      "parent_component_id": {
      -        "anyOf": [
      -          {
      -            "description": "Shareable id: TASK-N (task), KNOW-N (knowledge), COMP-N (component), DOC-N (document), or NODE-N (any).",
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) shareable id of the new parent component. Accepted for component, knowledge, task, and document nodes. null = move to project root (documents and components only). Same-project enforcement is implicit — short ids resolve only within the active project."
      -      },
      -      "priority": {
      -        "anyOf": [
      -          {
      -            "enum": [
      -              "P0",
      -              "P1",
      -              "P2",
      -              "P3"
      -            ],
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) task nodes only — priority P0 (most urgent) … P3. Pass null to clear."
      -      },
      -      "privacy_bypass": {
      -        "description": "true writes despite a privacy flag. Only meaningful after a flag; the user decides whether the content can be written.",
      -        "type": "boolean"
      -      },
      -      "privacy_false_positive": {
      -        "description": "With privacy_bypass: true reports the privacy flag as wrong.",
      -        "type": "boolean"
      -      },
      -      "privacy_judgement_id": {
      -        "description": "With privacy_bypass: the judgement_id the privacy flag returned.",
      -        "type": "string"
      -      },
      -      "privacy_note": {
      -        "description": "With privacy_bypass: an optional short reason, kept with the bypass.",
      -        "maxLength": 500,
      -        "type": "string"
      -      },
      -      "project_slug": {
      -        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "source": {
      -        "anyOf": [
      -          {
      -            "maxLength": 500,
      -            "minLength": 1,
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — free-form provenance label. Pass null to clear."
      -      },
      -      "source_type": {
      -        "description": "(update only) knowledge nodes only — provenance / trust signal (human, claude, import).",
      -        "enum": [
      -          "human",
      -          "claude",
      -          "import"
      -        ],
      -        "type": "string"
      -      },
      -      "source_url": {
      -        "anyOf": [
      -          {
      -            "format": "uri",
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — canonical URL for the source. Pass null to clear."
      -      },
      -      "status": {
      -        "description": "(update only) A task may be set to open or backlog here. Claim, completion, and cancellation must use the task lifecycle actions (claim, complete, cancel) so lifecycle records and agent identity remain atomic.",
      -        "enum": [
      -          "active",
      -          "archived",
      -          "backlog",
      -          "open",
      -          "in_progress",
      -          "done",
      -          "cancelled"
      -        ],
      -        "type": "string"
      -      },
      -      "title": {
      -        "description": "(update only) New title.",
      -        "maxLength": 300,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "topics": {
      -        "description": "(update only) knowledge or task nodes only",
      -        "items": {
      -          "type": "string"
      -        },
      -        "type": "array"
      -      }
      -    },
      -    "required": [
      -      "action",
      -      "project_slug",
      -      "node_id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "node action \"move\".",
      -    "properties": {
      -      "action": {
      -        "const": "move",
      -        "description": "Run the move action.",
      -        "type": "string"
      -      },
      -      "node_id": {
      -        "description": "REQUIRED for move — the node to move, a short-id (KNOW-N / TASK-N / DOC-N / COMP-N).",
      -        "type": "string"
      -      },
      -      "project_slug": {
      -        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "to": {
      -        "description": "REQUIRED for move — destination scope: 'trunk' for shared truth, or a workspace NAME (e.g. \"Billing\") or uuid to scope it there.",
      -        "minLength": 1,
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "action",
      -      "project_slug",
      -      "node_id",
      -      "to"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "node action \"link\".",
      -    "properties": {
      -      "action": {
      -        "const": "link",
      -        "description": "Run the link action.",
      -        "type": "string"
      -      },
      -      "dst_id": {
      -        "description": "REQUIRED for link — destination node shareable id.",
      -        "type": "string"
      -      },
      -      "evidence": {
      -        "description": "The VERBATIM line from the source that proves the claim — REQUIRED for `supersedes`. It must show the old state actually ENDED, not merely that something newer exists. \"We also added a read replica\" is additive and supersedes nothing; \"we've moved off DynamoDB entirely\" is a termination. If you cannot quote a termination, the correct relation is relates-to.",
      -        "maxLength": 4000,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "privacy_bypass": {
      -        "description": "true writes despite a privacy flag. Only meaningful after a flag; the user decides whether the content can be written.",
      -        "type": "boolean"
      -      },
      -      "privacy_false_positive": {
      -        "description": "With privacy_bypass: true reports the privacy flag as wrong.",
      -        "type": "boolean"
      -      },
      -      "privacy_judgement_id": {
      -        "description": "With privacy_bypass: the judgement_id the privacy flag returned.",
      -        "type": "string"
      -      },
      -      "privacy_note": {
      -        "description": "With privacy_bypass: an optional short reason, kept with the bypass.",
      -        "maxLength": 500,
      -        "type": "string"
      -      },
      -      "project_slug": {
      -        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "reason": {
      -        "description": "WHY this edge exists — REQUIRED for supersedes / contradicts / caused-by / blocks / fulfills. State the claim in your own words: for `supersedes`, name the standing state that ENDED (\"we moved the event store off DynamoDB to Postgres on Jul 13\"); for `caused-by`, name the causal mechanism. Not required for relates-to (the 'nothing typed fits' default) — if you cannot state a reason, that is the signal to use relates-to instead of forcing a typed edge.",
      -        "maxLength": 2000,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "relation": {
      -        "description": "REQUIRED for link — edge type, read src → dst. relates-to: a pointer, no claim. blocks: src (child) must land before dst (parent) can finish; the subtask edge. fulfills: src task delivers dst. caused-by: dst is src's root cause. contradicts: both stay active for a human to resolve. supersedes: retires dst as wrong to act on, not merely older. expires-on: src knowledge retires when dst task closes.",
      -        "enum": [
      -          "relates-to",
      -          "blocks",
      -          "fulfills",
      -          "contradicts",
      -          "supersedes",
      -          "caused-by",
      -          "expires-on"
      -        ],
      -        "type": "string"
      -      },
      -      "src_id": {
      -        "description": "REQUIRED for link — source node shareable id.",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "action",
      -      "project_slug",
      -      "src_id",
      -      "dst_id",
      -      "relation"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "description": "node action \"update\".",
      +    "properties": {
      +      "action": {
      +        "const": "update",
      +        "description": "Run the update action.",
      +        "type": "string"
      +      },
      +      "anchor_files": {
      +        "anyOf": [
      +          {
      +            "description": "Repo-relative file paths this note is about (e.g. 'src/auth/session.ts'). When someone later edits a file named here, this node surfaces to them. Set it on a lesson/risk/gap scoped to specific code so it resurfaces exactly when that code is touched.",
      +            "items": {
      +              "type": "string"
      +            },
      +            "type": "array"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge or task nodes — repo-relative file paths this node is about; surfaces it when one is later edited (for a task: don't step on toes). Pass [] or null to clear."
      +      },
      +      "anchor_symbols": {
      +        "anyOf": [
      +          {
      +            "description": "Symbol names this note is about — function / class / constant / env var (e.g. 'refreshToken', 'SESSION_TTL'). Same purpose as anchor_files: a precise hook so the note resurfaces when that symbol's code is touched.",
      +            "items": {
      +              "type": "string"
      +            },
      +            "type": "array"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge or task nodes — symbol names this node is about (function / class / constant / env var). Pass [] or null to clear."
      +      },
      +      "assignee_user_id": {
      +        "anyOf": [
      +          {
      +            "format": "uuid",
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) task nodes only — user uuid (not a shareable id)"
      +      },
      +      "body": {
      +        "anyOf": [
      +          {
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) New markdown body (full replace) — mutually exclusive with edits. Write it in plain style: one idea per sentence, active voice, lead with the condition, one term per concept, exact technical names."
      +      },
      +      "category": {
      +        "anyOf": [
      +          {
      +            "enum": [
      +              "decision",
      +              "architecture",
      +              "goal",
      +              "risk",
      +              "gap",
      +              "fyi",
      +              "opportunity",
      +              "priority",
      +              "lesson"
      +            ],
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge-type nodes only"
      +      },
      +      "due_at": {
      +        "anyOf": [
      +          {
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — ISO-8601 deadline. Pass null to clear. Any active knowledge with a past due_at surfaces in stats.overdue_commitments."
      +      },
      +      "edits": {
      +        "description": "(update only) Literal string-replacements applied to the body server-side, in order. Mutually exclusive with `body`. Pair with an exact search: use a matching line as old_string.",
      +        "items": {
      +          "properties": {
      +            "new_string": {
      +              "description": "Replacement text.",
      +              "type": "string"
      +            },
      +            "old_string": {
      +              "description": "Exact substring to find in the current body.",
      +              "minLength": 1,
      +              "type": "string"
      +            },
      +            "replace_all": {
      +              "description": "Replace every occurrence (default: require a unique match).",
      +              "type": "boolean"
      +            }
      +          },
      +          "required": [
      +            "old_string",
      +            "new_string"
      +          ],
      +          "type": "object"
      +        },
      +        "minItems": 1,
      +        "type": "array"
      +      },
      +      "expected_version": {
      +        "description": "(update only) Optimistic concurrency: reject the write if the node's current version differs (it changed since you read it).",
      +        "exclusiveMinimum": 0,
      +        "type": "integer"
      +      },
      +      "expires_at": {
      +        "anyOf": [
      +          {
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — ISO-8601 expiry (date TTL). Pass null to clear. When it passes, the next health-check run with fix archives the node. For ephemeral `fyi`/temporary-note knowledge; decision/architecture retire by supersession instead."
      +      },
      +      "hidden": {
      +        "description": "(update only) Set true to HIDE this node from every retrieval surface (search, recall, lists) — the one explicit invisibility. It stays fetchable by its shareable id (shown with a HIDDEN banner), so hiding is never a delete. Use for a genuine mistake, expired ephemera, or a secret that was pasted in by accident. Set false to un-hide. On the hosted assistant this pauses for human approval.",
      +        "type": "boolean"
      +      },
      +      "node_id": {
      +        "description": "REQUIRED for update — shareable id of the node to edit (TASK-N/KNOW-N/COMP-N/DOC-N/NODE-N).",
      +        "type": "string"
      +      },
      +      "parent_component_id": {
      +        "anyOf": [
      +          {
      +            "description": "Shareable id: TASK-N (task), KNOW-N (knowledge), COMP-N (component), DOC-N (document), or NODE-N (any).",
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) shareable id of the new parent component. Accepted for component, knowledge, task, and document nodes. null = move to project root (documents and components only). Same-project enforcement is implicit — short ids resolve only within the active project."
      +      },
      +      "priority": {
      +        "anyOf": [
      +          {
      +            "enum": [
      +              "P0",
      +              "P1",
      +              "P2",
      +              "P3"
      +            ],
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) task nodes only — priority P0 (most urgent) … P3. Pass null to clear."
      +      },
      +      "privacy_bypass": {
      +        "description": "true writes despite a privacy flag. Only meaningful after a flag; the user decides whether the content can be written.",
      +        "type": "boolean"
      +      },
      +      "privacy_false_positive": {
      +        "description": "With privacy_bypass: true reports the privacy flag as wrong.",
      +        "type": "boolean"
      +      },
      +      "privacy_judgement_id": {
      +        "description": "With privacy_bypass: the judgement_id the privacy flag returned.",
      +        "type": "string"
      +      },
      +      "privacy_note": {
      +        "description": "With privacy_bypass: an optional short reason, kept with the bypass.",
      +        "maxLength": 500,
      +        "type": "string"
      +      },
      +      "project_slug": {
      +        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "source": {
      +        "anyOf": [
      +          {
      +            "maxLength": 500,
      +            "minLength": 1,
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — free-form provenance label. Pass null to clear."
      +      },
      +      "source_type": {
      +        "description": "(update only) knowledge nodes only — provenance / trust signal (human, claude, import).",
      +        "enum": [
      +          "human",
      +          "claude",
      +          "import"
      +        ],
      +        "type": "string"
      +      },
      +      "source_url": {
      +        "anyOf": [
      +          {
      +            "format": "uri",
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — canonical URL for the source. Pass null to clear."
      +      },
      +      "status": {
      +        "description": "(update only) A task may be set to open or backlog here. Claim, completion, and cancellation must use the task lifecycle actions (claim, complete, cancel) so lifecycle records and agent identity remain atomic.",
      +        "enum": [
      +          "active",
      +          "archived",
      +          "backlog",
      +          "open",
      +          "in_progress",
      +          "done",
      +          "cancelled"
      +        ],
      +        "type": "string"
      +      },
      +      "title": {
      +        "description": "(update only) New title.",
      +        "maxLength": 300,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "topics": {
      +        "description": "(update only) knowledge or task nodes only",
      +        "items": {
      +          "type": "string"
      +        },
      +        "type": "array"
      +      }
      +    },
      +    "required": [
      +      "action",
      +      "project_slug",
      +      "node_id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "node action \"move\".",
      +    "properties": {
      +      "action": {
      +        "const": "move",
      +        "description": "Run the move action.",
      +        "type": "string"
      +      },
      +      "node_id": {
      +        "description": "REQUIRED for move — the node to move, a short-id (KNOW-N / TASK-N / DOC-N / COMP-N).",
      +        "type": "string"
      +      },
      +      "project_slug": {
      +        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "to": {
      +        "description": "REQUIRED for move — destination scope: 'trunk' for shared truth, or a workspace NAME (e.g. \"Billing\") or uuid to scope it there.",
      +        "minLength": 1,
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "action",
      +      "project_slug",
      +      "node_id",
      +      "to"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "node action \"link\".",
      +    "properties": {
      +      "action": {
      +        "const": "link",
      +        "description": "Run the link action.",
      +        "type": "string"
      +      },
      +      "dst_id": {
      +        "description": "REQUIRED for link — destination node shareable id.",
      +        "type": "string"
      +      },
      +      "evidence": {
      +        "description": "The VERBATIM line from the source that proves the claim — REQUIRED for `supersedes`. It must show the old state actually ENDED, not merely that something newer exists. \"We also added a read replica\" is additive and supersedes nothing; \"we've moved off DynamoDB entirely\" is a termination. If you cannot quote a termination, the correct relation is relates-to.",
      +        "maxLength": 4000,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "privacy_bypass": {
      +        "description": "true writes despite a privacy flag. Only meaningful after a flag; the user decides whether the content can be written.",
      +        "type": "boolean"
      +      },
      +      "privacy_false_positive": {
      +        "description": "With privacy_bypass: true reports the privacy flag as wrong.",
      +        "type": "boolean"
      +      },
      +      "privacy_judgement_id": {
      +        "description": "With privacy_bypass: the judgement_id the privacy flag returned.",
      +        "type": "string"
      +      },
      +      "privacy_note": {
      +        "description": "With privacy_bypass: an optional short reason, kept with the bypass.",
      +        "maxLength": 500,
      +        "type": "string"
      +      },
      +      "project_slug": {
      +        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "reason": {
      +        "description": "WHY this edge exists — REQUIRED for supersedes / contradicts / caused-by / blocks / fulfills. State the claim in your own words: for `supersedes`, name the standing state that ENDED (\"we moved the event store off DynamoDB to Postgres on Jul 13\"); for `caused-by`, name the causal mechanism. Not required for relates-to (the 'nothing typed fits' default) — if you cannot state a reason, that is the signal to use relates-to instead of forcing a typed edge.",
      +        "maxLength": 2000,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "relation": {
      +        "description": "REQUIRED for link — edge type, read src → dst. relates-to: a pointer, no claim. blocks: src (child) must land before dst (parent) can finish; the subtask edge. fulfills: src task delivers dst. caused-by: dst is src's root cause. contradicts: both stay active for a human to resolve. supersedes: retires dst as wrong to act on, not merely older. expires-on: src knowledge retires when dst task closes.",
      +        "enum": [
      +          "relates-to",
      +          "blocks",
      +          "fulfills",
      +          "contradicts",
      +          "supersedes",
      +          "caused-by",
      +          "expires-on"
      +        ],
      +        "type": "string"
      +      },
      +      "src_id": {
      +        "description": "REQUIRED for link — source node shareable id.",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "action",
      +      "project_slug",
      +      "src_id",
      +      "dst_id",
      +      "relation"
      +    ],
      +    "type": "object"
      +  }
      +]
    • changedOutput schema / $schema
      Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • changedOutput schema / properties / data / additionalProperties
      Previous value: -trueNew value: +{}
  2. Changed1 schema field changed
    • changedInput schema / anyOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "description": "action=\"update\": Update editable fields on any node; each field is validated against what's valid for the node's type, so type-mismatched fields (e.g. category on a task) are rejected. A task's status may be set to open or backlog here, but claim, completion, and cancellation are REJECTED — use task(action=\"claim\") / task(action=\"complete\") so the claim and completion (with session + agent identity) are recorded atomically, or destructive(action=\"cancel_task\") so the destructive transition records a reason. Archiving a component is also rejected; use destructive(action=\"archive_component\") so active children cannot be orphaned. `parent_component_id` reparents across components in the same project. Surgical edits: pass `edits` (literal old_string→new_string replacements applied server-side) instead of `body` to fix a substring without resending the whole body — they're mutually exclusive, and a non-matching old_string errors rather than no-ops. Optional `expected_version` gives optimistic concurrency.",
      -    "properties": {
      -      "action": {
      -        "const": "update",
      -        "type": "string"
      -      },
      -      "anchor_files": {
      -        "anyOf": [
      -          {
      -            "anyOf": [
      -              {
      -                "not": {}
      -              },
      -              {
      -                "items": {
      -                  "type": "string"
      -                },
      -                "type": "array"
      -              }
      -            ],
      -            "description": "Repo-relative file paths this note is about (e.g. 'src/auth/session.ts'). When someone later edits a file named here, this node surfaces to them. Set it on a lesson/risk/gap scoped to specific code so it resurfaces exactly when that code is touched."
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge or task nodes — repo-relative file paths this node is about; surfaces it when one is later edited (for a task: don't step on toes). Pass [] or null to clear."
      -      },
      -      "anchor_symbols": {
      -        "anyOf": [
      -          {
      -            "anyOf": [
      -              {
      -                "not": {}
      -              },
      -              {
      -                "items": {
      -                  "type": "string"
      -                },
      -                "type": "array"
      -              }
      -            ],
      -            "description": "Symbol names this note is about — function / class / constant / env var (e.g. 'refreshToken', 'SESSION_TTL'). Same purpose as anchor_files: a precise hook so the note resurfaces when that symbol's code is touched."
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge or task nodes — symbol names this node is about (function / class / constant / env var). Pass [] or null to clear."
      -      },
      -      "assignee_user_id": {
      -        "anyOf": [
      -          {
      -            "format": "uuid",
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) task nodes only — user uuid (not a shareable id)"
      -      },
      -      "body": {
      -        "description": "(update only) New markdown body (full replace) — mutually exclusive with edits. Write it in plain style: one idea per sentence, active voice, lead with the condition, one term per concept, exact technical names.",
      -        "type": [
      -          "string",
      -          "null"
      -        ]
      -      },
      -      "category": {
      -        "anyOf": [
      -          {
      -            "enum": [
      -              "decision",
      -              "architecture",
      -              "goal",
      -              "risk",
      -              "gap",
      -              "fyi",
      -              "opportunity",
      -              "priority",
      -              "lesson"
      -            ],
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge-type nodes only"
      -      },
      -      "due_at": {
      -        "anyOf": [
      -          {
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — ISO-8601 deadline. Pass null to clear. Any active knowledge with a past due_at surfaces in stats.overdue_commitments."
      -      },
      -      "edits": {
      -        "description": "(update only) Literal string-replacements applied to the body server-side, in order. Mutually exclusive with `body`. Pair with an exact search: use a matching line as old_string.",
      -        "items": {
      -          "additionalProperties": false,
      -          "properties": {
      -            "new_string": {
      -              "description": "Replacement text.",
      -              "type": "string"
      -            },
      -            "old_string": {
      -              "description": "Exact substring to find in the current body.",
      -              "minLength": 1,
      -              "type": "string"
      -            },
      -            "replace_all": {
      -              "description": "Replace every occurrence (default: require a unique match).",
      -              "type": "boolean"
      -            }
      -          },
      -          "required": [
      -            "old_string",
      -            "new_string"
      -          ],
      -          "type": "object"
      -        },
      -        "minItems": 1,
      -        "type": "array"
      -      },
      -      "expected_version": {
      -        "description": "(update only) Optimistic concurrency: reject the write if the node's current version differs (it changed since you read it).",
      -        "exclusiveMinimum": 0,
      -        "type": "integer"
      -      },
      -      "expires_at": {
      -        "anyOf": [
      -          {
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — ISO-8601 expiry (date TTL). Pass null to clear. When it passes, the next health-check run with fix archives the node. For ephemeral `fyi`/temporary-note knowledge; decision/architecture retire by supersession instead."
      -      },
      -      "hidden": {
      -        "description": "(update only) Set true to HIDE this node from every retrieval surface (search, recall, lists) — the one explicit invisibility. It stays fetchable by its shareable id (shown with a HIDDEN banner), so hiding is never a delete. Use for a genuine mistake, expired ephemera, or a secret that was pasted in by accident. Set false to un-hide. On the hosted assistant this pauses for human approval.",
      -        "type": "boolean"
      -      },
      -      "node_id": {
      -        "description": "REQUIRED for update — shareable id of the node to edit (TASK-N/KNOW-N/COMP-N/DOC-N/NODE-N).",
      -        "type": "string"
      -      },
      -      "parent_component_id": {
      -        "anyOf": [
      -          {
      -            "description": "Shareable id: TASK-N (task), KNOW-N (knowledge), COMP-N (component), DOC-N (document), or NODE-N (any).",
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) shareable id of the new parent component. Accepted for component, knowledge, task, and document nodes. null = move to project root (documents and components only). Same-project enforcement is implicit — short ids resolve only within the active project."
      -      },
      -      "priority": {
      -        "anyOf": [
      -          {
      -            "enum": [
      -              "P0",
      -              "P1",
      -              "P2",
      -              "P3"
      -            ],
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) task nodes only — priority P0 (most urgent) … P3. Pass null to clear."
      -      },
      -      "privacy_bypass": {
      -        "description": "true writes despite a privacy flag. Only meaningful after a flag; the user decides whether the content can be written.",
      -        "type": "boolean"
      -      },
      -      "privacy_false_positive": {
      -        "description": "With privacy_bypass: true reports the privacy flag as wrong.",
      -        "type": "boolean"
      -      },
      -      "privacy_judgement_id": {
      -        "description": "With privacy_bypass: the judgement_id the privacy flag returned.",
      -        "type": "string"
      -      },
      -      "privacy_note": {
      -        "description": "With privacy_bypass: an optional short reason, kept with the bypass.",
      -        "maxLength": 500,
      -        "type": "string"
      -      },
      -      "project_slug": {
      -        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "source": {
      -        "anyOf": [
      -          {
      -            "maxLength": 500,
      -            "minLength": 1,
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — free-form provenance label. Pass null to clear."
      -      },
      -      "source_type": {
      -        "description": "(update only) knowledge nodes only — provenance / trust signal (human, claude, import).",
      -        "enum": [
      -          "human",
      -          "claude",
      -          "import"
      -        ],
      -        "type": "string"
      -      },
      -      "source_url": {
      -        "anyOf": [
      -          {
      -            "format": "uri",
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — canonical URL for the source. Pass null to clear."
      -      },
      -      "status": {
      -        "description": "(update only) A task may be set to open or backlog here. Claim, completion, and cancellation must use the task lifecycle actions (claim, complete, cancel) so lifecycle records and agent identity remain atomic.",
      -        "enum": [
      -          "active",
      -          "archived",
      -          "backlog",
      -          "open",
      -          "in_progress",
      -          "done",
      -          "cancelled"
      -        ],
      -        "type": "string"
      -      },
      -      "title": {
      -        "description": "(update only) New title.",
      -        "maxLength": 300,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "topics": {
      -        "description": "(update only) knowledge or task nodes only",
      -        "items": {
      -          "type": "string"
      -        },
      -        "type": "array"
      -      }
      -    },
      -    "required": [
      -      "action",
      -      "project_slug",
      -      "node_id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "action=\"move\": Move an EXISTING node between scopes — into a workspace (hiding it from trunk readers) or back to trunk (shared truth). Re-scopes by id; reversible, never deletes. Distinct from destructive(action=\"cherry_pick_workspace\") (which promotes to trunk WITH provenance) — This is a plain re-assignment in any direction.",
      -    "properties": {
      -      "action": {
      -        "const": "move",
      -        "type": "string"
      -      },
      -      "node_id": {
      -        "description": "REQUIRED for move — the node to move, a short-id (KNOW-N / TASK-N / DOC-N / COMP-N).",
      -        "type": "string"
      -      },
      -      "project_slug": {
      -        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "to": {
      -        "description": "REQUIRED for move — destination scope: 'trunk' for shared truth, or a workspace NAME (e.g. \"Billing\") or uuid to scope it there.",
      -        "minLength": 1,
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "action",
      -      "project_slug",
      -      "node_id",
      -      "to"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "action=\"link\": Create a typed semantic link between two nodes (src → dst). Idempotent: re-linking the same (src, dst, relation) returns the existing edge, no error.\n\nSome relations change the graph: `supersedes` retires dst (it stays findable, but comes back marked SUPERSEDED and ranked below live knowledge), and `expires-on` retires src later. Every relation except `relates-to` is a claim and needs a `reason`; `supersedes` also needs `evidence`. If you cannot state the reason, use `relates-to`.\n\n## What each link means — and what it does\n\nAn edge is not decoration. Some of these change the graph.\n\n| Relation | Direction | Use it when | What it actually does |\n|---|---|---|---|\n| `relates-to` | A → B | Someone who pulls up B should know A exists. The weakest signal — use it when nothing typed fits. | Nothing but a pointer. Both nodes stay active. |\n| `blocks` | child → parent | A must land before B can be completed or become true. Also the **subtask** relation. | Surfaces A as a subtask of B and drives B's progress. |\n| `fulfills` | task → knowledge | This task delivers or acts on that decision, goal, or risk. | Records that the work discharged the commitment. |\n| `caused-by` | A → B | B is the root cause of A. | Builds the causal chain a provenance walk follows. |\n| `contradicts` | A → B | Two facts conflict and you cannot tell which is true. | Flags it for a human. **Both stay active** — this is the honest \"I don't know\" edge. |\n| `supersedes` | new → old | The old node has become **wrong to act on** — an agent following it would now do the wrong thing. **Never** for something that is merely older: history that is still true must stay. | **Retires the old node.** It stays in the graph and stays findable, but it comes back marked `SUPERSEDED` and ranked below live knowledge — so nothing acts on it as current. |\n| `expires-on` | knowledge → task | This knowledge is only true until that task closes. | Retires the knowledge when the task completes — same marked-not-deleted treatment. |\n\n`mentions` is not linkable by hand — write the id into a body and it is created for you.",
      -    "properties": {
      -      "action": {
      -        "const": "link",
      -        "type": "string"
      -      },
      -      "dst_id": {
      -        "description": "REQUIRED for link — destination node shareable id.",
      -        "type": "string"
      -      },
      -      "evidence": {
      -        "description": "The VERBATIM line from the source that proves the claim — REQUIRED for `supersedes`. It must show the old state actually ENDED, not merely that something newer exists. \"We also added a read replica\" is additive and supersedes nothing; \"we've moved off DynamoDB entirely\" is a termination. If you cannot quote a termination, the correct relation is relates-to.",
      -        "maxLength": 4000,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "privacy_bypass": {
      -        "description": "true writes despite a privacy flag. Only meaningful after a flag; the user decides whether the content can be written.",
      -        "type": "boolean"
      -      },
      -      "privacy_false_positive": {
      -        "description": "With privacy_bypass: true reports the privacy flag as wrong.",
      -        "type": "boolean"
      -      },
      -      "privacy_judgement_id": {
      -        "description": "With privacy_bypass: the judgement_id the privacy flag returned.",
      -        "type": "string"
      -      },
      -      "privacy_note": {
      -        "description": "With privacy_bypass: an optional short reason, kept with the bypass.",
      -        "maxLength": 500,
      -        "type": "string"
      -      },
      -      "project_slug": {
      -        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "reason": {
      -        "description": "WHY this edge exists — REQUIRED for supersedes / contradicts / caused-by / blocks / fulfills. State the claim in your own words: for `supersedes`, name the standing state that ENDED (\"we moved the event store off DynamoDB to Postgres on Jul 13\"); for `caused-by`, name the causal mechanism. Not required for relates-to (the 'nothing typed fits' default) — if you cannot state a reason, that is the signal to use relates-to instead of forcing a typed edge.",
      -        "maxLength": 2000,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "relation": {
      -        "description": "REQUIRED for link — edge type, read src → dst. relates-to: a pointer, no claim. blocks: src (child) must land before dst (parent) can finish; the subtask edge. fulfills: src task delivers dst. caused-by: dst is src's root cause. contradicts: both stay active for a human to resolve. supersedes: retires dst as wrong to act on, not merely older. expires-on: src knowledge retires when dst task closes.",
      -        "enum": [
      -          "relates-to",
      -          "blocks",
      -          "fulfills",
      -          "contradicts",
      -          "supersedes",
      -          "caused-by",
      -          "expires-on"
      -        ],
      -        "type": "string"
      -      },
      -      "src_id": {
      -        "description": "REQUIRED for link — source node shareable id.",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "action",
      -      "project_slug",
      -      "src_id",
      -      "dst_id",
      -      "relation"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "description": "node action \"update\".",
      +    "properties": {
      +      "action": {
      +        "const": "update",
      +        "description": "Run the update action.",
      +        "type": "string"
      +      },
      +      "anchor_files": {
      +        "anyOf": [
      +          {
      +            "anyOf": [
      +              {
      +                "not": {}
      +              },
      +              {
      +                "items": {
      +                  "type": "string"
      +                },
      +                "type": "array"
      +              }
      +            ],
      +            "description": "Repo-relative file paths this note is about (e.g. 'src/auth/session.ts'). When someone later edits a file named here, this node surfaces to them. Set it on a lesson/risk/gap scoped to specific code so it resurfaces exactly when that code is touched."
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge or task nodes — repo-relative file paths this node is about; surfaces it when one is later edited (for a task: don't step on toes). Pass [] or null to clear."
      +      },
      +      "anchor_symbols": {
      +        "anyOf": [
      +          {
      +            "anyOf": [
      +              {
      +                "not": {}
      +              },
      +              {
      +                "items": {
      +                  "type": "string"
      +                },
      +                "type": "array"
      +              }
      +            ],
      +            "description": "Symbol names this note is about — function / class / constant / env var (e.g. 'refreshToken', 'SESSION_TTL'). Same purpose as anchor_files: a precise hook so the note resurfaces when that symbol's code is touched."
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge or task nodes — symbol names this node is about (function / class / constant / env var). Pass [] or null to clear."
      +      },
      +      "assignee_user_id": {
      +        "anyOf": [
      +          {
      +            "format": "uuid",
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) task nodes only — user uuid (not a shareable id)"
      +      },
      +      "body": {
      +        "description": "(update only) New markdown body (full replace) — mutually exclusive with edits. Write it in plain style: one idea per sentence, active voice, lead with the condition, one term per concept, exact technical names.",
      +        "type": [
      +          "string",
      +          "null"
      +        ]
      +      },
      +      "category": {
      +        "anyOf": [
      +          {
      +            "enum": [
      +              "decision",
      +              "architecture",
      +              "goal",
      +              "risk",
      +              "gap",
      +              "fyi",
      +              "opportunity",
      +              "priority",
      +              "lesson"
      +            ],
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge-type nodes only"
      +      },
      +      "due_at": {
      +        "anyOf": [
      +          {
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — ISO-8601 deadline. Pass null to clear. Any active knowledge with a past due_at surfaces in stats.overdue_commitments."
      +      },
      +      "edits": {
      +        "description": "(update only) Literal string-replacements applied to the body server-side, in order. Mutually exclusive with `body`. Pair with an exact search: use a matching line as old_string.",
      +        "items": {
      +          "additionalProperties": false,
      +          "properties": {
      +            "new_string": {
      +              "description": "Replacement text.",
      +              "type": "string"
      +            },
      +            "old_string": {
      +              "description": "Exact substring to find in the current body.",
      +              "minLength": 1,
      +              "type": "string"
      +            },
      +            "replace_all": {
      +              "description": "Replace every occurrence (default: require a unique match).",
      +              "type": "boolean"
      +            }
      +          },
      +          "required": [
      +            "old_string",
      +            "new_string"
      +          ],
      +          "type": "object"
      +        },
      +        "minItems": 1,
      +        "type": "array"
      +      },
      +      "expected_version": {
      +        "description": "(update only) Optimistic concurrency: reject the write if the node's current version differs (it changed since you read it).",
      +        "exclusiveMinimum": 0,
      +        "type": "integer"
      +      },
      +      "expires_at": {
      +        "anyOf": [
      +          {
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — ISO-8601 expiry (date TTL). Pass null to clear. When it passes, the next health-check run with fix archives the node. For ephemeral `fyi`/temporary-note knowledge; decision/architecture retire by supersession instead."
      +      },
      +      "hidden": {
      +        "description": "(update only) Set true to HIDE this node from every retrieval surface (search, recall, lists) — the one explicit invisibility. It stays fetchable by its shareable id (shown with a HIDDEN banner), so hiding is never a delete. Use for a genuine mistake, expired ephemera, or a secret that was pasted in by accident. Set false to un-hide. On the hosted assistant this pauses for human approval.",
      +        "type": "boolean"
      +      },
      +      "node_id": {
      +        "description": "REQUIRED for update — shareable id of the node to edit (TASK-N/KNOW-N/COMP-N/DOC-N/NODE-N).",
      +        "type": "string"
      +      },
      +      "parent_component_id": {
      +        "anyOf": [
      +          {
      +            "description": "Shareable id: TASK-N (task), KNOW-N (knowledge), COMP-N (component), DOC-N (document), or NODE-N (any).",
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) shareable id of the new parent component. Accepted for component, knowledge, task, and document nodes. null = move to project root (documents and components only). Same-project enforcement is implicit — short ids resolve only within the active project."
      +      },
      +      "priority": {
      +        "anyOf": [
      +          {
      +            "enum": [
      +              "P0",
      +              "P1",
      +              "P2",
      +              "P3"
      +            ],
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) task nodes only — priority P0 (most urgent) … P3. Pass null to clear."
      +      },
      +      "privacy_bypass": {
      +        "description": "true writes despite a privacy flag. Only meaningful after a flag; the user decides whether the content can be written.",
      +        "type": "boolean"
      +      },
      +      "privacy_false_positive": {
      +        "description": "With privacy_bypass: true reports the privacy flag as wrong.",
      +        "type": "boolean"
      +      },
      +      "privacy_judgement_id": {
      +        "description": "With privacy_bypass: the judgement_id the privacy flag returned.",
      +        "type": "string"
      +      },
      +      "privacy_note": {
      +        "description": "With privacy_bypass: an optional short reason, kept with the bypass.",
      +        "maxLength": 500,
      +        "type": "string"
      +      },
      +      "project_slug": {
      +        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "source": {
      +        "anyOf": [
      +          {
      +            "maxLength": 500,
      +            "minLength": 1,
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — free-form provenance label. Pass null to clear."
      +      },
      +      "source_type": {
      +        "description": "(update only) knowledge nodes only — provenance / trust signal (human, claude, import).",
      +        "enum": [
      +          "human",
      +          "claude",
      +          "import"
      +        ],
      +        "type": "string"
      +      },
      +      "source_url": {
      +        "anyOf": [
      +          {
      +            "format": "uri",
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — canonical URL for the source. Pass null to clear."
      +      },
      +      "status": {
      +        "description": "(update only) A task may be set to open or backlog here. Claim, completion, and cancellation must use the task lifecycle actions (claim, complete, cancel) so lifecycle records and agent identity remain atomic.",
      +        "enum": [
      +          "active",
      +          "archived",
      +          "backlog",
      +          "open",
      +          "in_progress",
      +          "done",
      +          "cancelled"
      +        ],
      +        "type": "string"
      +      },
      +      "title": {
      +        "description": "(update only) New title.",
      +        "maxLength": 300,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "topics": {
      +        "description": "(update only) knowledge or task nodes only",
      +        "items": {
      +          "type": "string"
      +        },
      +        "type": "array"
      +      }
      +    },
      +    "required": [
      +      "action",
      +      "project_slug",
      +      "node_id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "node action \"move\".",
      +    "properties": {
      +      "action": {
      +        "const": "move",
      +        "description": "Run the move action.",
      +        "type": "string"
      +      },
      +      "node_id": {
      +        "description": "REQUIRED for move — the node to move, a short-id (KNOW-N / TASK-N / DOC-N / COMP-N).",
      +        "type": "string"
      +      },
      +      "project_slug": {
      +        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "to": {
      +        "description": "REQUIRED for move — destination scope: 'trunk' for shared truth, or a workspace NAME (e.g. \"Billing\") or uuid to scope it there.",
      +        "minLength": 1,
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "action",
      +      "project_slug",
      +      "node_id",
      +      "to"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "node action \"link\".",
      +    "properties": {
      +      "action": {
      +        "const": "link",
      +        "description": "Run the link action.",
      +        "type": "string"
      +      },
      +      "dst_id": {
      +        "description": "REQUIRED for link — destination node shareable id.",
      +        "type": "string"
      +      },
      +      "evidence": {
      +        "description": "The VERBATIM line from the source that proves the claim — REQUIRED for `supersedes`. It must show the old state actually ENDED, not merely that something newer exists. \"We also added a read replica\" is additive and supersedes nothing; \"we've moved off DynamoDB entirely\" is a termination. If you cannot quote a termination, the correct relation is relates-to.",
      +        "maxLength": 4000,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "privacy_bypass": {
      +        "description": "true writes despite a privacy flag. Only meaningful after a flag; the user decides whether the content can be written.",
      +        "type": "boolean"
      +      },
      +      "privacy_false_positive": {
      +        "description": "With privacy_bypass: true reports the privacy flag as wrong.",
      +        "type": "boolean"
      +      },
      +      "privacy_judgement_id": {
      +        "description": "With privacy_bypass: the judgement_id the privacy flag returned.",
      +        "type": "string"
      +      },
      +      "privacy_note": {
      +        "description": "With privacy_bypass: an optional short reason, kept with the bypass.",
      +        "maxLength": 500,
      +        "type": "string"
      +      },
      +      "project_slug": {
      +        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "reason": {
      +        "description": "WHY this edge exists — REQUIRED for supersedes / contradicts / caused-by / blocks / fulfills. State the claim in your own words: for `supersedes`, name the standing state that ENDED (\"we moved the event store off DynamoDB to Postgres on Jul 13\"); for `caused-by`, name the causal mechanism. Not required for relates-to (the 'nothing typed fits' default) — if you cannot state a reason, that is the signal to use relates-to instead of forcing a typed edge.",
      +        "maxLength": 2000,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "relation": {
      +        "description": "REQUIRED for link — edge type, read src → dst. relates-to: a pointer, no claim. blocks: src (child) must land before dst (parent) can finish; the subtask edge. fulfills: src task delivers dst. caused-by: dst is src's root cause. contradicts: both stay active for a human to resolve. supersedes: retires dst as wrong to act on, not merely older. expires-on: src knowledge retires when dst task closes.",
      +        "enum": [
      +          "relates-to",
      +          "blocks",
      +          "fulfills",
      +          "contradicts",
      +          "supersedes",
      +          "caused-by",
      +          "expires-on"
      +        ],
      +        "type": "string"
      +      },
      +      "src_id": {
      +        "description": "REQUIRED for link — source node shareable id.",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "action",
      +      "project_slug",
      +      "src_id",
      +      "dst_id",
      +      "relation"
      +    ],
      +    "type": "object"
      +  }
      +]
  3. Changed1 schema field changed
    • changedInput schema / anyOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "description": "action=\"update\": Update editable fields on any node; each field is validated against what's valid for the node's type, so type-mismatched fields (e.g. category on a task) are rejected. A task's status may be set to open or backlog here, but claim, completion, and cancellation are REJECTED — use task(action=\"claim\") / task(action=\"complete\") so the claim and completion (with session + agent identity) are recorded atomically, or destructive(action=\"cancel_task\") so the destructive transition records a reason. Archiving a component is also rejected; use destructive(action=\"archive_component\") so active children cannot be orphaned. `parent_component_id` reparents across components in the same project. Surgical edits: pass `edits` (literal old_string→new_string replacements applied server-side) instead of `body` to fix a substring without resending the whole body — they're mutually exclusive, and a non-matching old_string errors rather than no-ops. Optional `expected_version` gives optimistic concurrency.",
      -    "properties": {
      -      "action": {
      -        "const": "update",
      -        "type": "string"
      -      },
      -      "anchor_files": {
      -        "anyOf": [
      -          {
      -            "anyOf": [
      -              {
      -                "not": {}
      -              },
      -              {
      -                "items": {
      -                  "type": "string"
      -                },
      -                "type": "array"
      -              }
      -            ],
      -            "description": "Repo-relative file paths this note is about (e.g. 'src/auth/session.ts'). When someone later edits a file named here, this node surfaces to them. Set it on a lesson/risk/gap scoped to specific code so it resurfaces exactly when that code is touched."
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge or task nodes — repo-relative file paths this node is about; surfaces it when one is later edited (for a task: don't step on toes). Pass [] or null to clear."
      -      },
      -      "anchor_symbols": {
      -        "anyOf": [
      -          {
      -            "anyOf": [
      -              {
      -                "not": {}
      -              },
      -              {
      -                "items": {
      -                  "type": "string"
      -                },
      -                "type": "array"
      -              }
      -            ],
      -            "description": "Symbol names this note is about — function / class / constant / env var (e.g. 'refreshToken', 'SESSION_TTL'). Same purpose as anchor_files: a precise hook so the note resurfaces when that symbol's code is touched."
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge or task nodes — symbol names this node is about (function / class / constant / env var). Pass [] or null to clear."
      -      },
      -      "assignee_user_id": {
      -        "anyOf": [
      -          {
      -            "format": "uuid",
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) task nodes only — user uuid (not a shareable id)"
      -      },
      -      "body": {
      -        "description": "(update only) New markdown body (full replace) — mutually exclusive with edits. Write it in plain style: one idea per sentence, active voice, lead with the condition, one term per concept, exact technical names.",
      -        "type": [
      -          "string",
      -          "null"
      -        ]
      -      },
      -      "category": {
      -        "anyOf": [
      -          {
      -            "enum": [
      -              "decision",
      -              "architecture",
      -              "goal",
      -              "risk",
      -              "gap",
      -              "fyi",
      -              "opportunity",
      -              "priority",
      -              "lesson"
      -            ],
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge-type nodes only"
      -      },
      -      "due_at": {
      -        "anyOf": [
      -          {
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — ISO-8601 deadline. Pass null to clear. Any active knowledge with a past due_at surfaces in stats.overdue_commitments."
      -      },
      -      "edits": {
      -        "description": "(update only) Literal string-replacements applied to the body server-side, in order. Mutually exclusive with `body`. Pair with an exact search: use a matching line as old_string.",
      -        "items": {
      -          "additionalProperties": false,
      -          "properties": {
      -            "new_string": {
      -              "description": "Replacement text.",
      -              "type": "string"
      -            },
      -            "old_string": {
      -              "description": "Exact substring to find in the current body.",
      -              "minLength": 1,
      -              "type": "string"
      -            },
      -            "replace_all": {
      -              "description": "Replace every occurrence (default: require a unique match).",
      -              "type": "boolean"
      -            }
      -          },
      -          "required": [
      -            "old_string",
      -            "new_string"
      -          ],
      -          "type": "object"
      -        },
      -        "minItems": 1,
      -        "type": "array"
      -      },
      -      "expected_version": {
      -        "description": "(update only) Optimistic concurrency: reject the write if the node's current version differs (it changed since you read it).",
      -        "exclusiveMinimum": 0,
      -        "type": "integer"
      -      },
      -      "expires_at": {
      -        "anyOf": [
      -          {
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — ISO-8601 expiry (date TTL). Pass null to clear. When it passes, the next health-check run with fix archives the node. For ephemeral `fyi`/temporary-note knowledge; decision/architecture retire by supersession instead."
      -      },
      -      "hidden": {
      -        "description": "(update only) Set true to HIDE this node from every retrieval surface (search, recall, lists) — the one explicit invisibility. It stays fetchable by its shareable id (shown with a HIDDEN banner), so hiding is never a delete. Use for a genuine mistake, expired ephemera, or a secret that was pasted in by accident. Set false to un-hide. On the hosted assistant this pauses for human approval.",
      -        "type": "boolean"
      -      },
      -      "node_id": {
      -        "description": "REQUIRED for update — shareable id of the node to edit (TASK-N/KNOW-N/COMP-N/DOC-N/NODE-N).",
      -        "type": "string"
      -      },
      -      "parent_component_id": {
      -        "anyOf": [
      -          {
      -            "description": "Shareable id: TASK-N (task), KNOW-N (knowledge), COMP-N (component), DOC-N (document), or NODE-N (any).",
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) shareable id of the new parent component. Accepted for component, knowledge, task, and document nodes. null = move to project root (documents and components only). Same-project enforcement is implicit — short ids resolve only within the active project."
      -      },
      -      "priority": {
      -        "anyOf": [
      -          {
      -            "enum": [
      -              "P0",
      -              "P1",
      -              "P2",
      -              "P3"
      -            ],
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) task nodes only — priority P0 (most urgent) … P3. Pass null to clear."
      -      },
      -      "privacy_bypass": {
      -        "description": "true writes despite a privacy flag on a public project. Only meaningful after a flag; the user decides whether the content can be public.",
      -        "type": "boolean"
      -      },
      -      "privacy_false_positive": {
      -        "description": "With privacy_bypass: true reports the privacy flag as wrong.",
      -        "type": "boolean"
      -      },
      -      "privacy_judgement_id": {
      -        "description": "With privacy_bypass: the judgement_id the privacy flag returned.",
      -        "type": "string"
      -      },
      -      "privacy_note": {
      -        "description": "With privacy_bypass: an optional short reason, kept with the bypass.",
      -        "maxLength": 500,
      -        "type": "string"
      -      },
      -      "project_slug": {
      -        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "source": {
      -        "anyOf": [
      -          {
      -            "maxLength": 500,
      -            "minLength": 1,
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — free-form provenance label. Pass null to clear."
      -      },
      -      "source_type": {
      -        "description": "(update only) knowledge nodes only — provenance / trust signal (human, claude, import).",
      -        "enum": [
      -          "human",
      -          "claude",
      -          "import"
      -        ],
      -        "type": "string"
      -      },
      -      "source_url": {
      -        "anyOf": [
      -          {
      -            "format": "uri",
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — canonical URL for the source. Pass null to clear."
      -      },
      -      "status": {
      -        "description": "(update only) A task may be set to open or backlog here. Claim, completion, and cancellation must use the task lifecycle actions (claim, complete, cancel) so lifecycle records and agent identity remain atomic.",
      -        "enum": [
      -          "active",
      -          "archived",
      -          "backlog",
      -          "open",
      -          "in_progress",
      -          "done",
      -          "cancelled"
      -        ],
      -        "type": "string"
      -      },
      -      "title": {
      -        "description": "(update only) New title.",
      -        "maxLength": 300,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "topics": {
      -        "description": "(update only) knowledge or task nodes only",
      -        "items": {
      -          "type": "string"
      -        },
      -        "type": "array"
      -      }
      -    },
      -    "required": [
      -      "action",
      -      "project_slug",
      -      "node_id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "action=\"move\": Move an EXISTING node between scopes — into a workspace (hiding it from trunk readers) or back to trunk (shared truth). Re-scopes by id; reversible, never deletes. Distinct from destructive(action=\"cherry_pick_workspace\") (which promotes to trunk WITH provenance) — This is a plain re-assignment in any direction.",
      -    "properties": {
      -      "action": {
      -        "const": "move",
      -        "type": "string"
      -      },
      -      "node_id": {
      -        "description": "REQUIRED for move — the node to move, a short-id (KNOW-N / TASK-N / DOC-N / COMP-N).",
      -        "type": "string"
      -      },
      -      "project_slug": {
      -        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "to": {
      -        "description": "REQUIRED for move — destination scope: 'trunk' for shared truth, or a workspace NAME (e.g. \"Billing\") or uuid to scope it there.",
      -        "minLength": 1,
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "action",
      -      "project_slug",
      -      "node_id",
      -      "to"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "action=\"link\": Create a typed semantic link between two nodes (src → dst). Idempotent: re-linking the same (src, dst, relation) returns the existing edge, no error.\n\nSome relations change the graph: `supersedes` retires dst (it stays findable, but comes back marked SUPERSEDED and ranked below live knowledge), and `expires-on` retires src later. Every relation except `relates-to` is a claim and needs a `reason`; `supersedes` also needs `evidence`. If you cannot state the reason, use `relates-to`.\n\n## What each link means — and what it does\n\nAn edge is not decoration. Some of these change the graph.\n\n| Relation | Direction | Use it when | What it actually does |\n|---|---|---|---|\n| `relates-to` | A → B | Someone who pulls up B should know A exists. The weakest signal — use it when nothing typed fits. | Nothing but a pointer. Both nodes stay active. |\n| `blocks` | child → parent | A must land before B can be completed or become true. Also the **subtask** relation. | Surfaces A as a subtask of B and drives B's progress. |\n| `fulfills` | task → knowledge | This task delivers or acts on that decision, goal, or risk. | Records that the work discharged the commitment. |\n| `caused-by` | A → B | B is the root cause of A. | Builds the causal chain a provenance walk follows. |\n| `contradicts` | A → B | Two facts conflict and you cannot tell which is true. | Flags it for a human. **Both stay active** — this is the honest \"I don't know\" edge. |\n| `supersedes` | new → old | The old node has become **wrong to act on** — an agent following it would now do the wrong thing. **Never** for something that is merely older: history that is still true must stay. | **Retires the old node.** It stays in the graph and stays findable, but it comes back marked `SUPERSEDED` and ranked below live knowledge — so nothing acts on it as current. |\n| `expires-on` | knowledge → task | This knowledge is only true until that task closes. | Retires the knowledge when the task completes — same marked-not-deleted treatment. |\n\n`mentions` is not linkable by hand — write the id into a body and it is created for you.",
      -    "properties": {
      -      "action": {
      -        "const": "link",
      -        "type": "string"
      -      },
      -      "dst_id": {
      -        "description": "REQUIRED for link — destination node shareable id.",
      -        "type": "string"
      -      },
      -      "evidence": {
      -        "description": "The VERBATIM line from the source that proves the claim — REQUIRED for `supersedes`. It must show the old state actually ENDED, not merely that something newer exists. \"We also added a read replica\" is additive and supersedes nothing; \"we've moved off DynamoDB entirely\" is a termination. If you cannot quote a termination, the correct relation is relates-to.",
      -        "maxLength": 4000,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "privacy_bypass": {
      -        "description": "true writes despite a privacy flag on a public project. Only meaningful after a flag; the user decides whether the content can be public.",
      -        "type": "boolean"
      -      },
      -      "privacy_false_positive": {
      -        "description": "With privacy_bypass: true reports the privacy flag as wrong.",
      -        "type": "boolean"
      -      },
      -      "privacy_judgement_id": {
      -        "description": "With privacy_bypass: the judgement_id the privacy flag returned.",
      -        "type": "string"
      -      },
      -      "privacy_note": {
      -        "description": "With privacy_bypass: an optional short reason, kept with the bypass.",
      -        "maxLength": 500,
      -        "type": "string"
      -      },
      -      "project_slug": {
      -        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "reason": {
      -        "description": "WHY this edge exists — REQUIRED for supersedes / contradicts / caused-by / blocks / fulfills. State the claim in your own words: for `supersedes`, name the standing state that ENDED (\"we moved the event store off DynamoDB to Postgres on Jul 13\"); for `caused-by`, name the causal mechanism. Not required for relates-to (the 'nothing typed fits' default) — if you cannot state a reason, that is the signal to use relates-to instead of forcing a typed edge.",
      -        "maxLength": 2000,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "relation": {
      -        "description": "REQUIRED for link — edge type, read src → dst. relates-to: a pointer, no claim. blocks: src (child) must land before dst (parent) can finish; the subtask edge. fulfills: src task delivers dst. caused-by: dst is src's root cause. contradicts: both stay active for a human to resolve. supersedes: retires dst as wrong to act on, not merely older. expires-on: src knowledge retires when dst task closes.",
      -        "enum": [
      -          "relates-to",
      -          "blocks",
      -          "fulfills",
      -          "contradicts",
      -          "supersedes",
      -          "caused-by",
      -          "expires-on"
      -        ],
      -        "type": "string"
      -      },
      -      "src_id": {
      -        "description": "REQUIRED for link — source node shareable id.",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "action",
      -      "project_slug",
      -      "src_id",
      -      "dst_id",
      -      "relation"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "description": "action=\"update\": Update editable fields on any node; each field is validated against what's valid for the node's type, so type-mismatched fields (e.g. category on a task) are rejected. A task's status may be set to open or backlog here, but claim, completion, and cancellation are REJECTED — use task(action=\"claim\") / task(action=\"complete\") so the claim and completion (with session + agent identity) are recorded atomically, or destructive(action=\"cancel_task\") so the destructive transition records a reason. Archiving a component is also rejected; use destructive(action=\"archive_component\") so active children cannot be orphaned. `parent_component_id` reparents across components in the same project. Surgical edits: pass `edits` (literal old_string→new_string replacements applied server-side) instead of `body` to fix a substring without resending the whole body — they're mutually exclusive, and a non-matching old_string errors rather than no-ops. Optional `expected_version` gives optimistic concurrency.",
      +    "properties": {
      +      "action": {
      +        "const": "update",
      +        "type": "string"
      +      },
      +      "anchor_files": {
      +        "anyOf": [
      +          {
      +            "anyOf": [
      +              {
      +                "not": {}
      +              },
      +              {
      +                "items": {
      +                  "type": "string"
      +                },
      +                "type": "array"
      +              }
      +            ],
      +            "description": "Repo-relative file paths this note is about (e.g. 'src/auth/session.ts'). When someone later edits a file named here, this node surfaces to them. Set it on a lesson/risk/gap scoped to specific code so it resurfaces exactly when that code is touched."
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge or task nodes — repo-relative file paths this node is about; surfaces it when one is later edited (for a task: don't step on toes). Pass [] or null to clear."
      +      },
      +      "anchor_symbols": {
      +        "anyOf": [
      +          {
      +            "anyOf": [
      +              {
      +                "not": {}
      +              },
      +              {
      +                "items": {
      +                  "type": "string"
      +                },
      +                "type": "array"
      +              }
      +            ],
      +            "description": "Symbol names this note is about — function / class / constant / env var (e.g. 'refreshToken', 'SESSION_TTL'). Same purpose as anchor_files: a precise hook so the note resurfaces when that symbol's code is touched."
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge or task nodes — symbol names this node is about (function / class / constant / env var). Pass [] or null to clear."
      +      },
      +      "assignee_user_id": {
      +        "anyOf": [
      +          {
      +            "format": "uuid",
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) task nodes only — user uuid (not a shareable id)"
      +      },
      +      "body": {
      +        "description": "(update only) New markdown body (full replace) — mutually exclusive with edits. Write it in plain style: one idea per sentence, active voice, lead with the condition, one term per concept, exact technical names.",
      +        "type": [
      +          "string",
      +          "null"
      +        ]
      +      },
      +      "category": {
      +        "anyOf": [
      +          {
      +            "enum": [
      +              "decision",
      +              "architecture",
      +              "goal",
      +              "risk",
      +              "gap",
      +              "fyi",
      +              "opportunity",
      +              "priority",
      +              "lesson"
      +            ],
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge-type nodes only"
      +      },
      +      "due_at": {
      +        "anyOf": [
      +          {
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — ISO-8601 deadline. Pass null to clear. Any active knowledge with a past due_at surfaces in stats.overdue_commitments."
      +      },
      +      "edits": {
      +        "description": "(update only) Literal string-replacements applied to the body server-side, in order. Mutually exclusive with `body`. Pair with an exact search: use a matching line as old_string.",
      +        "items": {
      +          "additionalProperties": false,
      +          "properties": {
      +            "new_string": {
      +              "description": "Replacement text.",
      +              "type": "string"
      +            },
      +            "old_string": {
      +              "description": "Exact substring to find in the current body.",
      +              "minLength": 1,
      +              "type": "string"
      +            },
      +            "replace_all": {
      +              "description": "Replace every occurrence (default: require a unique match).",
      +              "type": "boolean"
      +            }
      +          },
      +          "required": [
      +            "old_string",
      +            "new_string"
      +          ],
      +          "type": "object"
      +        },
      +        "minItems": 1,
      +        "type": "array"
      +      },
      +      "expected_version": {
      +        "description": "(update only) Optimistic concurrency: reject the write if the node's current version differs (it changed since you read it).",
      +        "exclusiveMinimum": 0,
      +        "type": "integer"
      +      },
      +      "expires_at": {
      +        "anyOf": [
      +          {
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — ISO-8601 expiry (date TTL). Pass null to clear. When it passes, the next health-check run with fix archives the node. For ephemeral `fyi`/temporary-note knowledge; decision/architecture retire by supersession instead."
      +      },
      +      "hidden": {
      +        "description": "(update only) Set true to HIDE this node from every retrieval surface (search, recall, lists) — the one explicit invisibility. It stays fetchable by its shareable id (shown with a HIDDEN banner), so hiding is never a delete. Use for a genuine mistake, expired ephemera, or a secret that was pasted in by accident. Set false to un-hide. On the hosted assistant this pauses for human approval.",
      +        "type": "boolean"
      +      },
      +      "node_id": {
      +        "description": "REQUIRED for update — shareable id of the node to edit (TASK-N/KNOW-N/COMP-N/DOC-N/NODE-N).",
      +        "type": "string"
      +      },
      +      "parent_component_id": {
      +        "anyOf": [
      +          {
      +            "description": "Shareable id: TASK-N (task), KNOW-N (knowledge), COMP-N (component), DOC-N (document), or NODE-N (any).",
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) shareable id of the new parent component. Accepted for component, knowledge, task, and document nodes. null = move to project root (documents and components only). Same-project enforcement is implicit — short ids resolve only within the active project."
      +      },
      +      "priority": {
      +        "anyOf": [
      +          {
      +            "enum": [
      +              "P0",
      +              "P1",
      +              "P2",
      +              "P3"
      +            ],
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) task nodes only — priority P0 (most urgent) … P3. Pass null to clear."
      +      },
      +      "privacy_bypass": {
      +        "description": "true writes despite a privacy flag. Only meaningful after a flag; the user decides whether the content can be written.",
      +        "type": "boolean"
      +      },
      +      "privacy_false_positive": {
      +        "description": "With privacy_bypass: true reports the privacy flag as wrong.",
      +        "type": "boolean"
      +      },
      +      "privacy_judgement_id": {
      +        "description": "With privacy_bypass: the judgement_id the privacy flag returned.",
      +        "type": "string"
      +      },
      +      "privacy_note": {
      +        "description": "With privacy_bypass: an optional short reason, kept with the bypass.",
      +        "maxLength": 500,
      +        "type": "string"
      +      },
      +      "project_slug": {
      +        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "source": {
      +        "anyOf": [
      +          {
      +            "maxLength": 500,
      +            "minLength": 1,
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — free-form provenance label. Pass null to clear."
      +      },
      +      "source_type": {
      +        "description": "(update only) knowledge nodes only — provenance / trust signal (human, claude, import).",
      +        "enum": [
      +          "human",
      +          "claude",
      +          "import"
      +        ],
      +        "type": "string"
      +      },
      +      "source_url": {
      +        "anyOf": [
      +          {
      +            "format": "uri",
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — canonical URL for the source. Pass null to clear."
      +      },
      +      "status": {
      +        "description": "(update only) A task may be set to open or backlog here. Claim, completion, and cancellation must use the task lifecycle actions (claim, complete, cancel) so lifecycle records and agent identity remain atomic.",
      +        "enum": [
      +          "active",
      +          "archived",
      +          "backlog",
      +          "open",
      +          "in_progress",
      +          "done",
      +          "cancelled"
      +        ],
      +        "type": "string"
      +      },
      +      "title": {
      +        "description": "(update only) New title.",
      +        "maxLength": 300,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "topics": {
      +        "description": "(update only) knowledge or task nodes only",
      +        "items": {
      +          "type": "string"
      +        },
      +        "type": "array"
      +      }
      +    },
      +    "required": [
      +      "action",
      +      "project_slug",
      +      "node_id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "action=\"move\": Move an EXISTING node between scopes — into a workspace (hiding it from trunk readers) or back to trunk (shared truth). Re-scopes by id; reversible, never deletes. Distinct from destructive(action=\"cherry_pick_workspace\") (which promotes to trunk WITH provenance) — This is a plain re-assignment in any direction.",
      +    "properties": {
      +      "action": {
      +        "const": "move",
      +        "type": "string"
      +      },
      +      "node_id": {
      +        "description": "REQUIRED for move — the node to move, a short-id (KNOW-N / TASK-N / DOC-N / COMP-N).",
      +        "type": "string"
      +      },
      +      "project_slug": {
      +        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "to": {
      +        "description": "REQUIRED for move — destination scope: 'trunk' for shared truth, or a workspace NAME (e.g. \"Billing\") or uuid to scope it there.",
      +        "minLength": 1,
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "action",
      +      "project_slug",
      +      "node_id",
      +      "to"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "action=\"link\": Create a typed semantic link between two nodes (src → dst). Idempotent: re-linking the same (src, dst, relation) returns the existing edge, no error.\n\nSome relations change the graph: `supersedes` retires dst (it stays findable, but comes back marked SUPERSEDED and ranked below live knowledge), and `expires-on` retires src later. Every relation except `relates-to` is a claim and needs a `reason`; `supersedes` also needs `evidence`. If you cannot state the reason, use `relates-to`.\n\n## What each link means — and what it does\n\nAn edge is not decoration. Some of these change the graph.\n\n| Relation | Direction | Use it when | What it actually does |\n|---|---|---|---|\n| `relates-to` | A → B | Someone who pulls up B should know A exists. The weakest signal — use it when nothing typed fits. | Nothing but a pointer. Both nodes stay active. |\n| `blocks` | child → parent | A must land before B can be completed or become true. Also the **subtask** relation. | Surfaces A as a subtask of B and drives B's progress. |\n| `fulfills` | task → knowledge | This task delivers or acts on that decision, goal, or risk. | Records that the work discharged the commitment. |\n| `caused-by` | A → B | B is the root cause of A. | Builds the causal chain a provenance walk follows. |\n| `contradicts` | A → B | Two facts conflict and you cannot tell which is true. | Flags it for a human. **Both stay active** — this is the honest \"I don't know\" edge. |\n| `supersedes` | new → old | The old node has become **wrong to act on** — an agent following it would now do the wrong thing. **Never** for something that is merely older: history that is still true must stay. | **Retires the old node.** It stays in the graph and stays findable, but it comes back marked `SUPERSEDED` and ranked below live knowledge — so nothing acts on it as current. |\n| `expires-on` | knowledge → task | This knowledge is only true until that task closes. | Retires the knowledge when the task completes — same marked-not-deleted treatment. |\n\n`mentions` is not linkable by hand — write the id into a body and it is created for you.",
      +    "properties": {
      +      "action": {
      +        "const": "link",
      +        "type": "string"
      +      },
      +      "dst_id": {
      +        "description": "REQUIRED for link — destination node shareable id.",
      +        "type": "string"
      +      },
      +      "evidence": {
      +        "description": "The VERBATIM line from the source that proves the claim — REQUIRED for `supersedes`. It must show the old state actually ENDED, not merely that something newer exists. \"We also added a read replica\" is additive and supersedes nothing; \"we've moved off DynamoDB entirely\" is a termination. If you cannot quote a termination, the correct relation is relates-to.",
      +        "maxLength": 4000,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "privacy_bypass": {
      +        "description": "true writes despite a privacy flag. Only meaningful after a flag; the user decides whether the content can be written.",
      +        "type": "boolean"
      +      },
      +      "privacy_false_positive": {
      +        "description": "With privacy_bypass: true reports the privacy flag as wrong.",
      +        "type": "boolean"
      +      },
      +      "privacy_judgement_id": {
      +        "description": "With privacy_bypass: the judgement_id the privacy flag returned.",
      +        "type": "string"
      +      },
      +      "privacy_note": {
      +        "description": "With privacy_bypass: an optional short reason, kept with the bypass.",
      +        "maxLength": 500,
      +        "type": "string"
      +      },
      +      "project_slug": {
      +        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "reason": {
      +        "description": "WHY this edge exists — REQUIRED for supersedes / contradicts / caused-by / blocks / fulfills. State the claim in your own words: for `supersedes`, name the standing state that ENDED (\"we moved the event store off DynamoDB to Postgres on Jul 13\"); for `caused-by`, name the causal mechanism. Not required for relates-to (the 'nothing typed fits' default) — if you cannot state a reason, that is the signal to use relates-to instead of forcing a typed edge.",
      +        "maxLength": 2000,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "relation": {
      +        "description": "REQUIRED for link — edge type, read src → dst. relates-to: a pointer, no claim. blocks: src (child) must land before dst (parent) can finish; the subtask edge. fulfills: src task delivers dst. caused-by: dst is src's root cause. contradicts: both stay active for a human to resolve. supersedes: retires dst as wrong to act on, not merely older. expires-on: src knowledge retires when dst task closes.",
      +        "enum": [
      +          "relates-to",
      +          "blocks",
      +          "fulfills",
      +          "contradicts",
      +          "supersedes",
      +          "caused-by",
      +          "expires-on"
      +        ],
      +        "type": "string"
      +      },
      +      "src_id": {
      +        "description": "REQUIRED for link — source node shareable id.",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "action",
      +      "project_slug",
      +      "src_id",
      +      "dst_id",
      +      "relation"
      +    ],
      +    "type": "object"
      +  }
      +]
  4. Changed1 schema field changed
    • changedInput schema / anyOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "description": "action=\"update\": Update editable fields on any node; each field is validated against what's valid for the node's type, so type-mismatched fields (e.g. category on a task) are rejected. A task's status may be set to open or backlog here, but claim, completion, and cancellation are REJECTED — use task(action=\"claim\") / task(action=\"complete\") so the claim and completion (with session + agent identity) are recorded atomically, or destructive(action=\"cancel_task\") so the destructive transition records a reason. Archiving a component is also rejected; use destructive(action=\"archive_component\") so active children cannot be orphaned. `parent_component_id` reparents across components in the same project. Surgical edits: pass `edits` (literal old_string→new_string replacements applied server-side) instead of `body` to fix a substring without resending the whole body — they're mutually exclusive, and a non-matching old_string errors rather than no-ops. Optional `expected_version` gives optimistic concurrency.",
      -    "properties": {
      -      "action": {
      -        "const": "update",
      -        "type": "string"
      -      },
      -      "anchor_files": {
      -        "anyOf": [
      -          {
      -            "anyOf": [
      -              {
      -                "not": {}
      -              },
      -              {
      -                "items": {
      -                  "type": "string"
      -                },
      -                "type": "array"
      -              }
      -            ],
      -            "description": "Repo-relative file paths this note is about (e.g. 'src/auth/session.ts'). When someone later edits a file named here, this node surfaces to them. Set it on a lesson/risk/gap scoped to specific code so it resurfaces exactly when that code is touched."
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge or task nodes — repo-relative file paths this node is about; surfaces it when one is later edited (for a task: don't step on toes). Pass [] or null to clear."
      -      },
      -      "anchor_symbols": {
      -        "anyOf": [
      -          {
      -            "anyOf": [
      -              {
      -                "not": {}
      -              },
      -              {
      -                "items": {
      -                  "type": "string"
      -                },
      -                "type": "array"
      -              }
      -            ],
      -            "description": "Symbol names this note is about — function / class / constant / env var (e.g. 'refreshToken', 'SESSION_TTL'). Same purpose as anchor_files: a precise hook so the note resurfaces when that symbol's code is touched."
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge or task nodes — symbol names this node is about (function / class / constant / env var). Pass [] or null to clear."
      -      },
      -      "assignee_user_id": {
      -        "anyOf": [
      -          {
      -            "format": "uuid",
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) task nodes only — user uuid (not a shareable id)"
      -      },
      -      "body": {
      -        "description": "(update only) New markdown body (full replace) — mutually exclusive with edits. Write it in plain style: one idea per sentence, active voice, lead with the condition, one term per concept, exact technical names.",
      -        "type": [
      -          "string",
      -          "null"
      -        ]
      -      },
      -      "category": {
      -        "anyOf": [
      -          {
      -            "enum": [
      -              "decision",
      -              "architecture",
      -              "goal",
      -              "risk",
      -              "gap",
      -              "fyi",
      -              "opportunity",
      -              "priority",
      -              "lesson"
      -            ],
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge-type nodes only"
      -      },
      -      "due_at": {
      -        "anyOf": [
      -          {
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — ISO-8601 deadline. Pass null to clear. Any active knowledge with a past due_at surfaces in stats.overdue_commitments."
      -      },
      -      "edits": {
      -        "description": "(update only) Literal string-replacements applied to the body server-side, in order. Mutually exclusive with `body`. Pair with an exact search: use a matching line as old_string.",
      -        "items": {
      -          "additionalProperties": false,
      -          "properties": {
      -            "new_string": {
      -              "description": "Replacement text.",
      -              "type": "string"
      -            },
      -            "old_string": {
      -              "description": "Exact substring to find in the current body.",
      -              "minLength": 1,
      -              "type": "string"
      -            },
      -            "replace_all": {
      -              "description": "Replace every occurrence (default: require a unique match).",
      -              "type": "boolean"
      -            }
      -          },
      -          "required": [
      -            "old_string",
      -            "new_string"
      -          ],
      -          "type": "object"
      -        },
      -        "minItems": 1,
      -        "type": "array"
      -      },
      -      "expected_version": {
      -        "description": "(update only) Optimistic concurrency: reject the write if the node's current version differs (it changed since you read it).",
      -        "exclusiveMinimum": 0,
      -        "type": "integer"
      -      },
      -      "expires_at": {
      -        "anyOf": [
      -          {
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — ISO-8601 expiry (date TTL). Pass null to clear. When it passes, the next health-check run with fix archives the node. For ephemeral `fyi`/temporary-note knowledge; decision/architecture retire by supersession instead."
      -      },
      -      "hidden": {
      -        "description": "(update only) Set true to HIDE this node from every retrieval surface (search, recall, lists) — the one explicit invisibility. It stays fetchable by its shareable id (shown with a HIDDEN banner), so hiding is never a delete. Use for a genuine mistake, expired ephemera, or a secret that was pasted in by accident. Set false to un-hide. On the hosted assistant this pauses for human approval.",
      -        "type": "boolean"
      -      },
      -      "node_id": {
      -        "description": "REQUIRED for update — shareable id of the node to edit (TASK-N/KNOW-N/COMP-N/DOC-N/NODE-N).",
      -        "type": "string"
      -      },
      -      "parent_component_id": {
      -        "anyOf": [
      -          {
      -            "description": "Shareable id: TASK-N (task), KNOW-N (knowledge), COMP-N (component), DOC-N (document), or NODE-N (any).",
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) shareable id of the new parent component. Accepted for component, knowledge, task, and document nodes. null = move to project root (documents and components only). Same-project enforcement is implicit — short ids resolve only within the active project."
      -      },
      -      "priority": {
      -        "anyOf": [
      -          {
      -            "enum": [
      -              "P0",
      -              "P1",
      -              "P2",
      -              "P3"
      -            ],
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) task nodes only — priority P0 (most urgent) … P3. Pass null to clear."
      -      },
      -      "project_slug": {
      -        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "source": {
      -        "anyOf": [
      -          {
      -            "maxLength": 500,
      -            "minLength": 1,
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — free-form provenance label. Pass null to clear."
      -      },
      -      "source_type": {
      -        "description": "(update only) knowledge nodes only — provenance / trust signal (human, claude, import).",
      -        "enum": [
      -          "human",
      -          "claude",
      -          "import"
      -        ],
      -        "type": "string"
      -      },
      -      "source_url": {
      -        "anyOf": [
      -          {
      -            "format": "uri",
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — canonical URL for the source. Pass null to clear."
      -      },
      -      "status": {
      -        "description": "(update only) A task may be set to open or backlog here. Claim, completion, and cancellation must use the task lifecycle actions (claim, complete, cancel) so lifecycle records and agent identity remain atomic.",
      -        "enum": [
      -          "active",
      -          "archived",
      -          "backlog",
      -          "open",
      -          "in_progress",
      -          "done",
      -          "cancelled"
      -        ],
      -        "type": "string"
      -      },
      -      "title": {
      -        "description": "(update only) New title.",
      -        "maxLength": 300,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "topics": {
      -        "description": "(update only) knowledge or task nodes only",
      -        "items": {
      -          "type": "string"
      -        },
      -        "type": "array"
      -      }
      -    },
      -    "required": [
      -      "action",
      -      "project_slug",
      -      "node_id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "action=\"move\": Move an EXISTING node between scopes — into a workspace (hiding it from trunk readers) or back to trunk (shared truth). Re-scopes by id; reversible, never deletes. Distinct from destructive(action=\"cherry_pick_workspace\") (which promotes to trunk WITH provenance) — This is a plain re-assignment in any direction.",
      -    "properties": {
      -      "action": {
      -        "const": "move",
      -        "type": "string"
      -      },
      -      "node_id": {
      -        "description": "REQUIRED for move — the node to move, a short-id (KNOW-N / TASK-N / DOC-N / COMP-N).",
      -        "type": "string"
      -      },
      -      "project_slug": {
      -        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "to": {
      -        "description": "REQUIRED for move — destination scope: 'trunk' for shared truth, or a workspace NAME (e.g. \"Billing\") or uuid to scope it there.",
      -        "minLength": 1,
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "action",
      -      "project_slug",
      -      "node_id",
      -      "to"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "action=\"link\": Create a typed semantic link between two nodes (src → dst). Idempotent: re-linking the same (src, dst, relation) returns the existing edge, no error.\n\nSome relations change the graph: `supersedes` retires dst (it stays findable, but comes back marked SUPERSEDED and ranked below live knowledge), and `expires-on` retires src later. Every relation except `relates-to` is a claim and needs a `reason`; `supersedes` also needs `evidence`. If you cannot state the reason, use `relates-to`.\n\n## What each link means — and what it does\n\nAn edge is not decoration. Some of these change the graph.\n\n| Relation | Direction | Use it when | What it actually does |\n|---|---|---|---|\n| `relates-to` | A → B | Someone who pulls up B should know A exists. The weakest signal — use it when nothing typed fits. | Nothing but a pointer. Both nodes stay active. |\n| `blocks` | child → parent | A must land before B can be completed or become true. Also the **subtask** relation. | Surfaces A as a subtask of B and drives B's progress. |\n| `fulfills` | task → knowledge | This task delivers or acts on that decision, goal, or risk. | Records that the work discharged the commitment. |\n| `caused-by` | A → B | B is the root cause of A. | Builds the causal chain a provenance walk follows. |\n| `contradicts` | A → B | Two facts conflict and you cannot tell which is true. | Flags it for a human. **Both stay active** — this is the honest \"I don't know\" edge. |\n| `supersedes` | new → old | The old node has become **wrong to act on** — an agent following it would now do the wrong thing. **Never** for something that is merely older: history that is still true must stay. | **Retires the old node.** It stays in the graph and stays findable, but it comes back marked `SUPERSEDED` and ranked below live knowledge — so nothing acts on it as current. |\n| `expires-on` | knowledge → task | This knowledge is only true until that task closes. | Retires the knowledge when the task completes — same marked-not-deleted treatment. |\n\n`mentions` is not linkable by hand — write the id into a body and it is created for you.",
      -    "properties": {
      -      "action": {
      -        "const": "link",
      -        "type": "string"
      -      },
      -      "dst_id": {
      -        "description": "REQUIRED for link — destination node shareable id.",
      -        "type": "string"
      -      },
      -      "evidence": {
      -        "description": "The VERBATIM line from the source that proves the claim — REQUIRED for `supersedes`. It must show the old state actually ENDED, not merely that something newer exists. \"We also added a read replica\" is additive and supersedes nothing; \"we've moved off DynamoDB entirely\" is a termination. If you cannot quote a termination, the correct relation is relates-to.",
      -        "maxLength": 4000,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "project_slug": {
      -        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "reason": {
      -        "description": "WHY this edge exists — REQUIRED for supersedes / contradicts / caused-by / blocks / fulfills. State the claim in your own words: for `supersedes`, name the standing state that ENDED (\"we moved the event store off DynamoDB to Postgres on Jul 13\"); for `caused-by`, name the causal mechanism. Not required for relates-to (the 'nothing typed fits' default) — if you cannot state a reason, that is the signal to use relates-to instead of forcing a typed edge.",
      -        "maxLength": 2000,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "relation": {
      -        "description": "REQUIRED for link — edge type, read src → dst. relates-to: a pointer, no claim. blocks: src (child) must land before dst (parent) can finish; the subtask edge. fulfills: src task delivers dst. caused-by: dst is src's root cause. contradicts: both stay active for a human to resolve. supersedes: retires dst as wrong to act on, not merely older. expires-on: src knowledge retires when dst task closes.",
      -        "enum": [
      -          "relates-to",
      -          "blocks",
      -          "fulfills",
      -          "contradicts",
      -          "supersedes",
      -          "caused-by",
      -          "expires-on"
      -        ],
      -        "type": "string"
      -      },
      -      "src_id": {
      -        "description": "REQUIRED for link — source node shareable id.",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "action",
      -      "project_slug",
      -      "src_id",
      -      "dst_id",
      -      "relation"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "description": "action=\"update\": Update editable fields on any node; each field is validated against what's valid for the node's type, so type-mismatched fields (e.g. category on a task) are rejected. A task's status may be set to open or backlog here, but claim, completion, and cancellation are REJECTED — use task(action=\"claim\") / task(action=\"complete\") so the claim and completion (with session + agent identity) are recorded atomically, or destructive(action=\"cancel_task\") so the destructive transition records a reason. Archiving a component is also rejected; use destructive(action=\"archive_component\") so active children cannot be orphaned. `parent_component_id` reparents across components in the same project. Surgical edits: pass `edits` (literal old_string→new_string replacements applied server-side) instead of `body` to fix a substring without resending the whole body — they're mutually exclusive, and a non-matching old_string errors rather than no-ops. Optional `expected_version` gives optimistic concurrency.",
      +    "properties": {
      +      "action": {
      +        "const": "update",
      +        "type": "string"
      +      },
      +      "anchor_files": {
      +        "anyOf": [
      +          {
      +            "anyOf": [
      +              {
      +                "not": {}
      +              },
      +              {
      +                "items": {
      +                  "type": "string"
      +                },
      +                "type": "array"
      +              }
      +            ],
      +            "description": "Repo-relative file paths this note is about (e.g. 'src/auth/session.ts'). When someone later edits a file named here, this node surfaces to them. Set it on a lesson/risk/gap scoped to specific code so it resurfaces exactly when that code is touched."
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge or task nodes — repo-relative file paths this node is about; surfaces it when one is later edited (for a task: don't step on toes). Pass [] or null to clear."
      +      },
      +      "anchor_symbols": {
      +        "anyOf": [
      +          {
      +            "anyOf": [
      +              {
      +                "not": {}
      +              },
      +              {
      +                "items": {
      +                  "type": "string"
      +                },
      +                "type": "array"
      +              }
      +            ],
      +            "description": "Symbol names this note is about — function / class / constant / env var (e.g. 'refreshToken', 'SESSION_TTL'). Same purpose as anchor_files: a precise hook so the note resurfaces when that symbol's code is touched."
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge or task nodes — symbol names this node is about (function / class / constant / env var). Pass [] or null to clear."
      +      },
      +      "assignee_user_id": {
      +        "anyOf": [
      +          {
      +            "format": "uuid",
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) task nodes only — user uuid (not a shareable id)"
      +      },
      +      "body": {
      +        "description": "(update only) New markdown body (full replace) — mutually exclusive with edits. Write it in plain style: one idea per sentence, active voice, lead with the condition, one term per concept, exact technical names.",
      +        "type": [
      +          "string",
      +          "null"
      +        ]
      +      },
      +      "category": {
      +        "anyOf": [
      +          {
      +            "enum": [
      +              "decision",
      +              "architecture",
      +              "goal",
      +              "risk",
      +              "gap",
      +              "fyi",
      +              "opportunity",
      +              "priority",
      +              "lesson"
      +            ],
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge-type nodes only"
      +      },
      +      "due_at": {
      +        "anyOf": [
      +          {
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — ISO-8601 deadline. Pass null to clear. Any active knowledge with a past due_at surfaces in stats.overdue_commitments."
      +      },
      +      "edits": {
      +        "description": "(update only) Literal string-replacements applied to the body server-side, in order. Mutually exclusive with `body`. Pair with an exact search: use a matching line as old_string.",
      +        "items": {
      +          "additionalProperties": false,
      +          "properties": {
      +            "new_string": {
      +              "description": "Replacement text.",
      +              "type": "string"
      +            },
      +            "old_string": {
      +              "description": "Exact substring to find in the current body.",
      +              "minLength": 1,
      +              "type": "string"
      +            },
      +            "replace_all": {
      +              "description": "Replace every occurrence (default: require a unique match).",
      +              "type": "boolean"
      +            }
      +          },
      +          "required": [
      +            "old_string",
      +            "new_string"
      +          ],
      +          "type": "object"
      +        },
      +        "minItems": 1,
      +        "type": "array"
      +      },
      +      "expected_version": {
      +        "description": "(update only) Optimistic concurrency: reject the write if the node's current version differs (it changed since you read it).",
      +        "exclusiveMinimum": 0,
      +        "type": "integer"
      +      },
      +      "expires_at": {
      +        "anyOf": [
      +          {
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — ISO-8601 expiry (date TTL). Pass null to clear. When it passes, the next health-check run with fix archives the node. For ephemeral `fyi`/temporary-note knowledge; decision/architecture retire by supersession instead."
      +      },
      +      "hidden": {
      +        "description": "(update only) Set true to HIDE this node from every retrieval surface (search, recall, lists) — the one explicit invisibility. It stays fetchable by its shareable id (shown with a HIDDEN banner), so hiding is never a delete. Use for a genuine mistake, expired ephemera, or a secret that was pasted in by accident. Set false to un-hide. On the hosted assistant this pauses for human approval.",
      +        "type": "boolean"
      +      },
      +      "node_id": {
      +        "description": "REQUIRED for update — shareable id of the node to edit (TASK-N/KNOW-N/COMP-N/DOC-N/NODE-N).",
      +        "type": "string"
      +      },
      +      "parent_component_id": {
      +        "anyOf": [
      +          {
      +            "description": "Shareable id: TASK-N (task), KNOW-N (knowledge), COMP-N (component), DOC-N (document), or NODE-N (any).",
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) shareable id of the new parent component. Accepted for component, knowledge, task, and document nodes. null = move to project root (documents and components only). Same-project enforcement is implicit — short ids resolve only within the active project."
      +      },
      +      "priority": {
      +        "anyOf": [
      +          {
      +            "enum": [
      +              "P0",
      +              "P1",
      +              "P2",
      +              "P3"
      +            ],
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) task nodes only — priority P0 (most urgent) … P3. Pass null to clear."
      +      },
      +      "privacy_bypass": {
      +        "description": "true writes despite a privacy flag on a public project. Only meaningful after a flag; the user decides whether the content can be public.",
      +        "type": "boolean"
      +      },
      +      "privacy_false_positive": {
      +        "description": "With privacy_bypass: true reports the privacy flag as wrong.",
      +        "type": "boolean"
      +      },
      +      "privacy_judgement_id": {
      +        "description": "With privacy_bypass: the judgement_id the privacy flag returned.",
      +        "type": "string"
      +      },
      +      "privacy_note": {
      +        "description": "With privacy_bypass: an optional short reason, kept with the bypass.",
      +        "maxLength": 500,
      +        "type": "string"
      +      },
      +      "project_slug": {
      +        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "source": {
      +        "anyOf": [
      +          {
      +            "maxLength": 500,
      +            "minLength": 1,
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — free-form provenance label. Pass null to clear."
      +      },
      +      "source_type": {
      +        "description": "(update only) knowledge nodes only — provenance / trust signal (human, claude, import).",
      +        "enum": [
      +          "human",
      +          "claude",
      +          "import"
      +        ],
      +        "type": "string"
      +      },
      +      "source_url": {
      +        "anyOf": [
      +          {
      +            "format": "uri",
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — canonical URL for the source. Pass null to clear."
      +      },
      +      "status": {
      +        "description": "(update only) A task may be set to open or backlog here. Claim, completion, and cancellation must use the task lifecycle actions (claim, complete, cancel) so lifecycle records and agent identity remain atomic.",
      +        "enum": [
      +          "active",
      +          "archived",
      +          "backlog",
      +          "open",
      +          "in_progress",
      +          "done",
      +          "cancelled"
      +        ],
      +        "type": "string"
      +      },
      +      "title": {
      +        "description": "(update only) New title.",
      +        "maxLength": 300,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "topics": {
      +        "description": "(update only) knowledge or task nodes only",
      +        "items": {
      +          "type": "string"
      +        },
      +        "type": "array"
      +      }
      +    },
      +    "required": [
      +      "action",
      +      "project_slug",
      +      "node_id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "action=\"move\": Move an EXISTING node between scopes — into a workspace (hiding it from trunk readers) or back to trunk (shared truth). Re-scopes by id; reversible, never deletes. Distinct from destructive(action=\"cherry_pick_workspace\") (which promotes to trunk WITH provenance) — This is a plain re-assignment in any direction.",
      +    "properties": {
      +      "action": {
      +        "const": "move",
      +        "type": "string"
      +      },
      +      "node_id": {
      +        "description": "REQUIRED for move — the node to move, a short-id (KNOW-N / TASK-N / DOC-N / COMP-N).",
      +        "type": "string"
      +      },
      +      "project_slug": {
      +        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "to": {
      +        "description": "REQUIRED for move — destination scope: 'trunk' for shared truth, or a workspace NAME (e.g. \"Billing\") or uuid to scope it there.",
      +        "minLength": 1,
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "action",
      +      "project_slug",
      +      "node_id",
      +      "to"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "action=\"link\": Create a typed semantic link between two nodes (src → dst). Idempotent: re-linking the same (src, dst, relation) returns the existing edge, no error.\n\nSome relations change the graph: `supersedes` retires dst (it stays findable, but comes back marked SUPERSEDED and ranked below live knowledge), and `expires-on` retires src later. Every relation except `relates-to` is a claim and needs a `reason`; `supersedes` also needs `evidence`. If you cannot state the reason, use `relates-to`.\n\n## What each link means — and what it does\n\nAn edge is not decoration. Some of these change the graph.\n\n| Relation | Direction | Use it when | What it actually does |\n|---|---|---|---|\n| `relates-to` | A → B | Someone who pulls up B should know A exists. The weakest signal — use it when nothing typed fits. | Nothing but a pointer. Both nodes stay active. |\n| `blocks` | child → parent | A must land before B can be completed or become true. Also the **subtask** relation. | Surfaces A as a subtask of B and drives B's progress. |\n| `fulfills` | task → knowledge | This task delivers or acts on that decision, goal, or risk. | Records that the work discharged the commitment. |\n| `caused-by` | A → B | B is the root cause of A. | Builds the causal chain a provenance walk follows. |\n| `contradicts` | A → B | Two facts conflict and you cannot tell which is true. | Flags it for a human. **Both stay active** — this is the honest \"I don't know\" edge. |\n| `supersedes` | new → old | The old node has become **wrong to act on** — an agent following it would now do the wrong thing. **Never** for something that is merely older: history that is still true must stay. | **Retires the old node.** It stays in the graph and stays findable, but it comes back marked `SUPERSEDED` and ranked below live knowledge — so nothing acts on it as current. |\n| `expires-on` | knowledge → task | This knowledge is only true until that task closes. | Retires the knowledge when the task completes — same marked-not-deleted treatment. |\n\n`mentions` is not linkable by hand — write the id into a body and it is created for you.",
      +    "properties": {
      +      "action": {
      +        "const": "link",
      +        "type": "string"
      +      },
      +      "dst_id": {
      +        "description": "REQUIRED for link — destination node shareable id.",
      +        "type": "string"
      +      },
      +      "evidence": {
      +        "description": "The VERBATIM line from the source that proves the claim — REQUIRED for `supersedes`. It must show the old state actually ENDED, not merely that something newer exists. \"We also added a read replica\" is additive and supersedes nothing; \"we've moved off DynamoDB entirely\" is a termination. If you cannot quote a termination, the correct relation is relates-to.",
      +        "maxLength": 4000,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "privacy_bypass": {
      +        "description": "true writes despite a privacy flag on a public project. Only meaningful after a flag; the user decides whether the content can be public.",
      +        "type": "boolean"
      +      },
      +      "privacy_false_positive": {
      +        "description": "With privacy_bypass: true reports the privacy flag as wrong.",
      +        "type": "boolean"
      +      },
      +      "privacy_judgement_id": {
      +        "description": "With privacy_bypass: the judgement_id the privacy flag returned.",
      +        "type": "string"
      +      },
      +      "privacy_note": {
      +        "description": "With privacy_bypass: an optional short reason, kept with the bypass.",
      +        "maxLength": 500,
      +        "type": "string"
      +      },
      +      "project_slug": {
      +        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "reason": {
      +        "description": "WHY this edge exists — REQUIRED for supersedes / contradicts / caused-by / blocks / fulfills. State the claim in your own words: for `supersedes`, name the standing state that ENDED (\"we moved the event store off DynamoDB to Postgres on Jul 13\"); for `caused-by`, name the causal mechanism. Not required for relates-to (the 'nothing typed fits' default) — if you cannot state a reason, that is the signal to use relates-to instead of forcing a typed edge.",
      +        "maxLength": 2000,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "relation": {
      +        "description": "REQUIRED for link — edge type, read src → dst. relates-to: a pointer, no claim. blocks: src (child) must land before dst (parent) can finish; the subtask edge. fulfills: src task delivers dst. caused-by: dst is src's root cause. contradicts: both stay active for a human to resolve. supersedes: retires dst as wrong to act on, not merely older. expires-on: src knowledge retires when dst task closes.",
      +        "enum": [
      +          "relates-to",
      +          "blocks",
      +          "fulfills",
      +          "contradicts",
      +          "supersedes",
      +          "caused-by",
      +          "expires-on"
      +        ],
      +        "type": "string"
      +      },
      +      "src_id": {
      +        "description": "REQUIRED for link — source node shareable id.",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "action",
      +      "project_slug",
      +      "src_id",
      +      "dst_id",
      +      "relation"
      +    ],
      +    "type": "object"
      +  }
      +]
  5. Changed1 schema field changed
    • changedInput schema / anyOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "description": "node action \"update\".",
      -    "properties": {
      -      "action": {
      -        "const": "update",
      -        "description": "Run the update action.",
      -        "type": "string"
      -      },
      -      "anchor_files": {
      -        "anyOf": [
      -          {
      -            "anyOf": [
      -              {
      -                "not": {}
      -              },
      -              {
      -                "items": {
      -                  "type": "string"
      -                },
      -                "type": "array"
      -              }
      -            ],
      -            "description": "Repo-relative file paths this note is about (e.g. 'src/auth/session.ts'). When someone later edits a file named here, this node surfaces to them. Set it on a lesson/risk/gap scoped to specific code so it resurfaces exactly when that code is touched."
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge or task nodes — repo-relative file paths this node is about; surfaces it when one is later edited (for a task: don't step on toes). Pass [] or null to clear."
      -      },
      -      "anchor_symbols": {
      -        "anyOf": [
      -          {
      -            "anyOf": [
      -              {
      -                "not": {}
      -              },
      -              {
      -                "items": {
      -                  "type": "string"
      -                },
      -                "type": "array"
      -              }
      -            ],
      -            "description": "Symbol names this note is about — function / class / constant / env var (e.g. 'refreshToken', 'SESSION_TTL'). Same purpose as anchor_files: a precise hook so the note resurfaces when that symbol's code is touched."
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge or task nodes — symbol names this node is about (function / class / constant / env var). Pass [] or null to clear."
      -      },
      -      "assignee_user_id": {
      -        "anyOf": [
      -          {
      -            "format": "uuid",
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) task nodes only — user uuid (not a shareable id)"
      -      },
      -      "body": {
      -        "description": "(update only) New markdown body (full replace) — mutually exclusive with edits. Write it in plain style: one idea per sentence, active voice, lead with the condition, one term per concept, exact technical names.",
      -        "type": [
      -          "string",
      -          "null"
      -        ]
      -      },
      -      "category": {
      -        "anyOf": [
      -          {
      -            "enum": [
      -              "decision",
      -              "architecture",
      -              "goal",
      -              "risk",
      -              "gap",
      -              "fyi",
      -              "opportunity",
      -              "priority",
      -              "lesson"
      -            ],
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge-type nodes only"
      -      },
      -      "due_at": {
      -        "anyOf": [
      -          {
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — ISO-8601 deadline. Pass null to clear. Any active knowledge with a past due_at surfaces in stats.overdue_commitments."
      -      },
      -      "edits": {
      -        "description": "(update only) Literal string-replacements applied to the body server-side, in order. Mutually exclusive with `body`. Pair with grep_nodes: use a matching line as old_string.",
      -        "items": {
      -          "additionalProperties": false,
      -          "properties": {
      -            "new_string": {
      -              "description": "Replacement text.",
      -              "type": "string"
      -            },
      -            "old_string": {
      -              "description": "Exact substring to find in the current body.",
      -              "minLength": 1,
      -              "type": "string"
      -            },
      -            "replace_all": {
      -              "description": "Replace every occurrence (default: require a unique match).",
      -              "type": "boolean"
      -            }
      -          },
      -          "required": [
      -            "old_string",
      -            "new_string"
      -          ],
      -          "type": "object"
      -        },
      -        "minItems": 1,
      -        "type": "array"
      -      },
      -      "expected_version": {
      -        "description": "(update only) Optimistic concurrency: reject the write if the node's current version differs (it changed since you read it).",
      -        "exclusiveMinimum": 0,
      -        "type": "integer"
      -      },
      -      "expires_at": {
      -        "anyOf": [
      -          {
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — ISO-8601 expiry (date TTL). Pass null to clear. When it passes, doctor(fix=true) auto-archives the node. For ephemeral `fyi`/temporary-note knowledge; decision/architecture retire by supersession instead."
      -      },
      -      "hidden": {
      -        "description": "(update only) Set true to HIDE this node from every retrieval surface (search, recall, lists) — the one explicit invisibility. It stays fetchable by its shareable id (shown with a HIDDEN banner), so hiding is never a delete. Use for a genuine mistake, expired ephemera, or a secret that was pasted in by accident. Set false to un-hide. On the hosted assistant this pauses for human approval.",
      -        "type": "boolean"
      -      },
      -      "node_id": {
      -        "description": "REQUIRED for update — shareable id of the node to edit (TASK-N/KNOW-N/COMP-N/DOC-N/NODE-N).",
      -        "type": "string"
      -      },
      -      "parent_component_id": {
      -        "anyOf": [
      -          {
      -            "description": "Shareable id: TASK-N (task), KNOW-N (knowledge), COMP-N (component), DOC-N (document), or NODE-N (any).",
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) shareable id of the new parent component. Accepted for component, knowledge, task, and document nodes. null = move to project root (documents and components only). Same-project enforcement is implicit — short ids resolve only within the active project."
      -      },
      -      "priority": {
      -        "anyOf": [
      -          {
      -            "enum": [
      -              "P0",
      -              "P1",
      -              "P2",
      -              "P3"
      -            ],
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) task nodes only — priority P0 (most urgent) … P3. Pass null to clear."
      -      },
      -      "project_slug": {
      -        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "source": {
      -        "anyOf": [
      -          {
      -            "maxLength": 500,
      -            "minLength": 1,
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — free-form provenance label. Pass null to clear."
      -      },
      -      "source_type": {
      -        "description": "(update only) knowledge nodes only — provenance / trust signal (human, claude, import).",
      -        "enum": [
      -          "human",
      -          "claude",
      -          "import"
      -        ],
      -        "type": "string"
      -      },
      -      "source_url": {
      -        "anyOf": [
      -          {
      -            "format": "uri",
      -            "type": "string"
      -          },
      -          {
      -            "type": "null"
      -          }
      -        ],
      -        "description": "(update only) knowledge nodes only — canonical URL for the source. Pass null to clear."
      -      },
      -      "status": {
      -        "description": "(update only) A task may be set to open or backlog here. Claim, completion, and cancellation must use claim_task, complete_task, or cancel_task so lifecycle records and agent identity remain atomic.",
      -        "enum": [
      -          "active",
      -          "archived",
      -          "backlog",
      -          "open",
      -          "in_progress",
      -          "done",
      -          "cancelled"
      -        ],
      -        "type": "string"
      -      },
      -      "title": {
      -        "description": "(update only) New title.",
      -        "maxLength": 300,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "topics": {
      -        "description": "(update only) knowledge or task nodes only",
      -        "items": {
      -          "type": "string"
      -        },
      -        "type": "array"
      -      }
      -    },
      -    "required": [
      -      "action",
      -      "project_slug",
      -      "node_id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "node action \"move\".",
      -    "properties": {
      -      "action": {
      -        "const": "move",
      -        "description": "Run the move action.",
      -        "type": "string"
      -      },
      -      "node_id": {
      -        "description": "REQUIRED for move — the node to move, a short-id (KNOW-N / TASK-N / DOC-N / COMP-N).",
      -        "type": "string"
      -      },
      -      "project_slug": {
      -        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "to": {
      -        "description": "REQUIRED for move — destination scope: 'trunk' for shared truth, or a workspace NAME (e.g. \"Billing\") or uuid to scope it there.",
      -        "minLength": 1,
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "action",
      -      "project_slug",
      -      "node_id",
      -      "to"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "node action \"link\".",
      -    "properties": {
      -      "action": {
      -        "const": "link",
      -        "description": "Run the link action.",
      -        "type": "string"
      -      },
      -      "dst_id": {
      -        "description": "REQUIRED for link — destination node shareable id.",
      -        "type": "string"
      -      },
      -      "evidence": {
      -        "description": "The VERBATIM line from the source that proves the claim — REQUIRED for `supersedes`. It must show the old state actually ENDED, not merely that something newer exists. \"We also added a read replica\" is additive and supersedes nothing; \"we've moved off DynamoDB entirely\" is a termination. If you cannot quote a termination, the correct relation is relates-to.",
      -        "maxLength": 4000,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "project_slug": {
      -        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "reason": {
      -        "description": "WHY this edge exists — REQUIRED for supersedes / contradicts / caused-by / blocks / fulfills. State the claim in your own words: for `supersedes`, name the standing state that ENDED (\"we moved the event store off DynamoDB to Postgres on Jul 13\"); for `caused-by`, name the causal mechanism. Not required for relates-to (the 'nothing typed fits' default) — if you cannot state a reason, that is the signal to use relates-to instead of forcing a typed edge.",
      -        "maxLength": 2000,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "relation": {
      -        "description": "REQUIRED for link — edge type, read src → dst. relates-to: a pointer, no claim. blocks: src (child) must land before dst (parent) can finish; the subtask edge. fulfills: src task delivers dst. caused-by: dst is src's root cause. contradicts: both stay active for a human to resolve. supersedes: retires dst as wrong to act on, not merely older. expires-on: src knowledge retires when dst task closes.",
      -        "enum": [
      -          "relates-to",
      -          "blocks",
      -          "fulfills",
      -          "contradicts",
      -          "supersedes",
      -          "caused-by",
      -          "expires-on"
      -        ],
      -        "type": "string"
      -      },
      -      "src_id": {
      -        "description": "REQUIRED for link — source node shareable id.",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "action",
      -      "project_slug",
      -      "src_id",
      -      "dst_id",
      -      "relation"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "description": "action=\"update\": Update editable fields on any node; each field is validated against what's valid for the node's type, so type-mismatched fields (e.g. category on a task) are rejected. A task's status may be set to open or backlog here, but claim, completion, and cancellation are REJECTED — use task(action=\"claim\") / task(action=\"complete\") so the claim and completion (with session + agent identity) are recorded atomically, or destructive(action=\"cancel_task\") so the destructive transition records a reason. Archiving a component is also rejected; use destructive(action=\"archive_component\") so active children cannot be orphaned. `parent_component_id` reparents across components in the same project. Surgical edits: pass `edits` (literal old_string→new_string replacements applied server-side) instead of `body` to fix a substring without resending the whole body — they're mutually exclusive, and a non-matching old_string errors rather than no-ops. Optional `expected_version` gives optimistic concurrency.",
      +    "properties": {
      +      "action": {
      +        "const": "update",
      +        "type": "string"
      +      },
      +      "anchor_files": {
      +        "anyOf": [
      +          {
      +            "anyOf": [
      +              {
      +                "not": {}
      +              },
      +              {
      +                "items": {
      +                  "type": "string"
      +                },
      +                "type": "array"
      +              }
      +            ],
      +            "description": "Repo-relative file paths this note is about (e.g. 'src/auth/session.ts'). When someone later edits a file named here, this node surfaces to them. Set it on a lesson/risk/gap scoped to specific code so it resurfaces exactly when that code is touched."
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge or task nodes — repo-relative file paths this node is about; surfaces it when one is later edited (for a task: don't step on toes). Pass [] or null to clear."
      +      },
      +      "anchor_symbols": {
      +        "anyOf": [
      +          {
      +            "anyOf": [
      +              {
      +                "not": {}
      +              },
      +              {
      +                "items": {
      +                  "type": "string"
      +                },
      +                "type": "array"
      +              }
      +            ],
      +            "description": "Symbol names this note is about — function / class / constant / env var (e.g. 'refreshToken', 'SESSION_TTL'). Same purpose as anchor_files: a precise hook so the note resurfaces when that symbol's code is touched."
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge or task nodes — symbol names this node is about (function / class / constant / env var). Pass [] or null to clear."
      +      },
      +      "assignee_user_id": {
      +        "anyOf": [
      +          {
      +            "format": "uuid",
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) task nodes only — user uuid (not a shareable id)"
      +      },
      +      "body": {
      +        "description": "(update only) New markdown body (full replace) — mutually exclusive with edits. Write it in plain style: one idea per sentence, active voice, lead with the condition, one term per concept, exact technical names.",
      +        "type": [
      +          "string",
      +          "null"
      +        ]
      +      },
      +      "category": {
      +        "anyOf": [
      +          {
      +            "enum": [
      +              "decision",
      +              "architecture",
      +              "goal",
      +              "risk",
      +              "gap",
      +              "fyi",
      +              "opportunity",
      +              "priority",
      +              "lesson"
      +            ],
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge-type nodes only"
      +      },
      +      "due_at": {
      +        "anyOf": [
      +          {
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — ISO-8601 deadline. Pass null to clear. Any active knowledge with a past due_at surfaces in stats.overdue_commitments."
      +      },
      +      "edits": {
      +        "description": "(update only) Literal string-replacements applied to the body server-side, in order. Mutually exclusive with `body`. Pair with an exact search: use a matching line as old_string.",
      +        "items": {
      +          "additionalProperties": false,
      +          "properties": {
      +            "new_string": {
      +              "description": "Replacement text.",
      +              "type": "string"
      +            },
      +            "old_string": {
      +              "description": "Exact substring to find in the current body.",
      +              "minLength": 1,
      +              "type": "string"
      +            },
      +            "replace_all": {
      +              "description": "Replace every occurrence (default: require a unique match).",
      +              "type": "boolean"
      +            }
      +          },
      +          "required": [
      +            "old_string",
      +            "new_string"
      +          ],
      +          "type": "object"
      +        },
      +        "minItems": 1,
      +        "type": "array"
      +      },
      +      "expected_version": {
      +        "description": "(update only) Optimistic concurrency: reject the write if the node's current version differs (it changed since you read it).",
      +        "exclusiveMinimum": 0,
      +        "type": "integer"
      +      },
      +      "expires_at": {
      +        "anyOf": [
      +          {
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — ISO-8601 expiry (date TTL). Pass null to clear. When it passes, the next health-check run with fix archives the node. For ephemeral `fyi`/temporary-note knowledge; decision/architecture retire by supersession instead."
      +      },
      +      "hidden": {
      +        "description": "(update only) Set true to HIDE this node from every retrieval surface (search, recall, lists) — the one explicit invisibility. It stays fetchable by its shareable id (shown with a HIDDEN banner), so hiding is never a delete. Use for a genuine mistake, expired ephemera, or a secret that was pasted in by accident. Set false to un-hide. On the hosted assistant this pauses for human approval.",
      +        "type": "boolean"
      +      },
      +      "node_id": {
      +        "description": "REQUIRED for update — shareable id of the node to edit (TASK-N/KNOW-N/COMP-N/DOC-N/NODE-N).",
      +        "type": "string"
      +      },
      +      "parent_component_id": {
      +        "anyOf": [
      +          {
      +            "description": "Shareable id: TASK-N (task), KNOW-N (knowledge), COMP-N (component), DOC-N (document), or NODE-N (any).",
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) shareable id of the new parent component. Accepted for component, knowledge, task, and document nodes. null = move to project root (documents and components only). Same-project enforcement is implicit — short ids resolve only within the active project."
      +      },
      +      "priority": {
      +        "anyOf": [
      +          {
      +            "enum": [
      +              "P0",
      +              "P1",
      +              "P2",
      +              "P3"
      +            ],
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) task nodes only — priority P0 (most urgent) … P3. Pass null to clear."
      +      },
      +      "project_slug": {
      +        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "source": {
      +        "anyOf": [
      +          {
      +            "maxLength": 500,
      +            "minLength": 1,
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — free-form provenance label. Pass null to clear."
      +      },
      +      "source_type": {
      +        "description": "(update only) knowledge nodes only — provenance / trust signal (human, claude, import).",
      +        "enum": [
      +          "human",
      +          "claude",
      +          "import"
      +        ],
      +        "type": "string"
      +      },
      +      "source_url": {
      +        "anyOf": [
      +          {
      +            "format": "uri",
      +            "type": "string"
      +          },
      +          {
      +            "type": "null"
      +          }
      +        ],
      +        "description": "(update only) knowledge nodes only — canonical URL for the source. Pass null to clear."
      +      },
      +      "status": {
      +        "description": "(update only) A task may be set to open or backlog here. Claim, completion, and cancellation must use the task lifecycle actions (claim, complete, cancel) so lifecycle records and agent identity remain atomic.",
      +        "enum": [
      +          "active",
      +          "archived",
      +          "backlog",
      +          "open",
      +          "in_progress",
      +          "done",
      +          "cancelled"
      +        ],
      +        "type": "string"
      +      },
      +      "title": {
      +        "description": "(update only) New title.",
      +        "maxLength": 300,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "topics": {
      +        "description": "(update only) knowledge or task nodes only",
      +        "items": {
      +          "type": "string"
      +        },
      +        "type": "array"
      +      }
      +    },
      +    "required": [
      +      "action",
      +      "project_slug",
      +      "node_id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "action=\"move\": Move an EXISTING node between scopes — into a workspace (hiding it from trunk readers) or back to trunk (shared truth). Re-scopes by id; reversible, never deletes. Distinct from destructive(action=\"cherry_pick_workspace\") (which promotes to trunk WITH provenance) — This is a plain re-assignment in any direction.",
      +    "properties": {
      +      "action": {
      +        "const": "move",
      +        "type": "string"
      +      },
      +      "node_id": {
      +        "description": "REQUIRED for move — the node to move, a short-id (KNOW-N / TASK-N / DOC-N / COMP-N).",
      +        "type": "string"
      +      },
      +      "project_slug": {
      +        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "to": {
      +        "description": "REQUIRED for move — destination scope: 'trunk' for shared truth, or a workspace NAME (e.g. \"Billing\") or uuid to scope it there.",
      +        "minLength": 1,
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "action",
      +      "project_slug",
      +      "node_id",
      +      "to"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "action=\"link\": Create a typed semantic link between two nodes (src → dst). Idempotent: re-linking the same (src, dst, relation) returns the existing edge, no error.\n\nSome relations change the graph: `supersedes` retires dst (it stays findable, but comes back marked SUPERSEDED and ranked below live knowledge), and `expires-on` retires src later. Every relation except `relates-to` is a claim and needs a `reason`; `supersedes` also needs `evidence`. If you cannot state the reason, use `relates-to`.\n\n## What each link means — and what it does\n\nAn edge is not decoration. Some of these change the graph.\n\n| Relation | Direction | Use it when | What it actually does |\n|---|---|---|---|\n| `relates-to` | A → B | Someone who pulls up B should know A exists. The weakest signal — use it when nothing typed fits. | Nothing but a pointer. Both nodes stay active. |\n| `blocks` | child → parent | A must land before B can be completed or become true. Also the **subtask** relation. | Surfaces A as a subtask of B and drives B's progress. |\n| `fulfills` | task → knowledge | This task delivers or acts on that decision, goal, or risk. | Records that the work discharged the commitment. |\n| `caused-by` | A → B | B is the root cause of A. | Builds the causal chain a provenance walk follows. |\n| `contradicts` | A → B | Two facts conflict and you cannot tell which is true. | Flags it for a human. **Both stay active** — this is the honest \"I don't know\" edge. |\n| `supersedes` | new → old | The old node has become **wrong to act on** — an agent following it would now do the wrong thing. **Never** for something that is merely older: history that is still true must stay. | **Retires the old node.** It stays in the graph and stays findable, but it comes back marked `SUPERSEDED` and ranked below live knowledge — so nothing acts on it as current. |\n| `expires-on` | knowledge → task | This knowledge is only true until that task closes. | Retires the knowledge when the task completes — same marked-not-deleted treatment. |\n\n`mentions` is not linkable by hand — write the id into a body and it is created for you.",
      +    "properties": {
      +      "action": {
      +        "const": "link",
      +        "type": "string"
      +      },
      +      "dst_id": {
      +        "description": "REQUIRED for link — destination node shareable id.",
      +        "type": "string"
      +      },
      +      "evidence": {
      +        "description": "The VERBATIM line from the source that proves the claim — REQUIRED for `supersedes`. It must show the old state actually ENDED, not merely that something newer exists. \"We also added a read replica\" is additive and supersedes nothing; \"we've moved off DynamoDB entirely\" is a termination. If you cannot quote a termination, the correct relation is relates-to.",
      +        "maxLength": 4000,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "project_slug": {
      +        "description": "Project slug, e.g. \"acme-app\" — list(what=\"projects\") returns yours.",
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "reason": {
      +        "description": "WHY this edge exists — REQUIRED for supersedes / contradicts / caused-by / blocks / fulfills. State the claim in your own words: for `supersedes`, name the standing state that ENDED (\"we moved the event store off DynamoDB to Postgres on Jul 13\"); for `caused-by`, name the causal mechanism. Not required for relates-to (the 'nothing typed fits' default) — if you cannot state a reason, that is the signal to use relates-to instead of forcing a typed edge.",
      +        "maxLength": 2000,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "relation": {
      +        "description": "REQUIRED for link — edge type, read src → dst. relates-to: a pointer, no claim. blocks: src (child) must land before dst (parent) can finish; the subtask edge. fulfills: src task delivers dst. caused-by: dst is src's root cause. contradicts: both stay active for a human to resolve. supersedes: retires dst as wrong to act on, not merely older. expires-on: src knowledge retires when dst task closes.",
      +        "enum": [
      +          "relates-to",
      +          "blocks",
      +          "fulfills",
      +          "contradicts",
      +          "supersedes",
      +          "caused-by",
      +          "expires-on"
      +        ],
      +        "type": "string"
      +      },
      +      "src_id": {
      +        "description": "REQUIRED for link — source node shareable id.",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "action",
      +      "project_slug",
      +      "src_id",
      +      "dst_id",
      +      "relation"
      +    ],
      +    "type": "object"
      +  }
      +]
  6. First observed

TDQS

A4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare destructiveHint=true, openWorldHint=true and readOnlyHint=false, and the description adds the important structural fact that link creates a typed edge while unlink is deliberately elsewhere. However, it discloses none of the mutation consequences an agent would want here — e.g. that body is a full replace, or that hidden/move change retrieval surfaces — all of which live only in the schema. There is mild tension between 'unlink lives in destructive' and the destructiveHint=true on this tool, but update/move can still overwrite state, so this is a nuance rather than a contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three compact, front-loaded clauses: purpose first, then each action, then the explicit exclusion pointing at `destructive`. No sentence is redundant and nothing important is buried.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given an output schema, rich annotations, and a fully documented input schema, the description needs to carry only the action taxonomy and routing, which it does. Its one real gap is that the destructive/mutation consequences of update and move are left entirely to schema and annotation fields.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, and the schema documents every field including mutual exclusion of body vs edits, expected_version concurrency, and relation/reason semantics. The description only names the `action` values and adds no parameter-level detail beyond the schema, which is the correct baseline when the schema already does the work.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource ('Edit a node or its structure') and then enumerates the three supported actions with their distinct meanings (update fields/body, move/re-file, link a typed edge). It explicitly names the sibling (`destructive`) that owns the excluded operation, so an agent can separate this tool from its neighbors without reading the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Each action is scoped ('move (re-file under a component or workspace)', 'link (add a typed edge between two nodes)') and the tool routes unlink to `destructive`, which is genuine when-to-use guidance. It stops short of steering between this tool and other editing siblings (task, knowledge, document), so a few routing decisions are left implicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.