MCP tools
MCP tools
Section titled “MCP tools”Generated from the live tool_router().list_all() catalogue on McpServer. Every tool the running server exposes appears below.
Count: the server exposes 21 tools.
memstead_changes_sincememstead_checkmemstead_creatememstead_deletememstead_diffmemstead_entitymemstead_healthmemstead_mem_configurememstead_mem_creatememstead_mem_deletememstead_mem_set_schemamemstead_mem_set_versionmemstead_overviewmemstead_proposal_briefmemstead_relatememstead_reloadmemstead_renamememstead_retypememstead_schemamemstead_searchmemstead_update
memstead_changes_since
Section titled “memstead_changes_since”Per-mem change feed. since is backend-specific and NEVER a mutation’s write_id (a folder mem refuses that token with INVALID_CURSOR). Git-branch mem: since is a commit SHA — the head a prior call returned, or the empty-tree hash 4b825dc642cb6eb9a060e54bf8d69288fbee4904 for a first sync. Folder mem: since is an RFC3339 timestamp — the head a prior call returned (the timestamp of the last ledger entry), or empty for a first sync; that head passed back returns zero events until the mem moves. Returns a flat list of entity-level events — each event’s action is one of added, updated, removed, renamed. Non-removed events carry title and entity_type (from the post-diff store); a removed event carries neither key. Engine-authored renames pair via commit-note provenance (memstead: rename <old> → <new>), transitively composed across rename chains in one window. Non-engine renames (git mv) fall back to a content-similarity scorer (default 0.6, tunable via rename_similarity in [0.1, 1.0]), capped at 1000 rewrite pairs per diff. Either path surfaces as one renamed event with from_id and to_id, never a removed+added pair; a folder mem pairs its ledger’s rename row with the id that vanished, and an entity added and renamed inside one window arrives once, as added under its final id. Out-of-range rename_similarity refuses INVALID_INPUT naming details.allowed_range and details.requested. head is the next polling cursor on both backends: the HEAD SHA of a git-branch mem (prefer full SHAs over refs), the last ledger timestamp of a folder mem. No pagination — every qualifying commit ships in one response. Pass include_notes: true to fold per-commit agent-notes (notes[]) and memstead_ref into the response. Unknown or malformed since returns INVALID_CURSOR with details.mem and details.since.
Hints: read_only = true, destructive = false, idempotent = true, open_world = false
Input schema:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "include_notes": { "default": false, "description": "Fold per-commit agent-notes into the response. When true, the report carries a `notes[]` array (one entry per commit between `since` and `head`, with `sha`, `subject`, `tool_verb`, `entity_id`, `note`, `actor`, `tool`, `client`, `timestamp`) plus `memstead_ref` — the SHA of the unified schema + per-mem-config registry, absent when the workspace has not been migrated yet. Default false (entity-delta only). Commit-mirroring clients turn this on to receive notes and the registry-ref sha in one round-trip; agents that just need entity events leave it off.", "type": "boolean" }, "mem": { "description": "Writable mem name. Call memstead_health for the list.", "type": "string" }, "rename_similarity": { "description": "Rename detection threshold for content-similarity, in [0.1, 1.0]. Default (None) → 0.6. Lower values widen the recall window at the cost of false-positive rename pairing; raise it to 0.9+ when you want only near-byte-identical renames collapsed. Out-of-range values refuse with `INVALID_INPUT` naming `details.allowed_range` and `details.requested` — agents recover by reissuing with a value inside `[0.1, 1.0]`.", "format": "float", "type": [ "number", "null" ] }, "since": { "description": "Change cursor to diff against. Backend-specific, and never a mutation's `write_id`: on a git-branch mem a commit SHA (pass the `head` a prior call returned, or the canonical git empty-tree hash `4b825dc642cb6eb9a060e54bf8d69288fbee4904` to get every entity as `added` on a fresh-client first sync); on a folder mem an RFC3339 timestamp (the `ts` of the last entry you received, or empty for a first sync).", "type": "string" } }, "required": [ "mem", "since" ], "type": "object"}memstead_check
Section titled “memstead_check”Record a check: “entity E checked, verdict ok | failed, via method M” — the engine-recorded act of verification (never a mutation: markdown, _hash and commits are untouched, so staleness stays computable). The record carries the caller-declared role, actor/client identity and the entity’s _hash at check time, appended to the append-only check ledger — a newer check of the same kind supersedes older ones for state, never erases them. Derived check state (never_checked | checked_ok | check_failed | check_stale — stale means the entity changed after its last check, by hash comparison, never stamped) is served in memstead_entity’s mutation_provenance block and echoed here as check_state. Verdict vocabulary is closed (ok | failed) — nuance goes in method; an unknown verdict refuses INVALID_VERDICT. The check kind vocabulary is closed too (verification | conformance; omitted = verification, exactly the prior behaviour): a conformance record is the semantic judgment “this entity satisfies its type’s schema prose”, carries the mem’s schema pin as schema_ref stamped by the engine (never caller-supplied), and derives stale when the content hash OR the pin moves; state derives per (entity, kind), the kinds never supersede each other, a caller-declared x-<name> kind is recorded verbatim, never interpreted (no pin, no state, counted in health), and any other kind refuses INVALID_CHECK_KIND naming the vocabulary. An optional structured finding (code, message, section?, evidence?) rides the record and is served under the entity’s latest verdict in health; a malformed one refuses INVALID_CHECK_FINDING naming the shape. Optional observed_at (ISO-8601 UTC): when the check was MADE, stored beside the write time ts, informational only. Refuses typed on unknown entity (ENTITY_NOT_FOUND), unknown/quarantined mems, read-only mems (READ_ONLY_MOUNT) and persistence failure (CHECK_NOT_RECORDED: recording is never best-effort).
Hints: read_only = false, destructive = false, idempotent = false, open_world = false
Input schema:
{ "$defs": { "CheckFindingParam": { "description": "One `finding` on `memstead_check` — the wire shape of\n`memstead_base::check::CheckFinding`, validated as a whole by the\nengine type (unknown keys refuse there).", "properties": { "code": { "description": "The finding code. `UPPER_SNAKE` is the engine's namespace and must name a health condition (`DANGLING_LINK_TARGET_MISSING`, `MISSING_REQUIRED_OUTGOING`, …); any other spelling (`hidden-premise`, `stale-source`) is the checker's own vocabulary. Required, non-empty.", "type": "string" }, "evidence": { "description": "What the finding rests on: a quote, a coordinate, a reference.", "type": [ "string", "null" ] }, "message": { "description": "One or two sentences a reader can act on. Required, non-empty.", "type": "string" }, "section": { "description": "The section key the finding concerns, when it concerns one.", "type": [ "string", "null" ] } }, "required": [ "code", "message" ], "type": "object" } }, "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { "entity": { "description": "Full entity id (`mem--slug`) of the entity that was checked", "type": "string" }, "finding": { "anyOf": [ { "$ref": "#/$defs/CheckFindingParam" }, { "type": "null" } ], "description": "Optional structured finding: `{code, message, section?, evidence?}` — `code` in `UPPER_SNAKE` is the engine's namespace and must name a health condition (a `failed` verdict with a `method` naming the owner and the closing plan acknowledges that finding for `memstead health --strict`; a later `ok` withdraws it), any other spelling (`hidden-premise`, `stale-source`) is your own vocabulary; `message` one or two sentences a reader can act on, `section` the section key it concerns, `evidence` what it rests on. Persisted on the ledger line, echoed in this response, rendered by `memstead_health` `include: [\"checks\"]` under the entity's latest verdict. The wrapper shape is fixed: a missing `code` or `message`, an empty value, or an unknown key refuses `INVALID_CHECK_FINDING` naming the shape, and nothing is recorded." }, "identity": { "description": "WHO is checking: an opaque identity string, recorded immutably on the check record. The independence gate compares the author's recorded identity against this one and nothing else — declare a stable identity per agent/session and author≠checker becomes machine-checkable. Omit to record the session default, or nothing (the check then reads unconfirmable). Over-length refuses INVALID_IDENTITY (cap 128 chars).", "type": [ "string", "null" ] }, "kind": { "description": "The check kind: `verification` | `conformance` (the engine's two kinds), or a caller-declared `x-<name>` kind (lowercase letters, digits, hyphens) the engine records verbatim and never interprets — it stamps no pin, moves no `check_state`, and health lists it by count. Omit for `verification` — exactly today's behaviour. `conformance` records a semantic judgment (\"does this entity satisfy its type's schema prose\"): the engine stamps the mem's schema pin into the record (never caller-supplied), and the verdict derives stale when the content hash moves OR the pin changes; a mem with no pin refuses `INVALID_INPUT`. State derives per (entity, engine kind) — the kinds never supersede each other. Any other value refuses `INVALID_CHECK_KIND` naming the vocabulary.", "type": [ "string", "null" ] }, "method": { "description": "Optional free-text method note — how the check was performed (e.g. \"diffed against source spec\").", "type": [ "string", "null" ] }, "observed_at": { "description": "When the check was MADE, when that is not now: an ISO-8601 UTC instant (`YYYY-MM-DDTHH:MM:SSZ`) or a civil date (`YYYY-MM-DD`, midnight UTC). For a recorder transcribing checks made earlier or out of order: stored as `observed_at` beside the engine's own write time `ts`, echoed here and in health; a reader ordering checks by time takes `observed_at` where present and `ts` otherwise. Informational: which record is the newest of its kind is still decided by ledger order. An unreadable shape or an instant more than five minutes ahead of the engine's clock refuses `INVALID_INPUT`.", "type": [ "string", "null" ] }, "role": { "description": "The role this check is performed in, from the closed vocabulary `author` | `checker` | `verifier`. Recorded immutably on the check record — same trust model as mutation roles: caller-declared but tamper-evident. Omit to record the session default (or unspecified — legal, but an unspecified-role check cannot confirm independence downstream).", "type": [ "string", "null" ] }, "verdict": { "description": "The verdict, from the closed vocabulary `ok` | `failed`. Nuance goes in `method` or in process-mem entities — an unknown value refuses `INVALID_VERDICT` naming the vocabulary.", "type": "string" } }, "required": [ "entity", "verdict" ], "type": "object"}memstead_create
Section titled “memstead_create”Create a new entity. Read the target mem’s schema first (memstead_schema). Required: title, entity_type, plus the type’s required sections. Id: mem—slug(title). Titles accept any single-line text (control characters such as tab/newline are rejected); the title is stored verbatim as display text, while characters outside Unicode alphanumerics, whitespace, and hyphen are dropped from the derived slug — warning TITLE_CHARS_DROPPED_FROM_SLUG names them (INVALID_TITLE refuses control chars, empty-deriving or over-long titles). mem defaults to the primary writable mem. Pass relations to wire edges inline — {target, rel_type, description?}, no source field; unresolved targets auto-stub. Optional note (see instructions). Schema-bound failures (UNKNOWN_SECTION, UNKNOWN_METADATA_FIELD, INVALID_ENUM_VALUE, INVALID_FIELD_VALUE, SECTION_CONTENT_INVALID, REQUIRED_FIELD_UNSET, INVALID_REL_TYPE, …) carry recovery payloads (see instructions). Create-specific: REQUIRED_FIELD_UNSET also fires on an omitted required-no-default metadata field (retires warning MISSING_REQUIRED_FIELD); MISSING_REQUIRED_SECTION refuses on create — an entity never lands with a placeholder body — shipping per-section write_rules, type_guidance, and details.pre_announced naming still-unset required metadata so one retry clears both gates. Warnings (entity still lands): UNDECLARED_RELATIONSHIP_OPEN; INLINE_WIKI_LINK_AUTO_STUBBED (body [[wiki-links]] auto-stub unresolved targets; details.stubs); CROSS_SCHEMA_LINK_UNDECLARED (no cross_mem_relationships entry for the target schema: NO edge, prose only); MISSING_REQUIRED_OUTGOING (details.missing[]={relationships, cardinality}; then memstead_relate). Real writes return write_id (identity, not cursor). dry_run: true previews a VALID entity (prospective id, file_path, _hash, warnings, type_guidance, incoming edges adopted from a pre-existing stub; empty write_id) — an INVALID one refuses with the real call’s typed envelope.
Hints: read_only = false, destructive = false, idempotent = false, open_world = false
Input schema:
{ "$defs": { "AnchorInputParam": { "additionalProperties": false, "description": "One `anchors[]` element on `memstead_create` / `memstead_update` — a\nprovenance record tying the entity to a source artifact. Permissive by\ndesign: every field is optional / string-typed so a malformed element\n(unknown class or grain, missing artifact, hash on a non-hash class,\ngrain the medium's namespace cannot express) refuses the whole mutation\nwith a typed `INVALID_ANCHOR` envelope carrying recovery `details` —\nrather than an opaque schema-deserialisation error. Converts to the\nengine's `AnchorInput` which validates it. Not folded into `_hash`.", "properties": { "artifact": { "default": null, "description": "Artifact reference in the medium's own namespace — a repo-relative path, `path@commit`, URL, or entity id, interpreted per `grain`. Required; a missing/empty value refuses INVALID_ANCHOR.", "type": [ "string", "null" ] }, "at_version": { "anyOf": [ { "$ref": "#/$defs/AnchorVersionParam" }, { "type": "null" } ], "description": "Medium-typed pinned version this anchor was recorded against: `{ kind: \"commit\"|\"snapshot\"|\"etag\", value: \"<token>\" }`. Omit for a plain-path medium with no retrievable version." }, "binding": { "default": null, "description": "`hash(D)` of the binding that produced this anchor, when a binding produced it. Omit for a manually-authored anchor.", "type": [ "string", "null" ] }, "class": { "default": null, "description": "Provenance class — the entity's epistemic standing toward the artifact: `anchored` | `derived` | `authored` | `informed-by`. `anchored`/`derived` carry hash semantics (a `hash` is permitted and participates in drift adjudication); `authored`/`informed-by` do not (supplying `hash` refuses INVALID_ANCHOR). An unknown value refuses INVALID_ANCHOR.", "type": [ "string", "null" ] }, "content": { "default": null, "description": "The observed artifact CONTENT (UTF-8 text), for the engine to compute `hash` from through its preparation registry — the write-time observation for a grain the engine cannot observe itself: a `url` anchor (the engine never fetches; what you read is canonicalized exactly as a path grain's bytes are). Also accepted for `span`/`file`. Mutually exclusive with `hash` (both refuses INVALID_ANCHOR); refused on `authored`/`informed-by`, and on the `entity`/`tree` grains, whose prepared form is never computed from supplied bytes.", "type": [ "string", "null" ] }, "derived_from": { "default": null, "description": "For a `derived` class: the input artifact refs the entity was derived from. Empty/omitted for every other class.", "items": { "type": "string" }, "type": [ "array", "null" ] }, "grain": { "default": null, "description": "Granularity of the artifact reference: `span` | `file` | `tree` | `url` | `entity`. `span`/`file`/`tree` need a path-shaped medium namespace and `entity` the entity namespace, or the mutation refuses INVALID_ANCHOR; `url` is admitted beside every medium (a URL never enters a path namespace). A path-shaped grain whose artifact is a URL refuses INVALID_ANCHOR naming that rule: use `grain: url`. An unknown value refuses INVALID_ANCHOR.", "type": [ "string", "null" ] }, "hash": { "default": null, "description": "Content hash over the PREPARED artifact form (never raw bytes). Permitted only on hash-bearing classes (`anchored`/`derived`); supplying it on `authored`/`informed-by` refuses INVALID_ANCHOR.", "type": [ "string", "null" ] }, "hash_stability": { "default": null, "description": "Medium's declared hash stability: `stable` | `unstable` (defaults per grain: `url` to `unstable`, every other grain to `stable`). An unstable-source hash break resolves `recheck`, not `drifted`.", "type": [ "string", "null" ] }, "source": { "default": null, "description": "NAME of the source (as declared in the producing binding's `sources[]`) that produced this anchor — lets a discovery run be measured per entry point. Name the source you are working from whenever the binding declares more than one. Present-but-empty refuses INVALID_ANCHOR; a name the (resolvable) producing binding does not declare refuses with the declared names in `details.declared`. Omit for a manually-authored anchor.", "type": [ "string", "null" ] }, "span": { "default": null, "description": "The verbatim words the entity quotes from the artifact (`url`/`span`/`file` grains, hash-bearing classes). With `content`, the write refuses INVALID_ANCHOR unless the span occurs in it; the row then records the span, a hash over its canonical form and the document's prepared hash. Without `content` the row is accepted with the span unverified. From then on the row is adjudicated on the span's PRESENCE in observed text (`verify-anchors --observations` with `content` rows): `resolves` while the words stand whatever else on the page changed, `span_absent` once they are gone, never `drifted`. Compared in the canonical span form: NFC, whitespace runs to one space, soft hyphens and line-end hyphens removed, typographic quotes, dashes and ligatures to ASCII; digits, decimal marks, units, case and word order exact. The span is part of the row's identity beside `artifact`, `grain` and `class`: several spans on one document are several rows. Refused beside `hash` (the engine computes the hashes), on `entity`/`tree`, and when empty.", "type": [ "string", "null" ] } }, "type": "object" }, "AnchorVersionParam": { "additionalProperties": false, "description": "The medium-typed pinned version sub-object of an [`AnchorInputParam`].", "properties": { "kind": { "description": "Version kind: `commit` (git / path+commit) | `snapshot` (graph) | `etag` (web).", "type": "string" }, "value": { "description": "The version token — commit id, graph snapshot token, or web ETag.", "type": "string" } }, "required": [ "kind", "value" ], "type": "object" }, "RelationInput": { "additionalProperties": false, "description": "A relationship input for create/batch tools.", "properties": { "description": { "default": null, "description": "Optional per-edge description text. Validated against the rel-type's `per_edge_description` posture in the pinned schema: `forbidden` (default) rejects a non-empty description with `DESCRIPTION_NOT_PERMITTED`; `required` rejects its absence with `MISSING_REQUIRED_DESCRIPTION`; `optional` accepts both. Empty / whitespace-only strings normalise to absent before validation. Surfaces on `memstead_entity` and round-trips through the `## Relationships` markdown via the canonical em-dash delimiter (` — `).", "type": [ "string", "null" ] }, "rel_type": { "description": "Relationship type. Canonical form is UPPER_SNAKE_CASE (USES, PART_OF, DEPENDS_ON) and is what the engine stores; case-insensitive inputs (`uses`, `Part_Of`) are accepted and echoed back in the response as their canonical form. The JSON Schema `pattern` advertises `^[A-Za-z][A-Za-z_]*$` for client-side validators; the engine enforces the same character set independently — characters outside it return `INVALID_REL_TYPE` at the engine boundary regardless of whether the client pre-filters.", "pattern": "^[A-Za-z][A-Za-z_]*$", "type": "string" }, "target": { "description": "Full target entity ID", "type": "string" } }, "required": [ "target", "rel_type" ], "type": "object" } }, "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "anchors": { "description": "Optional provenance anchors to attach to the new entity — durable records tying it to the source artifacts it describes (which artifact, at which grain, under which provenance class). Written into the mem-branch anchors sidecar in the SAME commit as the entity (atomic); omitting it is byte-identical to a create without anchors. Anchor writes MERGE: later `memstead_update` calls carrying `anchors` add to this set (same `(artifact, grain, class, span)` identity replaces, otherwise appends; a new span on a stored artifact is a second row) and never silently discard it — removal is explicit via `memstead_update`'s `anchors_unset`. A malformed element refuses the whole create with `INVALID_ANCHOR` (`details` carries the offending field + allowed set) and the entity is not written. A payload naming one `(artifact, grain, class, span)` identity TWICE refuses: that identity is one row, so the repeats would collapse to the last one unannounced. A `span` anchor's locator must be usable (`#L<start>-L<end>`, `#<unit-key>`, or no locator for the whole file); one that addresses nothing refuses, and a span the engine could not check (no `content` supplied) is accepted and recorded as unverified. Anchors do NOT participate in `_hash`.", "items": { "$ref": "#/$defs/AnchorInputParam" }, "type": [ "array", "null" ] }, "dry_run": { "description": "Validate and preview the create without executing — no disk write, no store mutation, no VCS commit, no edges added. dry_run runs the SAME validation a real call runs; it is not a softer check. On a VALID entity the response carries the prospective `id`, `file_path`, and `_hash` (bit-identical to what a real call with the same arguments would produce, EXCEPT for engine-auto-stamped timestamps: the hash covers `created_date`, which is stamped from wall-clock `now()` independently in the dry-run and the real call, so the two `_hash` values diverge whenever a second ticks between them; the hash also covers `sections`, `metadata`, and `relations`, so a dry_run that omits `relations` will not match a real call that supplies them; `_hash` does NOT cover `anchors` — the anchors sidecar persists on the mem branch under `.memstead/` and is never folded into content hashing, so attaching or refreshing anchors never changes `_hash` or invalidates a cached `expected_hash`), plus any `warnings` and any `incoming` edges that would be adopted from a pre-existing stub at this id, with `write_id` empty. On an INVALID entity dry_run does NOT return a warnings-list preview: it refuses with the IDENTICAL typed envelope a real call would return (`MISSING_REQUIRED_SECTION`, `UNKNOWN_SECTION`, `UNKNOWN_METADATA_FIELD`, `INVALID_ENUM_VALUE`, `INVALID_FIELD_VALUE`, `REQUIRED_FIELD_UNSET`, …), carrying the same recovery `details.*` (e.g. `details.sections[]`). That typed refusal IS the pre-flight signal — read its `details` to fix coverage, then retry. So dry_run never reports a problem entity as clean: it and a real write agree on validity. Use to verify the id slug, or to pre-flight required-section / field coverage and pre-existing references before committing.", "type": [ "boolean", "null" ] }, "entity_type": { "description": "Entity type. Required. Allowed values are pinned by the target mem's schema — fetch them via `memstead_schema(name=<mem.schema_ref>)` (cached per session). Unknown types refuse with `UNKNOWN_ENTITY_TYPE`.", "type": "string" }, "identity": { "description": "WHO is acting: an opaque identity string of your choosing — an agent name, a session handle. Recorded immutably alongside the mutation (commit trailer / ledger); the author≠checker independence gate compares identities and nothing else. Caller-declared and unverified, but tamper-evident in append-only history. Omit to record the session default, or nothing — legal forever, never refused; identity-less records read unconfirmable at the gate. Over-length values refuse INVALID_IDENTITY (cap 128 chars).", "type": [ "string", "null" ] }, "mem": { "description": "Mem name (directory name of the write mem)", "type": [ "string", "null" ] }, "metadata": { "additionalProperties": { "type": "string" }, "description": "Metadata overrides: { \"level\": \"M1\", \"tags\": \"a, b\" }", "type": [ "object", "null" ] }, "note": { "description": "Agent-authored provenance note (≤280 chars, one sentence describing why this mutation happened). Lands in the per-mem commit body between the mechanical subject line and the provenance trailers (`Tool:`, `Actor:`, `Client:`), where `memstead_changes_since` with `include_notes` reads it back. Omit for pure-housekeeping edits; when `[mutations].require_notes = true` in workspace config a missing note adds a `NOTE_MISSING` `WarningHint` to the response (the mutation still commits).", "type": [ "string", "null" ] }, "relations": { "description": "Initial relationships, wired in the same call that creates the entity. An entry is literally `{ \"target\": \"<mem>--<slug>\", \"rel_type\": \"REL_TYPE\", \"description\": \"…\" }` — `description` optional, and there is no `from`: the entity being created is the source, so the far end is `target`. (`memstead_relate` names both ends: `{from, to, rel_type, …}`.) An unresolved `target` auto-creates a stub.", "items": { "$ref": "#/$defs/RelationInput" }, "type": [ "array", "null" ] }, "role": { "description": "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier`. Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary.", "type": [ "string", "null" ] }, "sections": { "additionalProperties": { "type": "string" }, "description": "Section contents: { \"identity\": \"...\", \"purpose\": \"...\" }", "type": [ "object", "null" ] }, "title": { "description": "Entity title (ID is derived automatically as mem--slug(title))", "type": "string" } }, "required": [ "title", "entity_type" ], "type": "object"}memstead_delete
Section titled “memstead_delete”Remove an entity permanently. Deletes the entity’s store record, every edge touching it (both directions), and its markdown file on disk. Optimistically locked at the engine on every delete: over MCP the caller supplies expected_hash from a prior memstead_entity read (as memstead_update / memstead_rename do); memstead delete on the CLI reads the current hash itself right before deleting, so it has no hash flag; a stale token emits HASH_MISMATCH with details.current. Binary semantics: any incoming reference from another entity in a Write-Mem refuses the delete with HAS_INCOMING_REFS and details.referrers listing each {from_id, rel_types, mem} (one entry per unique source, rel_types collapses multi-edge cases) — the agent removes the offending references via memstead_relate --remove (or memstead_update for body wiki-links) before retrying. There is no force flag. When the only incoming references come from ReadOnly mounts (archives), the delete proceeds: the on-disk file is removed and the in-memory entity is demoted to a stub at the same id so the surviving edges keep a valid target — the response carries a RESIDUAL_STUB_FOR_READONLY_REFERRERS warning naming the surviving referrers. PART_OF children survive the delete with their parent edge removed; file paths are unaffected. Stubs (_hash empty) are deleted with expected_hash: "" — the hash check is skipped because there is nothing to compare. Optional note (≤280 chars) — shared provenance contract, see memstead_create. Response carries relations_removed (edges removed by this delete), orphan_stubs_removed (ids of stub entities whose last incoming edge was this entity, GC’d in the same op; serde-omitted when empty), warnings (residual-stub warning when the demote path applied), and write_id (an identity, never a change cursor). Provenance anchors, if any, are removed in the same commit (no orphaned anchor survives).
Hints: read_only = false, destructive = true, idempotent = false, open_world = false
Input schema:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "expected_hash": { "description": "Hash from memstead_entity response (_hash field). Required for real entities — read first. Mirrors memstead_update / memstead_rename. Mismatch returns code HASH_MISMATCH with details.current carrying the current on-disk hash. Stubs carry an empty `_hash` (they have no on-disk file); pass the empty string to delete a stub — the hash check is skipped because there is nothing to compare.", "type": "string" }, "id": { "description": "Full entity ID to delete", "type": "string" }, "identity": { "description": "WHO is acting: an opaque identity string of your choosing — an agent name, a session handle. Recorded immutably alongside the mutation (commit trailer / ledger); the author≠checker independence gate compares identities and nothing else. Caller-declared and unverified, but tamper-evident in append-only history. Omit to record the session default, or nothing — legal forever, never refused; identity-less records read unconfirmable at the gate. Over-length values refuse INVALID_IDENTITY (cap 128 chars).", "type": [ "string", "null" ] }, "note": { "description": "Agent-authored provenance note (≤280 chars, one sentence describing why this mutation happened). Lands in the per-mem commit body between the mechanical subject line and the provenance trailers (`Tool:`, `Actor:`, `Client:`), where `memstead_changes_since` with `include_notes` reads it back. Omit for pure-housekeeping edits; when `[mutations].require_notes = true` in workspace config a missing note adds a `NOTE_MISSING` `WarningHint` to the response (the mutation still commits).", "type": [ "string", "null" ] }, "role": { "description": "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier`. Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary.", "type": [ "string", "null" ] } }, "required": [ "id", "expected_hash" ], "type": "object"}memstead_diff
Section titled “memstead_diff”Return a two-ref structural diff at entity granularity. Walks the tree at ref_a and the tree at ref_b in the mem’s gitdir, surfacing per-entity changes as entries[] whose status is one of added, modified, deleted, renamed, invalid_entity. Each entry carries the full markdown body on both sides by default in content_before / content_after; pass include_content: false for the metadata-only shape (id, title, entity_type, status). Ref-handling conventions mirror memstead_changes_since: the canonical empty-tree sentinel 4b825dc642cb6eb9a060e54bf8d69288fbee4904 is accepted as either ref and short-circuits to git’s empty tree (first-sync diffs against a fresh mem use this for ref_a); a bare HEAD resolves to the selected mem’s branch tip rather than the gitdir’s symbolic HEAD. Cross-mem diffs work via fully-qualified refs naming the peer mem’s branch; cross-different-gitdir diffs are out of scope (the op operates on one mem-repo). Refusal codes: UNKNOWN_MEM (details.name), UNKNOWN_REF (details.ref), INVALID_INPUT for folder / archive mounts and for rename_similarity outside the allowed range. Rename detection uses content-similarity tuned by rename_similarity; Each entry’s ripple field carries per-side {from_id, side} entries for entities with inbound wiki-links to the affected entry — side: "ref_a" lists referrers at the ref_a snapshot, side: "ref_b" at ref_b. Pass include_ripple: false to omit the field entirely (e.g. for large mems where the per-side wiki-link scan is the dominant cost). Response top-level: ref_a, ref_b, resolved_a_sha, resolved_b_sha, config, entries.
Hints: read_only = true, destructive = false, idempotent = true, open_world = false
Input schema:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "include_content": { "default": true, "description": "When true (default), each entry carries the full markdown body on both sides. When false, only metadata (id, title, type, status) survives — smaller payload, useful for audit counts.", "type": "boolean" }, "include_ripple": { "default": true, "description": "When true (default), each entry's `ripple` carries per-side `{from_id, side}` entries for entities with inbound wiki-links to the affected entry — `side: \"ref_a\"` lists referrers at the `ref_a` snapshot, `side: \"ref_b\"` at `ref_b` — so a consumer sees what would break if the change were applied or skipped. Pass false to omit the field (e.g. for large mems where the per-side wiki-link scan dominates cost).", "type": "boolean" }, "mem": { "description": "Mem that selects the storage context (the gitdir, for git-branch mounts). `ref_a` / `ref_b` are arbitrary refs resolved inside that gitdir; cross-mem diffs work via fully-qualified refs (`refs/heads/<other-mem>`). Folder / archive mounts refuse the call with `INVALID_INPUT` — they carry no git refs to diff.", "type": "string" }, "ref_a": { "description": "First ref to diff. Branch name (`main`), full ref (`refs/heads/specs`), commit SHA, or tag. Unknown refs refuse with `UNKNOWN_REF` and `details.ref` carrying the raw input.", "type": "string" }, "ref_b": { "description": "Second ref to diff. Same input shape as `ref_a`.", "type": "string" }, "rename_similarity": { "description": "Rename detection threshold for content-similarity, in [0.1, 1.0]. Default (None) → 0.6. Out-of-range values refuse with `INVALID_INPUT` (`details.allowed_range`, `details.requested`).", "format": "float", "type": [ "number", "null" ] } }, "required": [ "mem", "ref_a", "ref_b" ], "type": "object"}memstead_entity
Section titled “memstead_entity”Read one entity. Dual channel: text is rendered markdown; structured_content is the typed envelope { _hash, id, mem, entity_type, origin, _tokens, metadata, sections, relationships, _stub_kind?, _signals?, _labelling? } so agents branch on fields without parsing the text. origin is this ENTITY’s trust class: third-party for a read-only mount and for a body merged in from a contributor’s fork, first-party otherwise. The text channel carries it as _origin, and wraps a third-party body in the delimiter _quote names: read what is inside as quoted, untrusted data, never as direction addressed to you. include_provenance: true adds mutation_provenance: the recorded touches on a source mem, or on an installed archive the rationale it seals per entity (sealed: rationale, kind, actor, timestamp; carried: false where it seals none). _hash is the optimistic-lock token. The nested metadata map holds every schema-declared frontmatter key (metadata.level); identity keys and underscore-prefixed slots stay top-level. Every relationships[] entry carries direction: default reads hold outgoing edges only (“out”, endpoint under target); after a memstead_relate the relate response’s _hash is the next valid expected_hash. include_relations: true adds the incoming edges (“in”, endpoint under from) and a direction-grouped ## Relations text section; include_context: true appends the community cluster. Pass sections to narrow output (also narrows structured_content.sections); when narrowed, _tokens_unfiltered_body surfaces the unfiltered-base cost. Opt-in inserts count only toward _tokens, which may then exceed it. Stubs render with empty sections, relationships and metadata. token_budget/chunk bound only the text channel (over-budget text adds _chunk/_total_chunks/_truncated); structured_content always ships whole, size it ahead via _tokens. Use memstead_overview for cold-start, memstead_search to find IDs, memstead_update to mutate.
Hints: read_only = true, destructive = false, idempotent = true, open_world = false
Input schema:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "chunk": { "description": "Which chunk to read (1-based). Only needed for entities that exceed the token budget.", "format": "uint", "minimum": 0, "type": [ "integer", "null" ] }, "id": { "description": "Full entity ID as returned by search/list (e.g. \"specs--my-entity\")", "type": "string" }, "include_context": { "description": "Append a `## Community Context` section — the entity's cluster summary, members, and bridges to other clusters.", "type": [ "boolean", "null" ] }, "include_provenance": { "description": "Append a `mutation_provenance` block to structured_content: `created_by` and `last_modified_by`, each with actor, client, the caller-declared `role` (or `unspecified` — absence served as cannot-confirm, never as a real role), the caller-declared `identity` when one was recorded (the independence gate's only comparator), timestamp, and the backend reference. Derived from the append-only mutation record (commit trailers / ledger), which no verb can edit after the fact — the tamper-evident half of the role and identity trust model. When the recorded story does not start at the entity's creation, `created_by` is absent and `story_truncated` is true (stated, never fabricated). Default false: responses are byte-unchanged without the flag.", "type": [ "boolean", "null" ] }, "include_relations": { "description": "Append a `## Relations` section with typed edges grouped by direction.", "type": [ "boolean", "null" ] }, "sections": { "description": "Only return these sections (default: all). Use to read specific parts of large entities.", "items": { "type": "string" }, "type": [ "array", "null" ] }, "token_budget": { "description": "Max tokens for the rendered-markdown text channel only. If the text exceeds this, returns chunk 1 of N with _truncated in its frontmatter; use the chunk param to read subsequent chunks. The structured_content envelope is never chunked or truncated by this — it always ships whole (size it ahead via its _tokens field).", "format": "uint", "minimum": 0, "type": [ "integer", "null" ] } }, "required": [ "id" ], "type": "object"}memstead_health
Section titled “memstead_health”Return graph health. Typed payload on structured_content; text chunks past token_budget via chunk. Default: counts (orphans_by_schema/communities_by_schema), node/edge totals, distributions, writable_mems/read_mems, default_writable_mem (omitted-mem target), mem_schemas, verdict_coverage (examined/advisory/not_examined). include keys: orphans, stubs, most_connected, missing_fields, stale (anchor clock first; anchor_fresh), dangling_links, tags, missing_required_outgoing, constraints, conformance, integrity, config, anchors, friction, open_questions (unknowns; resolution missing/unchecked), stale_derivations (moved targets), checks (state counts, independence), signals, labelling, ledger (FOLDER ledger vs files; git-branch absent), vital_signs, due. missing_fields adds issues[] (code beside missing). config = the include_config projection. conformance lints entities against target_schema or each pin into findings {id, axis, code, detail} + body_observations (undeclared body; kept by a write?); integrity adds UNRESOLVED_STUB, CROSS_MEM_EDGE_UNGRANTED (no grant, target mounted; unmounted target dangles instead) and DANGLING_LINK_TARGET_MISSING/DANGLING_LINK_NOT_RELATED/DANGLING_RELATION_TARGET_MISSING; dangling_links carry theirs in kind. tags: tag_distribution (limit-capped), tag_distribution_folded, untagged_entities. missing_required_outgoing/constraints: unsatisfied blocks/violations. anchors: per-mem resolves/drifted/recheck/unresolvable (artifact gone)/unobserved/dangling (entity gone), entity_end_unreconciled, population. Unknown keys: UNKNOWN_INCLUDE_KEY; limit caps at 10 (>100: LIMIT_CLAMPED). Warnings: see instructions. mem scopes every section and every warning to one mem (only the mem rosters stay global). include_config: true adds mutations (require_notes), the opaque plugin map, per-mem mems (origin, vcs, write_guidance, extra).
Hints: read_only = true, destructive = false, idempotent = true, open_world = false
Input schema:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "chunk": { "description": "Which chunk of the rendered-markdown text channel to read (1-based). Only needed when a multi-include report exceeds the token budget. `structured_content` is whole regardless.", "format": "uint", "minimum": 0, "type": [ "integer", "null" ] }, "include": { "description": "Detail sections to include (default: none — summary counts only). Allowed keys: orphans, stubs, most_connected, missing_fields, stale, dangling_links, tags, missing_required_outgoing, constraints, conformance, integrity, config, anchors, friction, open_questions, stale_derivations, checks, signals, labelling, ledger, vital_signs. `conformance` lints every entity against the effective schema and returns per-entity `findings` (`{id, axis, code, detail}` with write-time typed codes); `integrity` additionally projects the consistency axis into the same findings list: UNRESOLVED_STUB; CROSS_MEM_EDGE_UNGRANTED (an existing cross-mem edge the workspace grant table no longer permits — a state the default-deny write gate would refuse to create today; reported and strict, never a load refusal, and the detail names the referrer, the target and that the cause is the absent grant rather than a missing target); plus three dangling conditions, each with its own repair and each finding carrying it as a `repair` detail — DANGLING_LINK_TARGET_MISSING (a body wiki-link with no markdown file behind it: create the target entity, or remove the wiki-link), DANGLING_LINK_NOT_RELATED (a body wiki-link to a written entity the referrer does not relate to: relate the two, so the link is backed by a relationship row), and DANGLING_RELATION_TARGET_MISSING (a relationships row naming an entity absent from the store: remove the row, or create the target). A stub target on a relationships row is a legitimate forward reference and is not flagged. `friction` summarizes the workspace-local refusal ledger — counts per typed refusal code and per verb, with a per-code `by_reason` breakdown where the code carries a closed engine-owned discriminator, whole-ledger plus a recent 24h window; the ledger is local-only, refusals-only, and every recorded value is drawn from a closed engine-defined vocabulary (never parameters, free-form strings, or payload text). `open_questions` composes a per-mem worklist of what the holding does not know — stubs, anchors that are recheck / unresolvable (artifact gone) / unobserved (not measured) / dangling (entity gone), unsatisfied constraints, dangling links, and a paired process mem's open entries with negative findings under a distinct already-searched heading (done, keep off); every list is capped with an explicit `more` remainder. `stale_derivations` lists per mem every explicit edge on a derivation-declared rel-type whose target's current hash differs from the recorded baseline (`stale`) or that has no baseline (`unbaselined` — never fabricated as fresh or stale); re-assert the edge via memstead_relate to refresh the baseline. `ledger` reconciles a FOLDER mem's change ledger against the markdown files beside it (`{ledger_without_file, file_without_ledger}` per mem) — read-only, it never writes or tidies a ledger line; git-branch mems are ABSENT from the map rather than clean in it, because their change set is a real two-tree diff and the divergence cannot arise. `due` serves the due brief as data over the default 90-day window (the rows `memstead due` renders: entities whose schema-declared due date is past, with the days past, or due soon, with the days until; a reading, never a verdict). Unknown keys surface as UNKNOWN_INCLUDE_KEY on warnings.", "items": { "type": "string" }, "type": [ "array", "null" ] }, "include_config": { "default": false, "description": "When true, the response carries the `[mutations]` posture (`mutations.require_notes`), the opaque `[plugin.*]` pass-through map, and a per-writable-mem `mems` detail array: `{ name, origin, storage, durable, vcs: { gitdir, head } }`, plus `title`, `subject`, `write_guidance` and `extra` where the mem's config sets them. Each `vcs` field is present where it exists: `gitdir` (the absolute canonical git directory of a mem-branch mount; a folder mem has none), `head` (the branch-tip SHA; omitted on a fresh mem with no commits yet), `worktree` (only where the mem has a working tree: a folder mem, or a mem-branch mount with an on-disk folder, never a plain mem-branch mount). For the sync flow and any tooling that needs the paths, so they never hardcode a layout or peel refs themselves. Documented alias: `include: [\"config\"]` renders the identical projection (the catalogue form every surface shares, including the CLI's `--include config`); passing both renders it once. Defaults to false — the absence of these fields is the default-posture signal. **Lifecycle policy** (`[[mem_management.create]]` / `[[mem_management.delete]]`) is surfaced via `memstead_overview`, not here — `memstead_health` is drift/diagnostics.", "type": "boolean" }, "limit": { "description": "Max results for most_connected (default: 10, max: 100)", "format": "uint", "minimum": 0, "type": [ "integer", "null" ] }, "mem": { "description": "Scope counts, distributions, and detail lists to a single writable mem. `writable_mems`/`read_mems` still show the full roster so the agent sees the whole workspace. Omit (default) for global aggregates.", "type": [ "string", "null" ] }, "target_schema": { "description": "Schema ref (`name@x.y.z`) the `conformance`/`integrity` includes lint against instead of each mem's current pin. Omit (default) to lint against the current pin. Only consulted when `include` requests the conformance axis; an unresolvable ref refuses with SCHEMA_NOT_FOUND.", "type": [ "string", "null" ] }, "token_budget": { "description": "Max tokens for the rendered-markdown text channel. If the report exceeds this, the text returns chunk 1 of N with `_chunk`/`_total_chunks`/`_truncated` frontmatter; page with the `chunk` param. The `structured_content` envelope is never chunked — it always ships whole. Omit to use the server's configured default budget.", "format": "uint", "minimum": 0, "type": [ "integer", "null" ] } }, "type": "object"}memstead_mem_configure
Section titled “memstead_mem_configure”Update a mem’s curation fields — display title, one-line description, subject block, and the declared process mem — in one call: set what is present. Absent field = untouched; empty string (title / description) = clear; clear_subject: true clears the subject block as a unit (mutually exclusive with subject, both set refuses INVALID_INPUT). subject is {scope, method?, exclusions?} — what the mem covers, how its content was arrived at, what was deliberately left out. Display text, never identity: the mem stays addressed by name everywhere; a title is roster/UI text only. process_mem declares the mounted ingest@* mem that holds this mem’s process tier; the declaration wins over the binding-name convention on the brief and the open-questions axis, an empty string clears it, and a name that is not mounted, not pinned to ingest@*, or the mem itself refuses PROCESS_MEM_NOT_ELIGIBLE (details.reason names which). Same validation and storage as the CLI’s mem set-title / set-description / set-subject / set-process-mem — one config commit per touched field. Gate-free like the sibling setters (no [[mem_management.*]] allowlist applies), but every structural gate holds: unknown mem refuses UNKNOWN_MEM; read-only mounts refuse READ_ONLY_MOUNT. A call with no field present is a no-op returning the unchanged state. Response {mem, title, description, subject, process_mem, warnings} carries the post-call values (null = unset); MEM_RELOADED rides on warnings when a sibling engine commit landed since the prior snapshot. CONFIG_WRITE_INTERVENED rides there when the stored config had moved since this engine read it: the write lands ON TOP of theirs and details.fields names what they had changed. Optional note (≤280 chars) rides each field’s config commit.
Hints: read_only = false, destructive = false, idempotent = true, open_world = false
Input schema:
{ "$defs": { "MemSubjectInput": { "additionalProperties": false, "description": "Subject block for mem curation — mirrors\n`memstead_schema::MemSubject`.", "properties": { "exclusions": { "default": null, "description": "What was considered and deliberately left out — prose statements, order preserved.", "items": { "type": "string" }, "type": [ "array", "null" ] }, "method": { "default": null, "description": "How the mem's content was arrived at.", "type": [ "string", "null" ] }, "scope": { "description": "What this mem covers. Required to set the block.", "type": "string" } }, "required": [ "scope" ], "type": "object" } }, "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "clear_subject": { "default": false, "description": "Clear the subject block as a unit. Mutually exclusive with `subject` (both set refuses `INVALID_INPUT`).", "type": "boolean" }, "description": { "default": null, "description": "New one-line description. Absent = untouched; empty string = clear.", "type": [ "string", "null" ] }, "identity": { "default": null, "description": "WHO is acting: an opaque identity string of your choosing — an agent name, a session handle. Recorded immutably alongside the mutation (commit trailer / ledger); the author≠checker independence gate compares identities and nothing else. Caller-declared and unverified, but tamper-evident in append-only history. Omit to record the session default, or nothing — legal forever, never refused; identity-less records read unconfirmable at the gate. Over-length values refuse INVALID_IDENTITY (cap 128 chars).", "type": [ "string", "null" ] }, "name": { "description": "Name of the mem to configure (must be a registered writable mem).", "type": "string" }, "note": { "default": null, "description": "Optional provenance note (≤280 chars) recorded on each field's config commit.", "type": [ "string", "null" ] }, "process_mem": { "default": null, "description": "Declared process mem: the mounted mem, pinned to an `ingest@*` schema, that holds this mem's process tier (verification targets, findings, inquiry entries). The declaration wins over the binding-name convention on the brief and the open-questions health axis. Absent = untouched; empty string = clear (back to the convention). A name that is not mounted, not pinned to `ingest@*`, or the mem itself refuses `PROCESS_MEM_NOT_ELIGIBLE` with `details.reason`.", "type": [ "string", "null" ] }, "role": { "default": null, "description": "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier`. Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary.", "type": [ "string", "null" ] }, "subject": { "anyOf": [ { "$ref": "#/$defs/MemSubjectInput" }, { "type": "null" } ], "description": "New subject block `{scope, method?, exclusions?}`. Absent = untouched; to clear the block as a unit pass `clear_subject: true` instead." }, "title": { "default": null, "description": "New display title. Absent = untouched; empty string = clear (the roster falls back to the mem name).", "type": [ "string", "null" ] } }, "required": [ "name" ], "type": "object"}memstead_mem_create
Section titled “memstead_mem_create”Create and register a new writable mem at runtime. Requires workspace opt-in via [[mem_management.create]] rules (each pattern + schemas[]) — discover via memstead_overview’s ## Lifecycle Namespaces. Engine composes the lifecycle candidate, canonicalizes location, runs first-match-wins glob over the rule list, then checks schema against the matched rule’s schemas[] (["*"] admits any). Two error envelopes: MEM_PATH_NOT_ALLOWED carries details.candidate, details.patterns, details.reason (no_allowlist_configured / no_match / outside_workspace); MEM_SCHEMA_NOT_ALLOWED carries details.candidate, details.matched_pattern, details.requested_schema, details.allowed_schemas. Name-collision check runs only after a path match — out-of-namespace collision surfaces as MEM_PATH_NOT_ALLOWED, not MEM_NAME_COLLISION. Storage-residue probe catches residue surviving a prior memstead mem unregister or a crash; residue left by a deliberate unregister reattaches and emits MEM_REATTACHED_AFTER_UNREGISTER (audit signal); residue from a crash refuses with MEM_STORAGE_RESIDUE_DETECTED — run memstead mem delete <name> first. Cross-mem edge authorization is workspace policy ([cross_mem_links]); the matched create-rule may carry default_cross_links. Bootstraps the gitdir per vcs, loads any pre-existing markdown, and produces a seed commit carrying note (≤280 chars). Response carries location, seed_write_id (the identity the mem’s backend minted for the seed write: a commit SHA on a git-branch mem, an opaque synthetic token on a folder mem; not a change cursor), and schema_ref. Pass include_schema: true to also inline the full schema body — byte-identical to memstead_schema(name=<resolved-schema>). Default false. A mem already present at the location returns CONFIG_ERROR. MEM_NAME_REF_CONFLICT: a git-branch name an existing mem branch sits above or below refuses before any write, details.suggestion naming a sibling; a failed seed rolls its config back.
Hints: read_only = false, destructive = false, idempotent = false, open_world = false
Input schema:
{ "$defs": { "MemSubjectInput": { "additionalProperties": false, "description": "Subject block for mem curation — mirrors\n`memstead_schema::MemSubject`.", "properties": { "exclusions": { "default": null, "description": "What was considered and deliberately left out — prose statements, order preserved.", "items": { "type": "string" }, "type": [ "array", "null" ] }, "method": { "default": null, "description": "How the mem's content was arrived at.", "type": [ "string", "null" ] }, "scope": { "description": "What this mem covers. Required to set the block.", "type": "string" } }, "required": [ "scope" ], "type": "object" }, "RecoveryActionInput": { "description": "Wire-shape recovery action for `memstead_mem_create`. The\nstorage-residue refusal path exposes three explicit\nrecovery options the caller picks via this enum. The wire\ntokens (`reattach` / `force_overwrite` / `hard_cleanup_first`)\nmatch `memstead_base::RecoveryAction::as_wire_str()` so the\nMCP serde shape and the CLI flag bridge converge on a single\nengine-side enum.", "oneOf": [ { "const": "reattach", "description": "Adopt the residual entities; skip the seed commit. Default\nwhen the residue was left by a deliberate `memstead mem\nunregister`. Explicit `reattach` overrides the default for\ncrash-residue scenarios where the operator has verified the\ncontent is safe to adopt.", "type": "string" }, { "const": "force_overwrite", "description": "Destroy the residue, then proceed with the normal create\npath: the residual branch and its `__MEMSTEAD` config blob are\npruned in one ref-edit transaction before the fresh seed\ncommit. Prior entities are gone by design.", "type": "string" }, { "const": "hard_cleanup_first", "description": "Refuse with `MEM_STORAGE_RESIDUE_DETECTED`, instructing the\ncaller to run `memstead mem delete <name>` first. Hard barrier\nagainst destructive auto-recovery — for operators who want\nthe cleanup to be a separate, named operation.", "type": "string" } ] }, "VcsConfigInput": { "additionalProperties": false, "description": "On-the-wire shape mirroring `memstead_schema::VcsConfig` with a\n`JsonSchema` derivation for rmcp tool routing. Kept separate from the\ncore type so the schema crate does not need a `schemars` dependency\njust to support one MCP-facing parameter. The fields and semantics\nmatch 1:1 — see `memstead_schema::VcsConfig` for the canonical\ndocumentation.", "properties": { "gitdir": { "description": "Path to the gitdir relative to the new mem's root.", "type": "string" }, "worktree": { "default": ".", "description": "Path to the worktree relative to the new mem's root. Defaults to `\".\"` (mem root) when omitted.", "type": "string" } }, "required": [ "gitdir" ], "type": "object" } }, "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "description": { "default": null, "description": "Optional one-line description applied at creation — embedded in `.mem` archive exports and surfaced on rosters. Same storage as the CLI's `mem set-description`.", "type": [ "string", "null" ] }, "identity": { "default": null, "description": "WHO is acting: an opaque identity string of your choosing — an agent name, a session handle. Recorded immutably alongside the mutation (commit trailer / ledger); the author≠checker independence gate compares identities and nothing else. Caller-declared and unverified, but tamper-evident in append-only history. Omit to record the session default, or nothing — legal forever, never refused; identity-less records read unconfirmable at the gate. Over-length values refuse INVALID_IDENTITY (cap 128 chars).", "type": [ "string", "null" ] }, "include_schema": { "default": false, "description": "Inline the resolved schema body on the response (byte-identical to `memstead_schema(name=<resolved-schema>)` at the same verbosity). Default `false` — the response carries only `schema_ref`, `name`, `location`, and `seed_write_id`. Set to `true` for first-time-schema callers that want one round-trip instead of two; the schema is workspace-stable, so for the agent's second+ mem on the same schema the omitted default is the right call.", "type": "boolean" }, "location": { "description": "Target filesystem location. Absolute path, or relative to the workspace root. Canonicalized before the allowlist check — `./a/../b` is reduced to `./b` prior to matching.", "type": "string" }, "name": { "description": "Unique name for the new mem — the full hierarchical identifier (e.g. `\"sub-mem\"` for flat layouts or `\"team/sub-mem\"` for hierarchical layouts); the value flows through verbatim. Grammar: lowercase ASCII letters, digits, hyphens; segments separated by `/`; no leading, trailing, or double slashes. Must not collide with any currently-registered mem.", "type": "string" }, "note": { "description": "Agent-authored provenance note recorded in the seed commit's body (≤280 chars). One sentence describing why this mem was created.", "type": [ "string", "null" ] }, "recovery": { "anyOf": [ { "$ref": "#/$defs/RecoveryActionInput" }, { "type": "null" } ], "description": "Explicit recovery action when on-disk storage residue is detected at the composed branch path. Three accepted values: `reattach` (adopt the residual entities, skip the seed commit), `force_overwrite` (destroy the residue — it is removed atomically, so either the residue is gone and the mem is created or nothing changed; the prior entities are gone by design), `hard_cleanup_first` (refuse with `MEM_STORAGE_RESIDUE_DETECTED`, instructing the caller to run `memstead_mem_delete` first). When omitted, the engine routes by whether the residue was left by a deliberate `memstead mem unregister`: such residue defaults to `reattach` and emits a `MEM_REATTACHED_AFTER_UNREGISTER` warning; residue from a crash refuses with `MEM_STORAGE_RESIDUE_DETECTED`. Bare create against a name with no residue ignores this field." }, "role": { "default": null, "description": "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier`. Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary.", "type": [ "string", "null" ] }, "schema": { "description": "Schema pin for the new mem. Format: `name@x.y.z` — e.g. `default@1.3.0`. Resolved against the per-mem schema registry at init time.", "type": "string" }, "schema_verbosity": { "description": "Verbosity of the inlined schema body when `include_schema: true`. `\"lite\"` (default, absent) inlines the cheap cold-start skeleton (entity-type names + section keys + field shapes, relationship names + endpoints, the alias pointer; prose dropped) — the right pairing for a first-mem create that only needs to orient, and byte-identical to `memstead_schema`'s default reply. `\"full\"` inlines the complete schema — byte-identical to `memstead_schema(name=<resolved-schema>, verbosity=\"full\")`. Ignored when `include_schema` is false. Any value other than `\"full\"`/`\"lite\"` returns `INVALID_INPUT` naming the bad value.", "type": [ "string", "null" ] }, "subject": { "anyOf": [ { "$ref": "#/$defs/MemSubjectInput" }, { "type": "null" } ], "description": "Optional subject block applied at creation — `{scope, method?, exclusions?}`: what the mem covers, how its content was arrived at, and what was deliberately left out. Same storage as the CLI's `mem set-subject`." }, "title": { "default": null, "description": "Optional human-readable display title applied at creation — display text, not identity (the mem is always addressed by `name`). Same validation and storage as the CLI's `mem set-title`. Omit to leave the mem untitled.", "type": [ "string", "null" ] }, "vcs": { "anyOf": [ { "$ref": "#/$defs/VcsConfigInput" }, { "type": "null" } ], "description": "Optional VCS layout override. Shape: `{ \"gitdir\": \".git\", \"worktree\": \".\" }` (default isolated) or `{ \"gitdir\": \"../.git\", \"worktree\": \"..\" }` (shared-gitdir idiom). Paths are relative to the new mem's root. When absent, the engine uses the isolated default." }, "write_guidance": { "additionalProperties": true, "default": {}, "description": "Optional per-instance writing guidance, written verbatim into the new mem's config `writeGuidance` map in the seed commit. An opaque string-keyed JSON object — e.g. `{ \"phase_context\": \"early design\", \"stack\": \"Rust\" }`. The engine never interprets the keys (schema-strictness D8 — `writeGuidance` is client-owned vocabulary); a client that read the resolved schema package's `mem-template.json` fills the instance keys and passes them here. Omit (or pass `{}`) to seed no guidance.", "type": "object" } }, "required": [ "name", "location", "schema" ], "type": "object"}memstead_mem_delete
Section titled “memstead_mem_delete”Remove a writable mem at runtime — always destructive: removes the mem and prunes every backend-visible artifact. Requires workspace opt-in via [[mem_management.delete]] rules (see memstead_overview’s ## Lifecycle Namespaces). Engine resolves name (UNKNOWN_MEM otherwise), composes the lifecycle candidate from the mem’s full hierarchical path (or the bare name for flat-layout mems), runs first-match-wins glob lookup over the delete rule list (refusing MEM_PATH_NOT_ALLOWED — details.candidate/details.patterns/details.reason discriminate no_allowlist_configured vs no_match). Refuses MEM_REFERENCED_BY_POLICY when the workspace cross_mem_links policy grants this mem as a write target (details.referring_mems names them). Refuses MEM_HAS_INCOMING_REFS when write-mem graph edges still target it (details.referrers lists each {from_id, rel_types, mem} — remove via memstead_relate / memstead_update first). On success the mem is gone: reads no longer see it and its storage is removed. The workspace policy is atomically scrubbed of the now-dangling [cross_mem_links] grants naming the deleted mem on either side. The [[mem_management.*]] allowlist rules are PRESERVED (exact-name and wildcard alike) — forward-looking permissions for the name; re-creating the same name needs no fresh allow rules. On a mem-repo workspace note (≤280 chars) rides the prune commit on the schema-and-config ref with the role and identity trailers; a folder-mem deletion produces no commit to carry it. Response: name, deleted_from_router: true, files_deleted: true, and allowlist_entries_removed[{table, pattern?, from?, to?}] listing the scrubbed cross-link grants (table is always cross_mem_links; empty when none named the mem). On partial cleanup failure files_deleted ends false and MEM_FILES_NOT_DELETED warnings name the survivors: details.reason is rmdir_failed (with details.path + details.error) or backend_prune_failed (with details.error).
Hints: read_only = false, destructive = true, idempotent = false, open_world = false
Input schema:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "identity": { "default": null, "description": "WHO is acting: an opaque identity string of your choosing — an agent name, a session handle. Recorded immutably alongside the mutation (commit trailer / ledger); the author≠checker independence gate compares identities and nothing else. Caller-declared and unverified, but tamper-evident in append-only history. Omit to record the session default, or nothing — legal forever, never refused; identity-less records read unconfirmable at the gate. Over-length values refuse INVALID_IDENTITY (cap 128 chars).", "type": [ "string", "null" ] }, "name": { "description": "Name of the mem to destroy.", "type": "string" }, "note": { "description": "Agent-authored provenance note (≤280 chars). On a mem-repo workspace it rides the prune commit on the schema-and-config ref, the one commit a deletion produces, beside the role and identity trailers; a folder-mem deletion produces no commit to carry it. Under `require_notes` a missing note warns `NOTE_MISSING`.", "type": [ "string", "null" ] }, "role": { "default": null, "description": "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier`. Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary.", "type": [ "string", "null" ] } }, "required": [ "name" ], "type": "object"}memstead_mem_set_schema
Section titled “memstead_mem_set_schema”Update a mem’s schema pin — the integrity-driven schema-migration trigger. Stable response {mem, schema_pin, migration_target, outcome, findings, stamped_schema} (stamped_schema: the generation the mem’s mutation stamp, the ENGINE_VERSION_SKEW marker, names after the call — a completed switch re-stamps it with the target, a dual-pin entry leaves it); branch on outcome: noop (requested == current pin), switched (mem already integral against the target — pin moved atomically), migration_started (not integral — mem enters dual-pin: writes now validate against the target, findings lists the non-integral entities as {id, axis, code, detail}), migration_pending (same target re-issued while repairs remain — findings carries the remaining entities). Migration loop: read findings, read both schemas via memstead_schema, repair each entity via memstead_update (validated strictly against the target; relations_unset is available on non-conformant entities), then re-issue this call — once every entity is integral it completes the switch. Reads stay permissive throughout; the dual-pin state survives engine restarts. Unknown mem refuses UNKNOWN_MEM; a schema ref that resolves to no loaded schema refuses SCHEMA_NOT_FOUND; malformed refs refuse INVALID_INPUT. Distinct from memstead_mem_set_version, which sets the mem content version, never the pin.
Hints: read_only = false, destructive = false, idempotent = false, open_world = false
Input schema:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "identity": { "default": null, "description": "WHO is acting: an opaque identity string of your choosing — an agent name, a session handle. Recorded immutably alongside the mutation (commit trailer / ledger); the author≠checker independence gate compares identities and nothing else. Caller-declared and unverified, but tamper-evident in append-only history. Omit to record the session default, or nothing — legal forever, never refused; identity-less records read unconfirmable at the gate. Over-length values refuse INVALID_IDENTITY (cap 128 chars).", "type": [ "string", "null" ] }, "mem": { "description": "Name of the writable mem whose schema pin is being set.", "type": "string" }, "note": { "description": "Optional provenance note (≤280 chars). Reserved: the pin lives in workspace state today (no mem commit is produced), so the note is accepted for wire-compat and recorded once the pin-relocation cut moves the schema pin into mem config.", "type": [ "string", "null" ] }, "role": { "default": null, "description": "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier`. Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary.", "type": [ "string", "null" ] }, "schema": { "description": "Target schema ref, exact `name@x.y.z`. Must resolve against the loaded schema catalogue (mem-pinned, workspace, built-in); unresolvable refs refuse with SCHEMA_NOT_FOUND, malformed refs with INVALID_INPUT.", "type": "string" } }, "required": [ "mem", "schema" ], "type": "object"}memstead_mem_set_version
Section titled “memstead_mem_set_version”Update a registered mem’s version field. The version is consumed by memstead_export --format mem to stamp the archive filename and the .mem archive’s published config — bump before publishing. Mem-create seeds 0.1.0 automatically, so this tool is the only surface that needs to fire when an agent or operator is ready to ship a new version. Gate-free: no [[mem_management.*]] allowlist check, no operator-mode bypass needed. Validates the new version as semver; malformed values refuse with INVALID_INPUT. Unknown mem name refuses with UNKNOWN_MEM; read-only mem refuses with READ_ONLY_MOUNT; a mem whose config failed to load returns INVALID_INPUT. Response carries {mem, old_version, new_version, warnings}; MEM_RELOADED rides on warnings when a sibling engine commit landed between the engine’s prior snapshot and this write (no extra read needed to learn the drift). CONFIG_WRITE_INTERVENED rides there too when the stored config had moved since this engine read it: the write lands ON TOP of theirs (nothing of theirs is lost) and details.fields names what they had changed.
Hints: read_only = false, destructive = false, idempotent = false, open_world = false
Input schema:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "identity": { "default": null, "description": "WHO is acting: an opaque identity string of your choosing — an agent name, a session handle. Recorded immutably alongside the mutation (commit trailer / ledger); the author≠checker independence gate compares identities and nothing else. Caller-declared and unverified, but tamper-evident in append-only history. Omit to record the session default, or nothing — legal forever, never refused; identity-less records read unconfirmable at the gate. Over-length values refuse INVALID_IDENTITY (cap 128 chars).", "type": [ "string", "null" ] }, "name": { "description": "Name of the mem whose `version` field is being updated.", "type": "string" }, "note": { "description": "Optional provenance note (≤280 chars) recorded on the version-bump commit body. When the workspace sets `require_notes`, omitting it rides a non-blocking `NOTE_MISSING` warning (the bump still lands).", "type": [ "string", "null" ] }, "role": { "default": null, "description": "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier`. Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary.", "type": [ "string", "null" ] }, "version": { "description": "New semver version (e.g. `0.2.0`, `1.0.0-beta.1`). Validated as semver; malformed values refuse with `INVALID_INPUT`. The version is consumed by `memstead_export --format mem` to stamp the archive filename and the `.mem` archive's published config — bump before publishing. Initial mem-create seeds `0.1.0` so this surface is the only path that needs to be invoked when an agent or operator is ready to ship.", "type": "string" } }, "required": [ "name", "version" ], "type": "object"}memstead_overview
Section titled “memstead_overview”Start here. Returns the schema catalogue, mem inventory, and community clusters as markdown. Schemas list as {ref, description} only — call memstead_schema(name=<ref>) for per-type bodies (lite skeleton by default; verbosity: "full" for prose write_rules/guidance) before any memstead_create / memstead_update / memstead_relate; cache per session, schema is workspace-stable. Token-budget-driven: hard-required content (mem roster, schema refs, community titles, workspace policy) always ships; heavy content is greedy-filled into the remaining budget by default-priority. Anything that didn’t fit appears in the ## Hints section with estimated_tokens; re-query by passing key into include[]. Override priority with include: keys there always ship, even past budget. Allowed include keys: community_members, community_bridges, mem_distribution, dangling_links. Control the budget via token_budget (default 8000). Frontmatter _overview_mode is "complete" (nothing dropped), "reduced" (heavy content omitted — see the Hints section), or "overbudget" (hard-required content alone exceeded the budget; raise token_budget or scope with mem). Workspace-level mutation and link policy is surfaced in ## Workspace policy and mirrored into the _policy frontmatter slot — entries appear only when the value deviates from the engine default (require_notes, cross_mem_links posture). Frontmatter _verdict_coverage: examined/advisory/not_examined buckets. Frontmatter _workspace_root is the engine’s absolute workspace path — target CLI calls with it, never with cwd. Pass mem to scope mems and schemas to any one visible mem (read-only mounts included); community detection stays workspace-global — mem only filters which clusters are reported (caveats on the mem param). rebuild: true recomputes the global partition. Non-fatal issues surface under ## Warnings with a stable code.
Hints: read_only = true, destructive = false, idempotent = true, open_world = false
Input schema:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "chunk": { "description": "Which chunk to read (1-based). Only needed if overview exceeds the MCP response cap.", "format": "uint", "minimum": 0, "type": [ "integer", "null" ] }, "include": { "description": "Opt into heavy content. Allowed keys: \"community_members\" (entity lists per cluster), \"community_bridges\" (inter-cluster edge aggregation with up to 3 sample edges per pair), \"mem_distribution\" (per-mem type_distribution), \"dangling_links\" (renders a `## Dangling Links` section, one line per unresolved reference as `[kind] source → target (in section)`, where `kind` is `DANGLING_LINK_TARGET_MISSING` (no markdown file behind the link: create the target, or remove the link), `DANGLING_LINK_NOT_RELATED` (the target is written but the referrer does not relate to it: relate the two) or `DANGLING_RELATION_TARGET_MISSING` (a relationships row naming an entity absent from the store: drop the row, or create the target); richer aggregation tracked in #12/#13). `include` keys are always shipped regardless of the token budget — use it to force content you need. Unknown keys emit a typed `warnings` entry. Schema bodies are not in this set — call memstead_schema(name=...) for the full per-type catalogue.", "items": { "type": "string" }, "type": [ "array", "null" ] }, "mem": { "description": "Restrict `mems[]` and `schemas[]` to any single visible mem — read-only mounts included. `used_by` inside each schema still lists all mems sharing it. Community scope: `mem` filters which clusters are *reported* (and makes `community_bridges` source-in-mem only) — it does NOT re-run detection per mem. Detection is always workspace-global and cluster ids stay the global-pass ids; passing `mem` never renumbers or re-scopes the partition. Because detection is global and disconnected / sparsely-connected nodes collapse into a single catch-all rather than forming their own cluster, a small or isolated mem-local subgraph may surface as no cluster at all under a `mem` filter.", "type": [ "string", "null" ] }, "rebuild": { "description": "Re-run community detection before returning overview (default: false). Detection is workspace-global: `rebuild` recomputes the Louvain partition over the *whole* workspace graph — it never scopes to `mem`, even when `mem` is also passed.", "type": [ "boolean", "null" ] }, "token_budget": { "description": "Target token budget for heavy content only (`community_members`, `community_bridges`, `mem_distribution`, `dangling_links`). Default: 8000. Hard-required content (mem roster, schema refs with relationship vocabulary, community titles, workspace policy) always ships in addition — total response size will exceed this budget. When hard-required content alone exceeds the budget, `overview_mode=\"overbudget\"` signals the agent to raise the budget or scope via `mem`. Heavy content not in `include` is greedy-filled until the budget is exhausted; anything left over is advertised in `hints[]` with `estimated_tokens`. `include` keys bypass the budget. Budgets below ~10 tokens are safe but unproductive — the structured envelope still arrives (`overview_mode=\"overbudget\"`) but no useful chunking happens and the full body ships as one chunk.", "format": "uint", "minimum": 0, "type": [ "integer", "null" ] } }, "type": "object"}memstead_proposal_brief
Section titled “memstead_proposal_brief”Return the review brief of a fork against the mem it was forked from: the entity-level three-way picture the owner reads before merging, and the disposition file to fill. Read-only: nothing is written anywhere. fork names a mounted git-branch mem whose config records its origin (memstead mem fork writes it); its changes are read against its base (the fork commit, or the ancestor for an older fork) and the source mem’s branch tip at call time, and the response names ancestor, base, fork_tip and target_tip so a merge can pin them. entries[] carries one entry per entity the fork touched, sorted by slug, each with status (added, modified, deleted, renamed), fork_delta (the sections and metadata that differ against the base; a self-link the fork commit retargeted is never a difference), referrers (the target’s), anchors (the fork’s rows: artifact, grain, class, span, last recorded state; nothing is observed), precheck (would the body pass the target’s write gate: clean, failed with the gate’s typed codes, or not_run; never a content judgement), conflict where the target modified, deleted or created the same entity since the ancestor (with the three bodies), and re_proposal marks where the target’s proposal record already rejected the same content hash or id. dispositions is the skeleton keyed by slug: empty disposition and reason, and the accepted values adopt, adopt_with_changes, reject (no adopt on a conflict); description is the empty slot for the proposal in the owner’s words, recorded by the merge. The text channel carries the markdown rendering, structured_content the JSON. Refusal codes: INVALID_INPUT (the mem records no origin, or a side is not git-backed), UNKNOWN_MEM (the fork, or its source, is not mounted), UNKNOWN_REF (a recorded sha no longer resolves). The merge is CLI-only (memstead proposal merge).
Hints: read_only = true, destructive = false, idempotent = true, open_world = false
Input schema:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "fork": { "description": "The fork mem: a mounted git-branch mem whose config records its origin (`memstead mem fork` writes it). Its changes are read against its base (the fork commit, or the ancestor for an older fork) and the source mem's branch tip at call time. A mem with no recorded origin refuses with `INVALID_INPUT`; a fork whose source mem is not mounted refuses with `UNKNOWN_MEM`.", "type": "string" } }, "required": [ "fork" ], "type": "object"}memstead_relate
Section titled “memstead_relate”Connect entities with typed edges — a list of relation operations applied atomically. relations carries one or more {from, to, rel_type, remove?, description?} entries; the whole list is all-or-nothing in ONE commit per touched mem, per-entry validation identical to a single operation, in-order semantics (later entries validate against the state earlier entries produced; an acyclic check sees edges added earlier in the list). A single-relation call is a list of one. Pre-fetch the mem’s schema via memstead_schema (see server instructions). Type names case-insensitive; stored UPPER_SNAKE_CASE. One failing entry refuses the WHOLE list — nothing commits, every failing entry reported: a list of one surfaces its entry’s own typed code top-level (INVALID_REL_TYPE with details.allowed + suggestion, INVALID_REL_SHAPE, CROSS_MEM_LINK_NOT_ALLOWED, CROSS_MEM_TARGET_NOT_FOUND, RELATIONSHIP_CYCLE with details.existing_path, INVALID_ENTITY_ID); larger lists wrap under BATCH_REFUSED with details.entries[] of {index, from, to, rel_type, code, message, details} (errors_suppressed counts envelopes past the cap). Remove skips shape validation. Per entry: remove: true deletes; from must be real; to may auto-stub (AUTO_STUB_CREATED; into an uncreated mem: CROSS_MEM_TARGET_MEM_UNCREATED). Add-existing / remove-missing are typed-warning no-ops (DUPLICATE_RELATIONSHIP / NO_SUCH_RELATIONSHIP, action: "noop"). Response: results[] in submission order, each {from, to, rel_type, action, source, _hash} — _hash is that source’s next expected_hash; top-level write_id (not a cursor; empty when all no-op), warnings, orphan_stubs_removed (stubs GC’d when a removed edge was their last referrer; surviving body wiki-links refuse RELATION_HAS_BODY_LINKS). Optional note rides every entry. dry_run: true rehearses the list: same validation and refusals, would-be actions and stubs reported, nothing lands; write_id stays empty (the rehearsal marker). Edges never move files.
Hints: read_only = false, destructive = false, idempotent = true, open_world = false
Input schema:
{ "$defs": { "RelateOpInput": { "additionalProperties": false, "description": "One relation operation in `memstead_relate`'s list.", "properties": { "description": { "default": null, "description": "Optional per-edge description applied on add. Validated against the rel-type's `per_edge_description` posture in the pinned schema: `forbidden` (default) rejects a non-empty description with `DESCRIPTION_NOT_PERMITTED`; `required` rejects its absence with `MISSING_REQUIRED_DESCRIPTION`; `optional` accepts both. Empty / whitespace-only strings normalise to absent before validation. Ignored on the remove path.", "type": [ "string", "null" ] }, "from": { "description": "Full source entity ID", "type": "string" }, "rel_type": { "description": "Relationship type. Canonical form is UPPER_SNAKE_CASE (USES, PART_OF, DEPENDS_ON) and is what the engine stores; case-insensitive inputs (`uses`, `Part_Of`) are accepted and echoed back in the response as their canonical form. The JSON Schema `pattern` advertises `^[A-Za-z][A-Za-z_]*$` for client-side validators; the engine enforces the same character set independently — characters outside it return `INVALID_REL_TYPE` at the engine boundary regardless of whether the client pre-filters.", "pattern": "^[A-Za-z][A-Za-z_]*$", "type": "string" }, "remove": { "description": "Set true to remove the relationship instead of creating it", "type": [ "boolean", "null" ] }, "to": { "description": "Full target entity ID", "type": "string" } }, "required": [ "from", "to", "rel_type" ], "type": "object" } }, "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "dry_run": { "description": "Validate and preview the relation operations without executing — no edge lands, no stub is created, no VCS commit. dry_run runs the SAME validation a real call runs (cross-mem policy, vocabulary, description posture, acyclicity, self-loop refusal); an illegal operation refuses with the IDENTICAL typed envelope a real call would return, and a legal one reports the would-be action with `_hash` set to the PROSPECTIVE post-write source hash, `write_id` empty (the rehearsal marker), and any would-be `AUTO_STUB_CREATED` warning for an absent target — reported, never created. The follow-up real call on an unchanged mem succeeds; like create's dry_run, its `_hash` diverges from the rehearsed one whenever a wall-clock second ticks between the calls (the auto-stamped `last_modified` enters the hash) — a timestamp shift, not drift.", "type": [ "boolean", "null" ] }, "identity": { "description": "WHO is acting: an opaque identity string of your choosing — an agent name, a session handle. Recorded immutably alongside the mutation (commit trailer / ledger); the author≠checker independence gate compares identities and nothing else. Caller-declared and unverified, but tamper-evident in append-only history. Omit to record the session default, or nothing — legal forever, never refused; identity-less records read unconfirmable at the gate. Over-length values refuse INVALID_IDENTITY (cap 128 chars).", "type": [ "string", "null" ] }, "note": { "description": "Agent-authored provenance note (≤280 chars, one sentence describing why this mutation happened). Lands in the per-mem commit body between the mechanical subject line and the provenance trailers (`Tool:`, `Actor:`, `Client:`), where `memstead_changes_since` with `include_notes` reads it back. Omit for pure-housekeeping edits; when `[mutations].require_notes = true` in workspace config a missing note adds a `NOTE_MISSING` `WarningHint` to the response (the mutation still commits).", "type": [ "string", "null" ] }, "relations": { "description": "Relation operations, applied atomically in order — all-or-nothing in one commit per touched mem. Each entry is `{from, to, rel_type, remove?, description?}` with per-entry validation identical to a single call; later entries validate against the graph state produced by earlier ones (an acyclic check sees edges added earlier in the list). A single failing entry refuses the WHOLE list and the refusal reports every failing entry.", "items": { "$ref": "#/$defs/RelateOpInput" }, "type": "array" }, "role": { "description": "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier`. Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary.", "type": [ "string", "null" ] } }, "required": [ "relations" ], "type": "object"}memstead_reload
Section titled “memstead_reload”Reload one writable mem’s slice of the in-memory store from its on-disk branch tip — or every writable mem when mem is omitted. For multi-engine coexistence: a sibling (forked subagent, parallel terminal) or out-of-band git pull may have advanced HEAD past this engine’s snapshot. The auto-reload-on-read pipeline surfaces MEM_RELOADED on the next read; this tool is explicit operator-driven refresh for the rare cases the throttle missed. Not a workaround for direct .md edits — restart the server instead. Per-mem form is cheap (~10 ms per few-hundred-entity mem); workspace-wide scales linearly. Response: reports[], each entry { mem, head_before, head_after, entities_loaded, changed_entity_ids[] }. head_before is the engine’s prior cached SHA (canonical empty-tree hash for fresh mems); head_after is the freshly-peeled branch tip. changed_entity_ids is the union of added ∪ content-hash-changed ∪ removed entity ids — pass head_before to memstead_changes_since for the full per-entity diff. The workspace-wide form (omit mem) additionally picks up CLI writes to allowlist / cross-link / mutation policy (via memstead workspace allow-create etc.) without process restart. Per-mem form skips that workspace-level settings refresh. Membership and the schema catalogue are fixed at boot for both default forms. full: true (workspace-wide only) adds the ADDITIVE re-scan: out-of-band schema installs become resolvable, out-of-band mems mount cold, no restart; removals are skipped and reported — a deleted mem leaves the roster only on restart (its content sweep still reads current storage, so a hard-deleted branch reads empty while membership stays). Response adds refresh {schemas_added, schema_removals_skipped, mems_mounted, mem_removals_skipped, failures, elapsed_ms}; per-item failures never surface as available and never abort the rest. In-band lifecycle: memstead_mem_create / memstead_mem_delete.
Hints: read_only = false, destructive = false, idempotent = true, open_world = false
Input schema:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "full": { "description": "Full refresh. `true` re-scans the schema sources and reconciles the mount roster ON TOP of the workspace-wide content reload — the same reconciliation every operation runs before it serves (`MEM_ROSTER_CHANGED`), forced here so the report is authoritative: schema versions installed out of band become resolvable, mems registered out of band mount cold, mems unregistered out of band are UNMOUNTED (their entities, search index and community partition gone; an operation naming one refuses `MEM_UNMOUNTED`), a roster entry that fails to mount is quarantined with its reason. Schema removals stay skipped. The response gains a `refresh` block — `schemas_added`, `schema_removals_skipped`, `mems_mounted`, `mems_unmounted`, `mems_quarantined`, `failures[]` (per-item; a failed source or mount never surfaces as newly available and does not abort the others), `elapsed_ms`. Incompatible with `mem`. Default `false`: the content reload alone (membership is still reconciled by the per-operation probe).", "type": [ "boolean", "null" ] }, "mem": { "description": "Writable mem name to reload. Omit to reload every writable mem. Use the per-mem form for cheap, targeted refreshes when you know which mem drifted; use the workspace-wide form (omit `mem`) when an out-of-band `git pull` may have advanced multiple branches at once, or to pick up CLI-driven workspace-policy edits (allowlist / cross-link / mutation policy) — per-mem reload skips that workspace-level settings refresh. Reload scope covers **mem-scoped** state: a mem's `sync_state` (the projection baselines `#synced`/`#verified`) rides its destination mem's config, so an out-of-band `sync_state` write — e.g. from `memstead projection advance` / `mem set-sync-state` in a sibling process — is picked up by a per-mem (or workspace-wide) reload of that mem. The engine-owned advance/disposition store (`.memstead/state/advance/`) is **workspace-store** state read fresh from disk per operation — it is reload-independent by design and is neither refreshed nor invalidated by this call.", "type": [ "string", "null" ] } }, "type": "object"}memstead_rename
Section titled “memstead_rename”Rename an entity by changing its title. Updates the entity id and its file path ({new_slug}.md). Atomic referrer rewrite: every Write-Mem entity whose relationships or section bodies point at the old id has its [[old-slug]] tokens rewritten in one per-mem commit. Cross-mem referrers are gated by cross_mem_links policy in the propagated edge’s direction — a blocked direction aborts up-front with RENAME_BLOCKED_BY_CROSS_MEM_POLICY (details.from_mem, details.blocked_referrers). Per-peer commits are parent-pinned; sibling-writer drift mid-rename surfaces RENAME_PARTIAL_FAILURE (details.committed_mems, details.failed_mem, details.failure_cause) — retry after reloading. Per-mem commits share a logical_operation_id — correlate via memstead_changes_since. ReadOnly referrers can’t be rewritten; the old id demotes to a stub holding their edges (warning RESIDUAL_STUB_FOR_READONLY_REFERRERS). Requires expected_hash; mismatch emits HASH_MISMATCH (details.current). Slug-noop: a new title whose slug matches the current one returns old_id == new_id, empty write_id, and warning TITLE_NORMALIZED_TO_SLUG_NOOP. ID collisions error. Titles accept any single-line text (control characters such as tab/newline are rejected); the title is stored verbatim as display text, while characters outside Unicode alphanumerics, whitespace, and hyphen are dropped from the derived slug — warning TITLE_CHARS_DROPPED_FROM_SLUG names them (INVALID_TITLE remains for control chars, empty-deriving titles, over-long ids). Stubs cannot be renamed. Optional note (≤280 chars) — shared provenance contract, see memstead_create. Response: old_id, new_id, _hash (the next expected_hash), warnings, and write_id (an identity, never a change cursor). Anchors and check records follow the id: checks carry over marked with their old id and keep their hash, so the renamed entity reads check_stale (the file changed) until re-checked, never never_checked.
Hints: read_only = false, destructive = false, idempotent = false, open_world = false
Input schema:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "expected_hash": { "description": "Hash from memstead_entity (_hash). Required. Mismatch returns code HASH_MISMATCH with details.current carrying the current on-disk hash.", "type": "string" }, "id": { "description": "Full current entity ID", "type": "string" }, "identity": { "description": "WHO is acting: an opaque identity string of your choosing — an agent name, a session handle. Recorded immutably alongside the mutation (commit trailer / ledger); the author≠checker independence gate compares identities and nothing else. Caller-declared and unverified, but tamper-evident in append-only history. Omit to record the session default, or nothing — legal forever, never refused; identity-less records read unconfirmable at the gate. Over-length values refuse INVALID_IDENTITY (cap 128 chars).", "type": [ "string", "null" ] }, "new_title": { "description": "New title for the entity", "type": "string" }, "note": { "description": "Agent-authored provenance note (≤280 chars, one sentence describing why this mutation happened). Lands in the per-mem commit body between the mechanical subject line and the provenance trailers (`Tool:`, `Actor:`, `Client:`), where `memstead_changes_since` with `include_notes` reads it back. Omit for pure-housekeeping edits; when `[mutations].require_notes = true` in workspace config a missing note adds a `NOTE_MISSING` `WarningHint` to the response (the mutation still commits).", "type": [ "string", "null" ] }, "role": { "description": "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier`. Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary.", "type": [ "string", "null" ] } }, "required": [ "id", "new_title", "expected_hash" ], "type": "object"}memstead_retype
Section titled “memstead_retype”Modify an entity’s type in place. The id, file path, and every incoming edge stay; nothing is deleted or re-created, so history and provenance survive. The entity’s existing sections and metadata are validated against the TARGET type and every problem is reported together in one refusal: unknown sections (UNKNOWN_SECTION, with details.target_sections, details.target_catch_all and a details.proposed_section_map to retry with), MISSING_REQUIRED_SECTION, metadata refusals (UNKNOWN_METADATA_FIELD, INVALID_ENUM_VALUE, INVALID_FIELD_VALUE, REQUIRED_FIELD_UNSET), block-tier constraints (MISSING_REQUIRED_OUTGOING, CONSTRAINT_UNSATISFIED), and every incoming or outgoing edge, cross-mem included, that the target type’s relationship pins refuse (INVALID_REL_SHAPE, each edge listed with referrer and rel-type). The envelope’s code is UNKNOWN_SECTION whenever a section key misses (fix the map first), otherwise the shared code when every problem is of one class, and RETYPE_REFUSED when they mix; details.problems carries each with its own code. Rename section keys on the way with section_map, and let go of fields the target does not declare with drop_metadata; nothing is moved or dropped silently. Referrers in a lazy (unloaded) mem are probed through storage; a mem that cannot be probed refuses RETYPE_REFERRER_UNPROBEABLE. Requires expected_hash (mismatch emits HASH_MISMATCH with details.current); dry_run validates and returns prospective_hash without writing and needs no hash. The current type refuses RETYPE_NO_OP. Response: old_type, new_type, _hash (the next expected_hash), sections_renamed, edges_rechecked, write_id (an identity, never a change cursor), and checks_stale with staleness_note: the content hash moved, so check records and derivation baselines on this entity are stale. One commit lands with the retype provenance kind. Optional note — shared provenance contract, see memstead_create.
Hints: read_only = false, destructive = false, idempotent = false, open_world = false
Input schema:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { "drop_metadata": { "description": "Metadata keys to drop explicitly: fields the current type declares and the target does not (a spec's `level` on the way to a memo). Never inferred — an undeclared field not listed here refuses UNKNOWN_METADATA_FIELD, since dropping data unannounced is what the write gates prevent. A key the entity does not carry is a no-op.", "items": { "type": "string" }, "type": [ "array", "null" ] }, "dry_run": { "description": "Validate everything (sections, metadata, every incoming and outgoing edge, block-tier constraints) and return the prospective `_hash` without writing, committing, or changing the store. The optimistic lock is skipped on a dry run.", "type": [ "boolean", "null" ] }, "expected_hash": { "description": "Hash from memstead_entity (_hash). Required unless `dry_run` is true. Mismatch returns code HASH_MISMATCH with details.current carrying the current on-disk hash.", "type": [ "string", "null" ] }, "id": { "description": "Full entity ID (`mem--slug`) of the entity to retype", "type": "string" }, "identity": { "description": "WHO is acting: an opaque identity string of your choosing — an agent name, a session handle. Recorded immutably alongside the mutation (commit trailer / ledger); the author≠checker independence gate compares identities and nothing else. Caller-declared and unverified, but tamper-evident in append-only history. Omit to record the session default, or nothing — legal forever, never refused; identity-less records read unconfirmable at the gate. Over-length values refuse INVALID_IDENTITY (cap 128 chars).", "type": [ "string", "null" ] }, "note": { "description": "Agent-authored provenance note (≤280 chars, one sentence describing why this mutation happened). Lands in the per-mem commit body between the mechanical subject line and the provenance trailers (`Tool:`, `Actor:`, `Client:`), where `memstead_changes_since` with `include_notes` reads it back. Omit for pure-housekeeping edits; when `[mutations].require_notes = true` in workspace config a missing note adds a `NOTE_MISSING` `WarningHint` to the response (the mutation still commits).", "type": [ "string", "null" ] }, "role": { "description": "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier`. Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary.", "type": [ "string", "null" ] }, "section_map": { "additionalProperties": { "type": "string" }, "description": "Section key renames applied before validation, `{\"old_key\": \"new_key\"}`. A section the target type does not declare refuses UNKNOWN_SECTION with `details.target_sections`, `details.target_catch_all` and a `details.proposed_section_map` to retry with; sections are never moved silently.", "type": [ "object", "null" ] }, "target_type": { "description": "The target entity type, as declared by the mem's schema (`memstead_schema`). Unknown types refuse UNKNOWN_ENTITY_TYPE; the current type refuses RETYPE_NO_OP.", "type": "string" } }, "required": [ "id", "target_type" ], "type": "object"}memstead_schema
Section titled “memstead_schema”Read one schema. Default verbosity is “lite”: a structural skeleton — entity-type names with their section keys (required flags kept) and metadata-field shapes (enum values + default + pattern), last_resort on the fallback type, required_outgoing, relationship names with endpoint constraints, plus relationship_mode (strict|open), community.{resolution, seed}, used_by[], top-level origin (first-party for an engine built-in or workspace-authored schema; third-party otherwise), and top-level alias_target_rel_type (names the rel-type body wiki-links auto-emit; absent means unbacked wiki-links refuse with WIKILINK_WITHOUT_RELATION). The skeleton carries every legality flag needed to author a valid write. Pass verbosity: "full" for the prose layer — per-section write_rules, type-level writing_guidance, system_context, relationship prose, default_writing_guidance — before substantial authoring; scope it with types: ["<name>", …] to get the complete prose for exactly the types you will write (unserved types listed in types_omitted; an unknown name refuses UNKNOWN_ENTITY_TYPE naming the valid set). An unscoped full reply exceeding token_budget degrades visibly — per-type prose drops to the skeleton, _schema_mode: "reduced" + _hint steer to types — never silent truncation. Full and lite ship the heavy arrays under distinct keys (types/relationships vs types_summary/relationships_summary) — decode by key presence. A third-party schema is served structural-only regardless of verbosity — its prose-instruction fields never reach the agent as instructions. Pass exactly one of name — bare (“default”) or canonical pin — or mem; both or neither returns INVALID_INPUT. Call once per writable mem per session before create/update/relate (schema-discovery contract); cache — schema is workspace-stable. Returns ENTITY_NOT_FOUND for an unknown name, UNKNOWN_MEM for an unmounted mem (details.known_mems lists the roster).
Hints: read_only = true, destructive = false, idempotent = true, open_world = false
Input schema:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "mem": { "description": "Mem name as listed in memstead_overview's `## Mems` section. The engine resolves the mem's pinned `schema_ref` from the workspace's mount roster and proceeds identically to the `name`-driven path. Mutually exclusive with `name`. Returns `UNKNOWN_MEM` when the mem is not mounted.", "type": [ "string", "null" ] }, "name": { "description": "Schema name as listed in memstead_overview's `## Schemas` section (e.g. \"default\" or \"default@1.3.0\"). Schemas are workspace-globally unique by name; the workspace registry resolves a bare name to the pinned version. Mutually exclusive with `mem`.", "type": [ "string", "null" ] }, "token_budget": { "description": "Token budget for an UNSCOPED `verbosity: \"full\"` reply. When the whole-package full payload would exceed it, the reply degrades visibly instead of overflowing the response cap: per-type prose drops to the lite skeleton, `_schema_mode: \"reduced\"` is stamped, and `_hint` steers to per-type retrieval via `types`. Scoped (`types`) requests are never degraded. Omit to use the server's default budget.", "format": "uint", "minimum": 0, "type": [ "integer", "null" ] }, "types": { "description": "Scope the per-type payload to these entity-type names (as listed in the lite skeleton). With `verbosity: \"full\"` this is the way to drill before substantial authoring: the reply carries the full package-level context plus the complete prose for exactly the named types, with every unserved type listed in `types_omitted` — one under-budget reply instead of a whole-package spill. A name not in the schema refuses `UNKNOWN_ENTITY_TYPE` naming the valid types. Omit for the whole roster.", "items": { "type": "string" }, "type": [ "array", "null" ] }, "verbosity": { "description": "Verbosity of the schema body. `\"lite\"` (default, absent) returns a cheap cold-start skeleton: entity-type names with their section keys (and `required` markers) and metadata-field shapes (name, `required`, `enum`, `default`), relationship-type names with their `allowed_sources`/`allowed_targets`, `manual_authoring`, `acyclic`, and `per_edge_description` — plus the top-level `alias_target_rel_type` pointer — with the long-form prose dropped. The lite skeleton carries every flag needed to author a legal write. `\"full\"` returns the complete payload — every description, `when_to_use`, write-rule, and writing-guidance string; escalate to full for the human-readable guidance before substantial authoring. Heavy arrays ship under distinct keys per mode (`types`/`relationships` vs. `types_summary`/`relationships_summary`). Any value other than `\"full\"`/`\"lite\"` returns `INVALID_INPUT` naming the bad value.", "type": [ "string", "null" ] } }, "type": "object"}memstead_search
Section titled “memstead_search”Search entities by text and structural filters. Dual channel: markdown plus a typed envelope on structured_content, one stable shape: { _total, _returned, _offset, _total_tokens, hits[], facets, warnings }; each hit: score, score_breakdown, matched_terms, expansion, origin (trust class, per ENTITY not per mem), snippet. warnings is [] when nothing warned; score_breakdown and matched_terms are null on a metadata-only call (no query), expansion on a primary hit, snippet where no text matched. A page is bounded by token_budget (default 12000); overflow trims with SEARCH_RESULTS_TRUNCATED (kept/budget), _total stays full, page with offset. query.any ORs keyword variants, query.not excludes, query.phrase matches adjacency, query.field restricts to one field. expand_via (relationship types) walks the graph; reached hits carry expansion (via_direction, score decayed 0.5^depth); direction (out|in|both) narrows each hop. facets count by_type, by_mem, by_level, by_status, by_confidence, by_subsection, by_expansion. Sub-heading matches carry heading_path. stub: true|false filters stubs (with entity_type it flags STUB_FILTER_EXCLUDES_ALL). filters takes filterable: equality fields; per outcome: FILTER_TYPE_SCOPED (applied, type-scoped), FIELD_NOT_FILTERABLE (ignored, never emptied), UNKNOWN_FILTER_KEY (ignored), INVALID_ENUM_VALUE (matches nothing, details.allowed). related_to: proximity-ranked neighbourhood, bounded by NEIGHBOURHOOD_CAPPED. range_filters takes filterable: range fields (min_<field>/max_<field>/<field>_before/<field>_after), same contract: RANGE_FILTER_KEY_MALFORMED, RANGE_FILTER_TYPE_SCOPED, UNKNOWN_RANGE_FILTER_FIELD, FIELD_NOT_RANGE_FILTERABLE. An unknown mem refuses UNKNOWN_MEM; an empty result is a valid mem with none. A failing mem index surfaces SEARCH_MEM_INDEX_UNAVAILABLE (details.mem/details.reason). Omit query for a pure metadata filter.
Hints: read_only = true, destructive = false, idempotent = true, open_world = false
Input schema:
{ "$defs": { "Query": { "description": "Flat query shape for full-text search. Four optional fields, all\ncombined with implicit AND across fields.\n\nWithin `any`: at least one term must match (OR semantics). Entities\nmatching more terms rank higher automatically — no explicit `and`.\nWithin `not`: none of the listed terms may appear. `phrase` requires\nexact adjacency (case- and diacritic-folded). `field` narrows the match\nregion for all three to a single indexed field; `None` = match anywhere\nindexed.\n\nEmpty/unset everywhere ⇒ no text predicate; `search` behaves as a\nmetadata-only filter (subsumes the former `list` semantics).\n\nNo stemming, wildcards, or regex — the caller expands morphology and\nsynonyms by enumerating variants in `any`.", "properties": { "any": { "description": "Terms where at least one must match (OR semantics).", "items": { "type": "string" }, "type": "array" }, "field": { "description": "Restrict `any` / `not` / `phrase` to a single field (title or section\nkey). `None` = match anywhere indexed.", "type": [ "string", "null" ] }, "not": { "description": "Terms that must not match (exclusion).", "items": { "type": "string" }, "type": "array" }, "phrase": { "description": "Exact phrase that must appear (case- and diacritic-folded).", "type": [ "string", "null" ] } }, "type": "object" }, "TraversalDirection": { "description": "Traversal direction relative to the seed, applied at EVERY hop —\ndepth > 1 is a pure transitive closure in the chosen direction,\nnever a mixed walk (an entity reachable only by alternating\ndirections is not in an `out` or `in` result at any depth; that\nper-hop property is what makes a fall-through analysis correct).\n\n`in`/`out` describe the edge relative to the seed and match the\nStore's own vocabulary — domain words (ancestors/upstream) invert\nper schema, so the engine does not use them.", "oneOf": [ { "const": "out", "description": "Follow edges pointing away from the seed (seed → target).", "type": "string" }, { "const": "in", "description": "Follow edges pointing at the seed (source → seed).", "type": "string" }, { "const": "both", "description": "Follow both — the historical undirected walk, and the default:\na query that omits the selector returns exactly what it always\nreturned.", "type": "string" } ] } }, "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "depth": { "description": "Max hops from related_to (default: 1, ignored without related_to)", "format": "uint", "minimum": 0, "type": [ "integer", "null" ] }, "direction": { "anyOf": [ { "$ref": "#/$defs/TraversalDirection" }, { "type": "null" } ], "description": "Traversal direction for `related_to` and `expand_via`, applied at EVERY hop: \"out\" follows edges pointing away from the seed (what does this rest on), \"in\" follows edges pointing at it (what rests on this), \"both\" (default) is the historical undirected walk. Depth > 1 is a pure transitive closure in the chosen direction — never a mixed walk. Expanded hits report the reaching edge's direction as `expansion.via_direction`." }, "edge_type": { "description": "Only entities having this edge type (e.g. IMPLEMENTS, USES)", "type": [ "string", "null" ] }, "entity_type": { "description": "Only entities of this type (e.g. \"spec\", \"memo\")", "type": [ "string", "null" ] }, "expand_depth": { "description": "Max hops to traverse via `expand_via` (default: 1).", "format": "uint", "minimum": 0, "type": [ "integer", "null" ] }, "expand_via": { "description": "Relationship types to follow from primary hits to pull in graph-proximal neighbours (e.g. [\"REALIZES\", \"REFERENCES\"]). Expanded hits carry `expansion: { of, via_edge, depth }` and a decayed score (0.5^depth). `by_expansion` facet shows the primary/expanded composition.", "items": { "type": "string" }, "type": [ "array", "null" ] }, "filters": { "additionalProperties": { "type": "string" }, "description": "Equality filters on schema-declared filterable fields, keyed by field name (e.g. `{\"level\": \"M0\", \"status\": \"active\", \"tags\": \"auth\", \"scope\": \"subsystem\"}`). Every field with `filterable: equality` in the type's schema is reachable here. One typed warning per outcome, branch on `code`: `FILTER_TYPE_SCOPED` (a *filterable* key declared only on other types — applied with strict type-narrowing), `FIELD_NOT_FILTERABLE` (declared but not filterable on any reachable type — ignored in both the scoped and unscoped case, result unfiltered not emptied), `UNKNOWN_FILTER_KEY` (no schema declares it — ignored), `INVALID_ENUM_VALUE` (a value outside the field's `enum_values` — the filter applies but matches nothing, so a 0-hit result isn't a true no-match; `details.allowed` lists the values). The per-field `level`/`status`/`confidence` parameters are retired — agents declare any filterable field uniformly through this map. Use `entity_type` (typed parameter) and `edge_type` (typed parameter) for the engine's first-class graph axes, not for metadata filters.", "type": [ "object", "null" ] }, "limit": { "description": "Max results to return (default: all, max: 200)", "format": "uint", "minimum": 0, "type": [ "integer", "null" ] }, "mem": { "description": "Only entities in this mem", "type": [ "string", "null" ] }, "offset": { "description": "Skip first N results for pagination. Use with limit.", "format": "uint", "minimum": 0, "type": [ "integer", "null" ] }, "query": { "anyOf": [ { "$ref": "#/$defs/Query" }, { "type": "null" } ], "description": "Structured flat query. Fields: `any: [terms]` (OR, ranks entities matching more terms higher — no explicit AND needed), `not: [terms]` (exclusion), `phrase: \"exact adjacency\"`, `field: \"title\"|section-key` (narrow all three). Omit (or pass `{}`) to use search as a pure structural/metadata filter — hits come back in title-ascending order. No stemming: include morphological variants explicitly (run, running, runs)." }, "range_filters": { "additionalProperties": { "type": "string" }, "description": "Range filters on schema-declared range-filterable fields, keyed by `min_<field>` / `max_<field>` (numeric) or `<field>_before` / `<field>_after` (date). Example: `{\"created_date_after\": \"2026-01-01\", \"max_score\": \"5\"}`. Every field with `filterable: range` in the type's schema is reachable here. Composable with `filters` (equality). One typed warning per outcome, branch on `code`: `RANGE_FILTER_KEY_MALFORMED` (key lacks a `min_`/`max_`/`*_before`/`*_after` shape), `RANGE_FILTER_TYPE_SCOPED` (a *range-filterable* field declared only on other types — applied with strict type-narrowing), `UNKNOWN_RANGE_FILTER_FIELD` (derived field name not declared on any reachable schema — ignored), `FIELD_NOT_RANGE_FILTERABLE` (field declared but not `filterable: range` on any reachable type — ignored in both the scoped and unscoped case, result unfiltered not emptied).", "type": [ "object", "null" ] }, "related_to": { "description": "Full entity ID — only return entities within depth hops (BFS, undirected). Results are ranked by proximity: nearer hops first, then a typed (dependency) link to the anchor before a co-mention at the same hop. A neighbourhood larger than the cap is bounded to its nearest members with a `NEIGHBOURHOOD_CAPPED` warning (`kept`/`total`).", "type": [ "string", "null" ] }, "stub": { "description": "Filter by stub status. Omit (default) = both stubs and real entities. `true` = stubs only. `false` = real entities only.", "type": [ "boolean", "null" ] }, "token_budget": { "description": "Token budget bounding the returned hit payload (default: 12000). A page whose hits exceed it is greedily trimmed to the highest-ranked hits that fit (at least one always returns) and a `SEARCH_RESULTS_TRUNCATED` warning carries `kept`/`budget`; `_total` still reflects the full match count, so page the remainder with `offset` or narrow the query. Raise it to pull more hits in one call when the agent can afford the tokens. Independent of `limit`, which caps the count before the budget trims by size.", "format": "uint", "minimum": 0, "type": [ "integer", "null" ] } }, "type": "object"}memstead_update
Section titled “memstead_update”Modify an existing entity. Pre-fetch the target mem’s schema (memstead_schema). Pass the entity’s _hash as expected_hash when the update CHANGES CONTENT (anchors-only may omit it; a stored (artifact, grain, class, span) row is replaced, a new span added: anchors_changed) — mismatch emits HASH_MISMATCH (details.current = live hash). Warnings INLINE_WIKI_LINK_AUTO_STUBBED, CROSS_SCHEMA_LINK_UNDECLARED, MISSING_REQUIRED_OUTGOING mirror memstead_create’s. Section modes (one per key): sections (replace), append_sections, patch_sections (find-and-replace; every occurrence via all: true; errors on missing old or empty section), sections_unset (removes heading+body; no-op if absent; refused for REQUIRED sections). Schema-bound errors (UNKNOWN_SECTION, UNKNOWN_METADATA_FIELD, INVALID_ENUM_VALUE, INVALID_FIELD_VALUE, SECTION_CONTENT_INVALID, …) carry recovery payloads — fix from details (see server instructions). metadata sets frontmatter; metadata_unset removes it (no-ops on absent or section keys). Setting and unsetting one key is a hard error. Read-only on SET (READ_ONLY_FIELD): mem/id/type (memstead_rename for title) and engine-stamped created_date/last_modified (also unset-refused). Unset MAY name the reserved triple (sanctioned repair; no-op when healthy). Stubs cannot be updated — memstead_create as real first. Optional note (≤280 chars), see instructions. No-op short-circuit: post-state bytes-identical to disk returns UPDATE_NOOP, empty write_id, unchanged _hash — expected_hash stays stable. dry_run: true validates then previews OR recovers from a stale hash: it bypasses ONLY the expected_hash check (returns current _hash + prospective_hash); section/field validation still refuses with the same typed envelope — never an invalid update reported clean. Reuse _hash as expected_hash, never prospective_hash. A body-link removal orphaning its stub GC’s it into orphan_stubs_removed. Real writes carry write_id.
Hints: read_only = false, destructive = false, idempotent = false, open_world = false
Input schema:
{ "$defs": { "AnchorInputParam": { "additionalProperties": false, "description": "One `anchors[]` element on `memstead_create` / `memstead_update` — a\nprovenance record tying the entity to a source artifact. Permissive by\ndesign: every field is optional / string-typed so a malformed element\n(unknown class or grain, missing artifact, hash on a non-hash class,\ngrain the medium's namespace cannot express) refuses the whole mutation\nwith a typed `INVALID_ANCHOR` envelope carrying recovery `details` —\nrather than an opaque schema-deserialisation error. Converts to the\nengine's `AnchorInput` which validates it. Not folded into `_hash`.", "properties": { "artifact": { "default": null, "description": "Artifact reference in the medium's own namespace — a repo-relative path, `path@commit`, URL, or entity id, interpreted per `grain`. Required; a missing/empty value refuses INVALID_ANCHOR.", "type": [ "string", "null" ] }, "at_version": { "anyOf": [ { "$ref": "#/$defs/AnchorVersionParam" }, { "type": "null" } ], "description": "Medium-typed pinned version this anchor was recorded against: `{ kind: \"commit\"|\"snapshot\"|\"etag\", value: \"<token>\" }`. Omit for a plain-path medium with no retrievable version." }, "binding": { "default": null, "description": "`hash(D)` of the binding that produced this anchor, when a binding produced it. Omit for a manually-authored anchor.", "type": [ "string", "null" ] }, "class": { "default": null, "description": "Provenance class — the entity's epistemic standing toward the artifact: `anchored` | `derived` | `authored` | `informed-by`. `anchored`/`derived` carry hash semantics (a `hash` is permitted and participates in drift adjudication); `authored`/`informed-by` do not (supplying `hash` refuses INVALID_ANCHOR). An unknown value refuses INVALID_ANCHOR.", "type": [ "string", "null" ] }, "content": { "default": null, "description": "The observed artifact CONTENT (UTF-8 text), for the engine to compute `hash` from through its preparation registry — the write-time observation for a grain the engine cannot observe itself: a `url` anchor (the engine never fetches; what you read is canonicalized exactly as a path grain's bytes are). Also accepted for `span`/`file`. Mutually exclusive with `hash` (both refuses INVALID_ANCHOR); refused on `authored`/`informed-by`, and on the `entity`/`tree` grains, whose prepared form is never computed from supplied bytes.", "type": [ "string", "null" ] }, "derived_from": { "default": null, "description": "For a `derived` class: the input artifact refs the entity was derived from. Empty/omitted for every other class.", "items": { "type": "string" }, "type": [ "array", "null" ] }, "grain": { "default": null, "description": "Granularity of the artifact reference: `span` | `file` | `tree` | `url` | `entity`. `span`/`file`/`tree` need a path-shaped medium namespace and `entity` the entity namespace, or the mutation refuses INVALID_ANCHOR; `url` is admitted beside every medium (a URL never enters a path namespace). A path-shaped grain whose artifact is a URL refuses INVALID_ANCHOR naming that rule: use `grain: url`. An unknown value refuses INVALID_ANCHOR.", "type": [ "string", "null" ] }, "hash": { "default": null, "description": "Content hash over the PREPARED artifact form (never raw bytes). Permitted only on hash-bearing classes (`anchored`/`derived`); supplying it on `authored`/`informed-by` refuses INVALID_ANCHOR.", "type": [ "string", "null" ] }, "hash_stability": { "default": null, "description": "Medium's declared hash stability: `stable` | `unstable` (defaults per grain: `url` to `unstable`, every other grain to `stable`). An unstable-source hash break resolves `recheck`, not `drifted`.", "type": [ "string", "null" ] }, "source": { "default": null, "description": "NAME of the source (as declared in the producing binding's `sources[]`) that produced this anchor — lets a discovery run be measured per entry point. Name the source you are working from whenever the binding declares more than one. Present-but-empty refuses INVALID_ANCHOR; a name the (resolvable) producing binding does not declare refuses with the declared names in `details.declared`. Omit for a manually-authored anchor.", "type": [ "string", "null" ] }, "span": { "default": null, "description": "The verbatim words the entity quotes from the artifact (`url`/`span`/`file` grains, hash-bearing classes). With `content`, the write refuses INVALID_ANCHOR unless the span occurs in it; the row then records the span, a hash over its canonical form and the document's prepared hash. Without `content` the row is accepted with the span unverified. From then on the row is adjudicated on the span's PRESENCE in observed text (`verify-anchors --observations` with `content` rows): `resolves` while the words stand whatever else on the page changed, `span_absent` once they are gone, never `drifted`. Compared in the canonical span form: NFC, whitespace runs to one space, soft hyphens and line-end hyphens removed, typographic quotes, dashes and ligatures to ASCII; digits, decimal marks, units, case and word order exact. The span is part of the row's identity beside `artifact`, `grain` and `class`: several spans on one document are several rows. Refused beside `hash` (the engine computes the hashes), on `entity`/`tree`, and when empty.", "type": [ "string", "null" ] } }, "type": "object" }, "AnchorUnsetParam": { "additionalProperties": false, "description": "One `anchors_unset[]` entry on `memstead_update` — an explicit anchor-\nremoval selector. Permissive like [`AnchorInputParam`]: a malformed\nselector refuses the whole mutation with a typed `INVALID_ANCHOR`\nenvelope rather than an opaque schema-deserialisation error.", "properties": { "artifact": { "default": null, "description": "Artifact reference whose anchors to remove, exactly as stored. Required; a missing/empty value refuses INVALID_ANCHOR. Bare (no grain/class) removes every anchor on the artifact.", "type": [ "string", "null" ] }, "class": { "default": null, "description": "Optional narrowing: only remove anchors of this provenance class (`anchored` | `derived` | `authored` | `informed-by`). An unknown value refuses INVALID_ANCHOR.", "type": [ "string", "null" ] }, "grain": { "default": null, "description": "Optional narrowing: only remove anchors of this grain (`span` | `file` | `tree` | `url` | `entity`). An unknown value refuses INVALID_ANCHOR.", "type": [ "string", "null" ] }, "span": { "default": null, "description": "Optional narrowing: only remove the row quoting this span (compared in the canonical span form). Without it, a selector removes span rows and span-less rows alike.", "type": [ "string", "null" ] } }, "type": "object" }, "AnchorVersionParam": { "additionalProperties": false, "description": "The medium-typed pinned version sub-object of an [`AnchorInputParam`].", "properties": { "kind": { "description": "Version kind: `commit` (git / path+commit) | `snapshot` (graph) | `etag` (web).", "type": "string" }, "value": { "description": "The version token — commit id, graph snapshot token, or web ETag.", "type": "string" } }, "required": [ "kind", "value" ], "type": "object" }, "PatchInput": { "additionalProperties": false, "description": "Find-and-replace input.", "properties": { "all": { "description": "Replace every occurrence of `old` when true; replace only the first when false or omitted. Literal match, case-sensitive.", "type": [ "boolean", "null" ] }, "new": { "description": "Replacement (empty string = delete)", "type": "string" }, "old": { "description": "Exact substring to find in current content", "type": "string" } }, "required": [ "old", "new" ], "type": "object" }, "PatchesInput": { "anyOf": [ { "$ref": "#/$defs/PatchInput" }, { "items": { "$ref": "#/$defs/PatchInput" }, "type": "array" } ], "description": "One patch or a list of patches for a single section — the wire\naccepts both shapes (`{...}` and `[{...}, ...]`); a single object is\nthe historical form and stays valid unchanged." }, "RelationInput": { "additionalProperties": false, "description": "A relationship input for create/batch tools.", "properties": { "description": { "default": null, "description": "Optional per-edge description text. Validated against the rel-type's `per_edge_description` posture in the pinned schema: `forbidden` (default) rejects a non-empty description with `DESCRIPTION_NOT_PERMITTED`; `required` rejects its absence with `MISSING_REQUIRED_DESCRIPTION`; `optional` accepts both. Empty / whitespace-only strings normalise to absent before validation. Surfaces on `memstead_entity` and round-trips through the `## Relationships` markdown via the canonical em-dash delimiter (` — `).", "type": [ "string", "null" ] }, "rel_type": { "description": "Relationship type. Canonical form is UPPER_SNAKE_CASE (USES, PART_OF, DEPENDS_ON) and is what the engine stores; case-insensitive inputs (`uses`, `Part_Of`) are accepted and echoed back in the response as their canonical form. The JSON Schema `pattern` advertises `^[A-Za-z][A-Za-z_]*$` for client-side validators; the engine enforces the same character set independently — characters outside it return `INVALID_REL_TYPE` at the engine boundary regardless of whether the client pre-filters.", "pattern": "^[A-Za-z][A-Za-z_]*$", "type": "string" }, "target": { "description": "Full target entity ID", "type": "string" } }, "required": [ "target", "rel_type" ], "type": "object" }, "RelationUnsetInput": { "additionalProperties": false, "description": "One `relations_unset` entry — `{ rel_type, target }`.", "properties": { "rel_type": { "description": "Relationship type of the edge to remove (canonical UPPER_SNAKE_CASE; case-insensitive input accepted)", "type": "string" }, "target": { "description": "Full target entity ID of the edge to remove", "type": "string" } }, "required": [ "rel_type", "target" ], "type": "object" } }, "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { "anchors": { "description": "Optional provenance anchors to attach to this entity — durable records tying it to the source artifacts it describes. Anchors MERGE into the entity's existing set: an incoming anchor replaces the existing anchor with the same `(artifact, grain, class, span)` identity and appends otherwise — the span is part of the identity, so a row quoting a NEW span on a stored `(artifact, grain, class)` is a second row beside the old one, never its replacement (to move a span: `anchors_unset` the old span and send the new row in the same call) — and writing anchors never removes an anchor this call did not name in `anchors_unset` (an empty or omitted list leaves the stored set untouched; incremental anchoring works). Written into the mem-branch anchors sidecar in the SAME commit as the update (atomic). An update carrying only `anchors` (no section/metadata change) still commits the sidecar. A malformed element refuses the whole update with `INVALID_ANCHOR` and nothing is written. A payload naming one `(artifact, grain, class, span)` identity TWICE refuses (`INVALID_ANCHOR`): that identity is one row, so the repeats would collapse to the last one and an anchor you sent would vanish unannounced. A re-pin that omits `hash` KEEPS the stored baseline rather than dropping it; supply `hash` to replace it, or `anchors_unset` the row and write it fresh to clear it. A `span` anchor's locator must be usable (`#L<start>-L<end>`, `#<unit-key>`, or no locator for the whole file); a locator that addresses nothing refuses, and a span the engine could not check (no `content` supplied) is accepted and recorded as unverified. Anchors do NOT participate in `_hash`.", "items": { "$ref": "#/$defs/AnchorInputParam" }, "type": [ "array", "null" ] }, "anchors_unset": { "description": "Explicit anchor removals, applied BEFORE the `anchors` merge in the same mutation (mirroring `metadata_unset` / `relations_unset`) — removal is explicit, never a side effect of writing. Each entry names an `artifact` and may narrow by `grain`, `class` and/or `span` (the one row quoting that span); a bare artifact removes every anchor on it, span rows included. Unsetting an anchor that does not exist is a no-op, not an error. Full-replace stays expressible: unset the artifact(s) and write the new set in one call. A malformed selector (missing artifact, unknown grain/class) refuses the whole update with `INVALID_ANCHOR`.", "items": { "$ref": "#/$defs/AnchorUnsetParam" }, "type": [ "array", "null" ] }, "append_sections": { "additionalProperties": { "type": "string" }, "description": "Section fields to append to: { \"specifies\": \"extra content\" }", "type": [ "object", "null" ] }, "declare_relations": { "description": "Atomic batched relation declarations applied before the section/metadata changes land. Each `{ target, rel_type }` is validated like a `memstead_relate` call (schema-shape, cross-mem policy, target-id grammar) and appended to the entity's relations; absent Write-mem targets are auto-stubbed identically to the relate path. The strict wiki-link/relation validator then runs against the post-mutation state with the freshly-declared relations in place — so adding a `[[target]]` body wiki-link + declaring the backing `REFERENCES` relation can land in a single `memstead_update` call (without `declare_relations`, the post-migration strict validator would refuse the body link). Each successful entry is echoed in `relations_declared` on the response with `target_was_stubbed` flagging whether the target was absent at call time. Omit for mutations that don't introduce new relations.", "items": { "$ref": "#/$defs/RelationInput" }, "type": [ "array", "null" ] }, "dry_run": { "description": "Validate and preview what would change without executing. On a valid update the response carries the unchanged on-disk hash as `_hash` plus the post-write `prospective_hash` — pass `_hash` as `expected_hash` on the follow-up real call. `dry_run` deliberately bypasses ONLY the `expected_hash` check (the returned `_hash` is the current on-disk hash, safe to reuse on the real follow-up), making it the designated recovery path for stale hashes. It does NOT relax the rest of validation: an update that a real call would refuse on section/field grounds (`UNKNOWN_SECTION`, `UNKNOWN_METADATA_FIELD`, `INVALID_ENUM_VALUE`, `INVALID_FIELD_VALUE`, `REQUIRED_FIELD_UNSET`, `PATCH_OLD_NOT_FOUND`, …) refuses under dry_run with the same typed envelope and the same recovery `details.*` — that refusal is the pre-flight signal, not a clean preview. So dry_run and a real write agree on validity (modulo the intentionally-skipped hash check).", "type": [ "boolean", "null" ] }, "expected_hash": { "default": null, "description": "Hash from memstead_entity response (_hash field). Required for any update that changes content (sections, metadata, relations) — read the entity first. OMIT it for an anchors-only update (`anchors` / `anchors_unset` and nothing else): the anchors sidecar is outside `_hash` by design, so the token would compare a value the write cannot move, and requiring it taxed exactly the backfill flows anchors exist for. An update that changes content without it refuses `EXPECTED_HASH_REQUIRED`. Mismatch returns code HASH_MISMATCH with details.current carrying the current on-disk hash; pass dry_run=true to bypass the check as a recovery path.", "type": [ "string", "null" ] }, "id": { "description": "Full entity ID to update", "type": "string" }, "identity": { "description": "WHO is acting: an opaque identity string of your choosing — an agent name, a session handle. Recorded immutably alongside the mutation (commit trailer / ledger); the author≠checker independence gate compares identities and nothing else. Caller-declared and unverified, but tamper-evident in append-only history. Omit to record the session default, or nothing — legal forever, never refused; identity-less records read unconfirmable at the gate. Over-length values refuse INVALID_IDENTITY (cap 128 chars).", "type": [ "string", "null" ] }, "metadata": { "additionalProperties": { "type": "string" }, "description": "Metadata fields to set: { \"level\": \"M1\" }", "type": [ "object", "null" ] }, "metadata_unset": { "description": "Metadata keys to remove. Silent no-op if absent. Errors on the engine-stamped timestamp fields (created_date / last_modified) and on schema-required fields. The reserved identity triple (mem / id / type) is asymmetric by design: SET refuses (READ_ONLY_FIELD, here and on create; a type change is `memstead_retype`, which re-validates the entity against the target type) but UNSET is allowed — the sanctioned repair for an entity that acquired a smuggled reserved key before the write gates closed. Unsetting `type` never leaves the entity typeless (the engine re-seeds the authoritative discriminator; on a healthy entity it is a no-op). Cannot overlap with `metadata` keys — pass one or the other per key.", "items": { "type": "string" }, "type": [ "array", "null" ] }, "note": { "description": "Agent-authored provenance note (≤280 chars, one sentence describing why this mutation happened). Lands in the per-mem commit body between the mechanical subject line and the provenance trailers (`Tool:`, `Actor:`, `Client:`), where `memstead_changes_since` with `include_notes` reads it back. Omit for pure-housekeeping edits; when `[mutations].require_notes = true` in workspace config a missing note adds a `NOTE_MISSING` `WarningHint` to the response (the mutation still commits).", "type": [ "string", "null" ] }, "patch_sections": { "additionalProperties": { "$ref": "#/$defs/PatchesInput" }, "description": "Section fields to patch (find-and-replace): { \"specifies\": { \"old\": \"...\", \"new\": \"...\" } } — or a LIST of patches per section, applied in order against the evolving body: { \"specifies\": [{...}, {...}] }. Batched edits to one section land in one call.", "type": [ "object", "null" ] }, "relations_unset": { "description": "Repair-shaped relation removals `[{ rel_type, target }]`, applied atomically within this update. Accepted only when the entity currently FAILS the conformance check (see memstead_health include=conformance) — on a conformant entity the call refuses with REPAIR_NOT_NEEDED and the entity is unmodified; use memstead_relate(remove=true) for everyday edge detachment. Absent pairs are silent no-ops (symmetric with metadata_unset). The strict-write post-condition is unchanged: the post-repair entity must validate or the whole update refuses with the relevant write-time code. During a schema migration every not-yet-repaired entity is non-conformant against the target, so this param works on exactly those entities with no mode flag.", "items": { "$ref": "#/$defs/RelationUnsetInput" }, "type": [ "array", "null" ] }, "role": { "description": "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier`. Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary.", "type": [ "string", "null" ] }, "sections": { "additionalProperties": { "type": "string" }, "description": "Section fields to set (replaces content): { \"identity\": \"new content\" }", "type": [ "object", "null" ] }, "sections_unset": { "description": "Section keys to REMOVE from the entity — heading and body both: [\"notes\"]. The close gesture for a declared-but-empty heading with nothing to receive, and the repair for a legacy undeclared heading. Silent no-op on an absent key (symmetric with metadata_unset). Refuses for a schema-REQUIRED section (MISSING_REQUIRED_SECTION — fill it instead), for `relationships` (SECTION_NOT_UPDATABLE), and for a key also named in sections / append_sections / patch_sections (CONFLICTING_SECTION_MODES).", "items": { "type": "string" }, "type": [ "array", "null" ] } }, "required": [ "id" ], "type": "object"}