// Package mcp implements pad's Model Context Protocol server. // // Layered build (PLAN-942): // - TASK-944 (this file + server.go) — handshake skeleton. // - TASK-945 — cmdhelp-derived tool registry + shell-out dispatch. // - TASK-946 — MCP resources (items, dashboard, collections). // - TASK-947 — MCP prompts (planning / ideation / retro). // - TASK-948 — `pad mcp install ` client config writer. // - TASK-963 — cmdhelp_version handshake metadata + pad://_meta/version. package mcp // ServerName is the canonical name pad's MCP server advertises in the // initialize handshake's serverInfo.name field. Stable across versions — // MCP clients (Claude Desktop, Cursor, Windsurf) display it verbatim, // so changing it would break user-visible installations. const ServerName = "pad-mcp" // FallbackVersion populates serverInfo.version when NewServer is // constructed without an explicit Options.Version. Production callers // (the cobra `pad mcp serve` command) always pass pad's runtime // fullVersion(); this fallback covers tests, embedders, and `dev` // builds where the version string is empty. const FallbackVersion = "0.0.0-dev" // CmdhelpVersion is the cmdhelp CLI-help-tree stability contract this // MCP server advertises. cmdhelp is the source of truth for individual // CLI command schemas (args, flags, types) consumed at MCP dispatch // time by BuildCLIArgs. Bump the major when those CLI-side schemas // change incompatibly: // // - "0.1" — initial cmdhelp surface from PLAN-942. // // This is independent of ToolSurfaceVersion below — cmdhelp owns the // CLI's help-tree contract; ToolSurfaceVersion owns the MCP tool // catalog's contract. Two contracts, two version constants. // // Discovery surfaces (paths into the JSON-RPC envelope): // // - result.capabilities.experimental.padCmdhelp.version (handshake). // - pad://_meta/version resource (queryable JSON document). const CmdhelpVersion = "0.1" // ToolSurfaceVersion is the MCP tool catalog stability contract this // server advertises. External agents (Claude Desktop, Cursor, ChatGPT // connectors, future Pad Cloud remote MCP) pin against it so a future // tool rename, action enum change, or parameter reshape doesn't // silently break consumers. Bump the major when the catalog shape // changes incompatibly: // // - "0.1" — historical. cmdhelp-derived ~85 flat verb tools // (PLAN-942). Lived from PLAN-942 through TASK-980 of PLAN-969's // 3-stage rollout; never bumped during rollout because the // user-visible surface was a transitional mix of v0.1 walker // output + the partial v0.2 catalog. // // - "0.2" — historical. Hand-curated resource × action catalog // (PLAN-969, TASK-981). The cmdhelp leaf walker retired; tools/list // advertises only the catalog (~7 tools + pad_set_workspace). // // - "0.3" — historical. PLAN-1377 / TASK-1380: // // - pad_meta gains an action: bootstrap that returns the // AgentBootstrap blob (and pad_meta.Schema.Workspace flipped // to true so the workspace param is available to that action). // // - pad_set_workspace's response shape extends from // {workspace, status} to {workspace, status, bootstrap?} — // the embedded blob lets one call hand the agent full session // context. Purely additive; older clients that ignore unknown // keys keep working. // // - pad://workspace/{ws}/bootstrap resource added. // // - "0.5" — historical. PLAN-1560 / TASK-1563: adds `pad_library` to // the catalog as the ninth resource × action tool. Three actions — // list / get / activate — surface the global convention + playbook // library (previously CLI-only) to MCP callers. Pure addition; no // existing tool/action/param/bootstrap shapes changed. Backwards- // compatible for any v0.4 consumer that doesn't enumerate the new // tool. // // - "0.6" — historical. PLAN-1593 / TASK-1596: adds `backlinks` action // to `pad_item` so MCP callers can answer "what mentions X?" // without scanning the full content corpus. Adds `offset` to the // param vocabulary, extends `limit` to cover the backlinks // pagination. Pure addition; existing pad_item actions unchanged. // Backwards-compatible for v0.5 consumers that don't enumerate // the new action. // // - "0.7" — historical. Artifact export/import (Phase 5): adds two // actions to `pad_item` mirroring the CLI `pad item export` / // `pad item import`. `export` (read-only) takes `ref` and returns // the portable artifact TEXT (YAML frontmatter + Markdown body) — // it forces the CLI's stdout sink (`-o -`) so the bytes come back // as the tool result rather than being written to a file the MCP // host can't see. `import` (mutating, not destructive — creates a // draft like create) takes a new `artifact` param (the full // artifact text) and returns {ref, slug, warnings}; because the // ExecDispatcher doesn't pipe stdin, it spills the artifact to a // temp file and dispatches `item import `. Both cover // playbooks AND conventions (the server gates by collection). Adds // the `artifact` param to the vocabulary. Pure addition; existing // pad_item actions unchanged. Backwards-compatible for v0.6 // consumers that don't enumerate the new actions. // // - "0.21" — BUG-2608: bounds `pad_item.action=history`, which // was unbounded on every surface. Extends the `limit` param's // vocabulary to cover history (default 50, max 300 — the same pair // list and backlinks already use, so an agent has no third set of // numbers to remember) and applies it in the CATALOG action rather // than either dispatcher, so it lands on BOTH transports: the HTTP // path reads it off the input, the exec path receives it as the // CLI's new --limit through BuildCLIArgs. // // ADDITIVE param bump on the v0.6/v0.18/v0.19 pattern: `limit` // already existed, no tool, action enum, or param SHAPE changed, // and a v0.20 consumer that sends no limit keeps working — it now // receives the newest 50 versions instead of all of them, which is // the point of the fix rather than a break in it. // // The window is the NEWEST N and there is deliberately no `offset`. // Versions are stored as REVERSE patches, so reconstructing any // version means walking back from the item's current content // through everything newer: a newest-end window is the cheap prefix // of that walk, while an older window would still pay for // everything above it. Offering an offset would advertise a // pagination whose later pages cost the same as no bound at all. // // Behaviour change worth stating even though the shape is stable: // summary mode now asks the SERVER to skip patch resolution // (`?summary=true`) instead of resolving every body and discarding // it in the dispatcher. Same result payload, minus a full chain // walk per call. // // - "0.20" — BUG-2302: every advertised tool now carries an // EXPLICIT annotation block derived from readOnlyActions (the same // single source the tool-surface serializer uses) instead of // inheriting mcp-go's NewTool defaults, which stamped // ReadOnlyHint:false + DestructiveHint:true + OpenWorldHint:true on // every tool — including pure reads like pad_search and pad_project, // training users to click through host confirmation prompts. // Policy: fully-read-only tools (every action in readOnlyActions) // advertise ReadOnlyHint:true, DestructiveHint:false, // IdempotentHint:true; tools whose writes are all purely ADDITIVE // (additiveWriteActions — pad_workspace's invite/create/claim/ // restore, pad_library's activate) advertise ReadOnlyHint:false + // DestructiveHint:false; tools with any overwrite/delete-capable // action (pad_item, pad_collection, pad_role) keep the // conservative ReadOnlyHint:false + DestructiveHint:true — // unchanged on the wire from the old defaults for exactly those; // OpenWorldHint:false everywhere (pad tools are closed-world). // pad_set_workspace gets its own block (write, non-destructive, // idempotent, closed-world). Also adds the missing `history` entry // to readOnlyActions — pad_item.history was documented read-only // since v0.14 but reported read_only:false on the tool-surface // descriptor and would have kept `pad_item` annotations honest but // the descriptor wrong. BEHAVIOR bump (v0.9/v0.16 precedent): no // tool names, action enums, or parameter shapes changed — the // advertised annotation metadata and one read_only flag did. // // Also in 0.20 (BUG-2305, same window — one bump, appended entry): // `pad_item.list` is summary-shaped on the REMOTE HTTP transport // too. The exec path always projected through cli.ToItemSummaries // (the CLI default TASK-2000 relied on); the HTTP path forwarded // raw handler responses, so a bare list returned up to 50 FULL // content bodies. A hand-written dispatchItemList (routeTable // can't transform responses; the server has no projection param) // now applies the same projection, and a new declared `full` // boolean on pad_item is the discoverable opt-in for complete // bodies on BOTH transports (stdio forwards it as the CLI's // --full). Additive param + a shape fix restoring transport // symmetry (v0.16/v0.17 precedent). // // Post-0.20, deliberately NO bump (BUG-2304): `item backlinks`, // `item history`, and `project report` gained HTTP transport // routes — they were advertised in the catalog on both transports // but answered "not yet implemented over HTTP transport" on the // remote /mcp path. No tool names, action enums, or parameter // shapes changed; advertised actions now behave as already // documented, which is a defect fix inside the existing contract, // not a contract change. `history` defaults to the CLI's // itemVersionSummary projection over HTTP too (full=true opts // into complete rows, mirroring stdio's --full). A catalog↔route // parity test (dispatch_http_parity_test.go) now fails on any // future advertised-but-unrouted action. // // - "0.19" — BUG-2078: adds a `clear_parent` boolean to // `pad_item`, the canonical and DISCOVERABLE way to detach an item from // its parent. Additive param bump, same grounds as v0.18 — no existing // tool, action, or param changed shape. // // What this closes: the server has supported clearing a parent since // BUG-2013 (extractParentLink treats a PRESENT-but-empty "parent" key in // fields_patch as "detach"), but neither client surface could reach it. // `pad item update --parent ""` was a silent no-op (the CLI's // `if parentRef != ""` guard drops it before it ever becomes a key), and // the MCP `parent` param — a plain string — has the same ambiguity every // other schema-declared string on this tool has: empty reads as "not // provided", so `parent: ""` cannot be given a destructive meaning // without becoming a trap for a client that pads unused params with "". // // Same two reasons as `clear_assigned_user` / `clear_agent_role` (v0.18) // for using a boolean rather than overloading the empty string: // 1. Keeps the "empty declared string = not provided" invariant intact // for every other param on this tool. // 2. Only a boolean reaches LOCAL STDIO — BuildCLIArgs emits the CLI's // real flags, so a catalog param with no flag behind it is dropped // before dispatch. `clear_parent` maps to a new `--clear-parent` // bareword flag on `pad item update`, exactly as `clear_assigned_user` // maps to `--clear-assigned-user`. // // UPDATE ONLY, same asymmetry as v0.18 and for the same reason: an item // has no parent unless one is given at create, so a create-time clear // would be a no-op teaching a wrong affordance. // // A simultaneous set-and-clear (`parent` + `clear_parent`, including via // `field: ["parent=..."]`) is REFUSED on both transports, not silently // resolved — mirrors the v0.18 conflict checks. // // Also refused, not silently applied: `clear_parent` against a collection // whose schema declares its own "parent"/"plan" field — extractParentLink // skips hierarchy handling entirely for a schema-shadowed key and lets it // fall through as an ordinary field write, so the wire shape // {"parent":""} can no longer distinguish clear-hierarchy intent from a // legitimate blank-a-real-field write once it reaches the server; the // ambiguity is created at the client surface that accepted `clear_parent`, // so that surface refuses rather than guessing (codex round 2). // // - "0.18" — historical. IDEA-2584: `clear_assigned_user` / `clear_agent_role` // booleans on `pad_item`, the canonical and DISCOVERABLE way to unassign. // Additive param bump, v0.5 / v0.6 precedent — no existing tool, action or // param changed shape, and v0.16/v0.17's empty-string forms keep working // and are NOT deprecated. // // What this closes: v0.16 and v0.17 made the clear WORK, but the params // that do it (`assigned_user_id` / `agent_role_id`) were never in the // catalog. An agent reading the schema to find out how to unassign saw // only `assign` (a name) and reached for `assign: ""`, which is a no-op // and stays one. The capability existed; nothing advertised it. // // WHY BOOLEANS, not the empty-string params declared as-is. Two reasons, // and the second is decisive: // 1. An empty DECLARED string is inert everywhere else on this tool // (title, content, comment, tags), so a client that pads optional // params with "" is harmless today. Giving one a destructive meaning // would turn that client into one that silently unassigns everything // it touches. A boolean carries its meaning in its name. // 2. Only a boolean can reach LOCAL STDIO. BuildCLIArgs emits the CLI's // REAL flags, so a catalog param with no flag behind it is dropped // before dispatch — declaring `assigned_user_id` would have left the // direct form remote-only, i.e. would not have closed the gap it // exists to close. These map to `--clear-assigned-user` / // `--clear-agent-role`, new bareword flags on `pad item update`, // exactly as `allow_draft` maps to `--allow-draft`. // // UPDATE ONLY, deliberately asymmetric with create: clearing at create is // a request to not-set something never set, so the only honest behaviour // is a no-op — which teaches a wrong affordance and pads every create // call's schema. `item create` has no such flags and a test fails if // someone adds them. An item is created unassigned unless `assign` is // given. // // A simultaneous set-and-clear is REFUSED on both transports rather than // silently resolved: the store's branch order sets before it clears, so // sending both would make the clear a no-op and the assignment win. The // check runs AFTER --field lifting, since that is a second route to a // competing value. // // Server-side this is WIRING, not new semantics: // models.ItemUpdate.ClearAssignedUser / ClearAgentRole already existed and // the store has honoured them since BUG-2566, on the same branch as the // empty-string form. // // - "0.17" — historical. BUG-2583: unassign now works on the LOCAL STDIO // transport too, closing the gap v0.16 documented. No tool, action, or // parameter shape changed — another BEHAVIOUR bump, same grounds as // v0.16 and v0.9. // // THE FORM MATTERS, and only one works everywhere: // `field: ["assigned_user_id="]` clears on BOTH transports. The direct // `assigned_user_id: ""` param clears on REMOTE ONLY — it is not // declared in the pad_item schema, so it survives to the remote mapper // by riding the verbatim input map, while BuildCLIArgs drops it on // stdio (verified: the stdio call is a clean no-op, not a corrupting // one). IDEA-2584 tracks declaring the params properly; until then the // `field` form is the one to document. // // v0.16 fixed the two filters in the remote dispatcher. Local stdio MCP // goes nowhere near them: ExecDispatcher shells out to the `pad` CLI, // and the CLI wrote `--field assigned_user_id=` into the item's // FIELDS JSON BLOB while the column stayed stale — then printed // "Updated TASK-9". A success message for a write that did nothing the // caller asked for, and a blob key shadowing a real column's name. // `cmd/pad/cmd_item.go` now lifts columnFieldKeys onto the columns on // both create and update, mirroring liftFieldsToColumns (and its // INVARIANT) on the MCP side, so the two surfaces stop diverging. // // TWO compat changes, ruled separately: (1) NON-empty // `--field assigned_user_id=` now writes the COLUMN and no longer // writes the blob key — accepted on the grounds that relying on the old // behaviour is relying on a shadowing defect; (2) EMPTY clears the // column, which falls out of the lift and inherits BUG-2566's store // semantics. `agent_role_id` gets identical treatment. Existing stray // blob keys are left alone — this stops minting new ones; a cleanup // sweep would be its own change. // // Still true from v0.16: an empty `assign` / `role` does NOT clear on // either transport, deliberately (IDEA-2584) — see resolveAssignName. // // - "0.16" — historical. TASK-2571: an MCP agent can now UNASSIGN an item. // No tool, action, or parameter shape changes — this is a BEHAVIOUR // change, bumped on the same grounds as v0.9 (which moved for a return // shape with an unchanged signature): an empty-string // `assigned_user_id` / `agent_role_id`, whether passed at the top level // or via `field: ["assigned_user_id="]`, was silently dropped by the // dispatcher and is now forwarded as a clear-to-NULL. // // Compat posture: ACCEPTED, not worked around. A caller sending `""` // today gets a no-op; after this they get a clear. That is the correct // reading of the input — nobody sends an empty assignment ID meaning // "leave it alone" — and the no-op was the surprising half. The store // has had defined clear-to-NULL semantics for exactly these two columns // since BUG-2566 and the HTTP surface inherited them, so this is // uniformity restoration: MCP was the only surface with no way to // unassign. // // Deliberately NOT changed: the empty-string filter on `tags` at the // same call site (codex #547 r3 P2). `tags: ""` is not a clear, it is a // corrupt write into a JSONB/TEXT column. Same-looking guard, opposite // justification. // // Also not done: `clear_assigned_user` / `clear_agent_role` schema flags // mirroring the HTTP body (option (b) on the task) — deferred by the // lead as additive sugar, then reopened by codex review and filed as // IDEA-2584: the catalog exposes `assign` / `role`, NOT the ID params, // so an agent reading the schema still cannot discover the clear. // // TRANSPORT SCOPE, verified live rather than assumed. This fixes the // REMOTE /mcp transport (HTTPHandlerDispatcher), which is where both // filters lived. LOCAL STDIO MCP — `pad mcp serve`, i.e. Claude // Desktop / Cursor / Windsurf — still cannot clear an assignment, // because ExecDispatcher shells out to the CLI and the CLI has no // unassign at all: `--assign` / `--role` skip on empty, and // `--field assigned_user_id=` lands in the item's fields JSON blob // (observed: fields became {"assigned_user_id":"",...} while the // column stayed set) instead of being lifted onto the column the way // liftFieldsToColumns does for HTTP. That is a separate defect with a // CLI-wide blast radius, tracked as BUG-2583 — do not read this // entry as covering it. CLOSED IN v0.17 BELOW; this paragraph // describes v0.16 only. The schema-declared `assign` / `role` // aliases also still no-op on both transports (IDEA-2584); see // resolveAssignName for why an empty alias is deliberately NOT a // clear. // // - "0.15" — historical. TASK-2096: adds the `unparented` boolean parameter // to `pad_item.list`, mutually exclusive with `parent`, and forwards it // through both local exec and remote HTTP dispatchers. The parameter // selects items with neither the legacy parent_id column nor an outgoing // parent/implements relationship. // // - "0.14" — historical. TASK-2022: adds a `history` action to `pad_item` // (read-only item version history — newest-first metadata: id, // created_at, created_by, source, change_summary; the resolved // content body is omitted for token thrift). Also adds an // `expected_updated_at` param to `pad_item` for optimistic- // concurrency on `update`: round-trip the updated_at you last read // and the update fails with a structured 409 (code=update_conflict) // if the item changed since. The `update` action's field writes are // now a server-side field-level MERGE (only the keys you set change) // rather than a full-blob replace, closing the concurrent-update // lost-write race (IDEA-1480) — a behavior change to the update // path plus a new action and a new param, hence the version bump. // Pure addition to the action enum + param vocabulary; existing // pad_item actions/params are unchanged and backwards-compatible. // // - "0.13" — TASK-2019: agent-oriented backlog queries. // Adds `ready` + `stale` actions to `pad_project`, mirroring the // existing CLI `pad project ready` / `pad project stale`. `ready` // (read-only) returns the current actionable backlog — the // query-oriented counterpart to `pad project next`, reusing the // dashboard's suggested-next logic. `stale` (read-only) lists items // needing attention (stalled, blocked, overdue, or otherwise out of // the active workflow). Both HTTP dispatchers already existed // (dispatch_http_project.go); this bump wires them onto the catalog // surface. `pad project reconcile` stays CLI-only — it shells out to // `gh` to compare stored PR metadata against live GitHub state, a // local-git dependency an MCP agent lacks. Pure addition of two // read-only actions; existing pad_project actions unchanged. // Backwards-compatible for v0.12 consumers that don't enumerate the // new actions. // // - "0.12" — TASK-2018: agent-accessible activity feed. // Adds an `activity` action to `pad_project` mirroring the new CLI // `pad project activity [--limit N] [--actor user|agent] [--since DATE]`. // It's the non-streaming, bounded query counterpart to // `pad project watch` (the live SSE stream, which stays CLI-only): // a read-only snapshot of the workspace's enriched activity feed — // item refs, titles, and field-level change details — so an agent // can catch up on what OTHER agents/users did since it last worked. // Backed by the existing `GET /workspaces/{ws}/activity` endpoint // (previously web-UI-only), extended with a server-side `since` // date filter so `limit`, `actor`, and `since` behave identically // across the CLI, local stdio MCP, and cloud HTTP transports. Adds // `actor` + `limit` params to the pad_project vocabulary (`since` // already existed for changelog). Pure addition of one action + // params; existing pad_project actions unchanged. Backwards- // compatible for v0.11 consumers that don't enumerate the new // action. // // - "0.11" — TASK-2017: read-only attachments surface. // Adds a new `pad_attachment` tool (the tenth resource × action // tool) with two read-only actions — `list` and `show` — mirroring // the CLI `pad attachment list` / `pad attachment show`. `list` // (read-only) enumerates a workspace's attachments with optional // filters (item, category, collection, attached/unattached, sort, // limit, offset); `show` (read-only) returns one attachment's // metadata (MIME, size, filename, ETag, last-modified) via a HEAD // request without transferring bytes. Both HTTP dispatchers already // existed (dispatch_http_attachments.go, TASK-871 era); this bump // just wires them onto the catalog surface. Upload / download / view // stay CLI-only (filesystem-bound) and are NOT exposed. Pure // addition of one tool + two read actions; existing tools/actions // unchanged. Backwards-compatible for v0.10 consumers that don't // enumerate the new tool. The base64 image RESOURCE for multimodal // agents was tracked separately (TASK-2076/2077) and shipped later // in PR #930; it was not part of this bump. // // - "0.10" — BUG-2020: server-side draft-playbook gate. // `pad_playbook.run` now refuses a playbook whose status isn't // "active" (a draft still being authored) with a structured // `playbook_not_active` error, and adds an `allow_draft` boolean // param (escape hatch) that runs a draft anyway. Both the `run` and // `get` responses now echo the playbook's `status`. Pure addition of // one param + one echoed field + a new refusal path; existing active // playbooks run unchanged. Backwards-compatible for v0.9 consumers // that don't set allow_draft — except that running a draft (which // the skill already told agents not to do) now errors instead of // silently returning the body. // // - "0.9" — TASK-2000: `pad_item.list` is now summary-shaped // and bounded. Two changes for agent-token thrift: // // - The `list` action injects a default `limit` (50) and clamps an // oversized one (max 300), mirroring the backlinks default/max, so // a bare agent list can't dump the whole workspace into context. // // - The list RESULT shape changed: `pad item list` (which the // ExecDispatcher shells out to) now defaults to a token-light // SUMMARY projection — the rich `content` body is replaced by a // short `content_preview`, UUID plumbing (id, workspace_id, // collection_id, *_user_id, parent_id, agent_role_id) and the // duplicate collection/parent join fields are dropped, and // `fields`/`tags` are emitted as nested JSON rather than escaped // strings. This is a BREAKING result-shape change for consumers // that read `content` or the dropped fields off a list row; the // full former shape is available via the CLI `--full` flag (at // v0.9 not yet surfaced as an MCP param; v0.20 declared it as the // `full` boolean and closed the HTTP transport's shape gap — // BUG-2305). No action-enum or param // removals; `limit` semantics unchanged for callers that pass one // under the max. // // - "0.8" — historical. TASK-1973: workspace soft-delete recovery. // Adds two actions to `pad_workspace` mirroring the CLI // `pad workspace deleted` / `pad workspace restore` (TASK-1972): // `deleted` (read-only) lists the caller's soft-deleted workspaces // still inside the 30-day restore window; `restore` (mutating, not // destructive) un-soft-deletes a workspace by `slug` while it's // still restorable (owner-only). Both non-interactive. Reuses the // existing `slug` param (now also required for action=restore); no // new params. Pure addition; existing pad_workspace actions // unchanged. Backwards-compatible for v0.7 consumers that don't // enumerate the new actions. // // - "0.4" — PLAN-1410: comprehensive bootstrap-payload // trim, cutting ~40% of bytes off the AgentBootstrap response. // Same tool catalog (still eight resource × action tools + // pad_set_workspace); the shape changes are entirely inside the // bootstrap JSON those tools/resources return: // // - Slim BootstrapCollection projection (TASK-1412): drops `id`, // `workspace_id`, `created_at`, `updated_at`, `settings`; // `schema` is now a nested JSON object rather than an // escaped JSON-encoded string. // // - Slim BootstrapRole projection (TASK-1423): drops `id`, // `workspace_id`, `tools`, `created_at`, `updated_at`. // // - Convention `slug` dropped (TASK-1413) — agent addresses by ref. // // - Top-level `recent_activity` removed (TASK-1413) — was a // bit-for-bit duplicate of `dashboard.recent_activity`. // // - BootstrapDashboard wrapper caps five dashboard sub-arrays // (attention, recent_activity, active_items, active_plans, // by_role) at 5 entries each, with parallel // `_overflow_count` fields surfaced when truncation // fired. TASK-1413 added the first two; TASK-1422 added the // remaining three. suggested_next deliberately excluded — // already capped to 3 upstream in buildDashboardResponse. // // - Schema field `label` omitted when label == TitleCase(key) // (TASK-1424); custom labels preserved. // // Compatibility note: most v0.4 changes are subtractive (dropped // fields) or additive (overflow counts), but ONE field had its // JSON type change — collections[].schema went from a // JSON-encoded string ("schema":"{\"fields\":[...]}") to a nested // JSON object ("schema":{"fields":[...]}). This is a breaking // change for any v0.3 consumer that read schema as a string and // JSON.parse()'d it themselves. Agents now read it as a parsed // object directly. Clients that relied on the dropped fields // (UUIDs, timestamps, settings, the duplicate recent_activity, // convention.slug) need to switch to the canonical alternatives // (slugs for addressing; pad collection list / pad role list for // the full models when needed). // // Discovery surfaces: // // - result.capabilities.experimental.padToolSurface.version (handshake). // - pad://_meta/version resource (queryable JSON document). // - pad_meta.action: tool-surface (full catalog introspection). // - "0.22" — BUG-2674: `pad_item.action=move` no longer // destroys an item's system metadata, and now REFUSES a `field` // setter naming one of those keys. // // BEHAVIOR bump on the v0.9/v0.16/v0.17 grounds — no tool, action // enum, or param shape changed. Two observable differences: // // A move used to drop implementation_notes / decision_log / // github_pr / convention outright, because MigrateFields matched // every key against the destination schema and no schema declares // those. They now carry, and any field the target schema HAS no // home for is reported in the move's activity entry rather than // vanishing. github_pr is the one exception and only across // WORKSPACES (the copy path), where the repository it names // belongs to the source project — reported there as // `referent_not_portable`. // // `field: ["implementation_notes=..."]` on a move or copy now // answers `malformed_override` instead of writing the key. That // write was never legitimate: it bypassed the append guard // (BUG-2627) and, on a cross-workspace copy, could reintroduce the // github_pr the migration had just dropped. Agents write these // through `action=note` / `action=decide` and the GitHub link flow. // // Compat posture, stated deliberately: today's callers passing such // a setter get a 400 where they previously got a silent corrupt // write. Relying on the old behaviour is relying on a defect, the // same reading v0.17 took for the fields-blob shadowing. // // - "0.23" — BUG-2627 part 2 + BUG-2675, one bump for two // changes at the same door. BEHAVIOR bump on the v0.9/v0.16/v0.17/ // v0.22 grounds — no tool, action enum, or param shape changed. // // (1) `pad_item.action=update` REFUSES a `field` setter naming // implementation_notes, decision_log or convention, where it // previously wrote it. HTTP answers 400 `validation_error`; MCP // clients see that as `validation_failed`, which is the code the // catalog and instructions.md name, since that is the one an agent // branches on. // // `github_pr` is deliberately NOT refused, and the exception is // load-bearing rather than a soft edge. The rule being applied is // "a raw write is refused where a real writer exists"; for the // other three that writer reaches every surface, and for github_pr // it does not — `pad github link` needs a local git checkout and // the `gh` CLI, so it is excluded from remote MCP by name, and // noRemoteEquivalent points remote agents at // `item update --field github_pr=...` as the alternative. Refusing // it would have deleted the only door those agents have and // answered with a message naming a command they cannot run (Codex // round 3). // // Round 4 then found that door does not actually work: a `field` // value is stored as a STRING on every surface, so the PR data // lands double-encoded and no link appears — the BUG-2627 shape one // key over, filed as BUG-2696. That does not change the exemption // (refusing would leave remote agents with strictly less), but it // does change what the catalog and instructions.md may PROMISE, so // both now say the door is open and broken rather than advertising // a capability that isn't there. v0.22 closed the same door on move/copy field // OVERRIDES; this closes it on the ordinary update, which is the // door agents actually reach for. Server-side in the fields_patch // gate, so it lands on BOTH transports at once — remote posts // fields_patch directly, and stdio's CLI (`pad item update // --field`) lowers into the same key. That was verified at both // call sites rather than assumed, because a fix that reaches one // transport is the shape v0.16 shipped and v0.17 had to finish. // // Compat posture, same as v0.22's: today's callers passing such a // setter get a 400 where they previously got a write that made the // entries unreadable everywhere AND disabled `action=note` / // `action=decide` on that item until the row was repaired. Relying // on the old behaviour is relying on a defect. // // Item CREATE is deliberately NOT covered: its full-`fields` door // is shared with Pad's own writers (convention activation lowers // into it), so it needs a different rule, tracked in BUG-2685. // // (2) NEW ERROR CODE `stored_state_unreadable` (BUG-2675) — the // first addition to the closed ErrorCode set since v0.14's // update_conflict. Fires when an operation is refused because the // ITEM'S STORED value cannot be decoded: today, `action=note` / // `action=decide` against a field whose stored value is not a list // of entries (the v0.22-era guard from BUG-2627 part 3). // // It replaces `server_error` for exactly that condition, which was // wrong in both halves — not our fault, and not transient. The // defining property is RETRY-HOSTILITY: the refusal is fully // deterministic, so an agent that backs off and retries burns // round-trips against a state only a repair can clear. Both // transports emit it: HTTP classifies the sentinel error directly, // stdio via the `pad-structured-error/v1:` marker the CLI now // writes for its own local refusal. // // ADDITIVE for consumers that switch on code — an unknown code was // always possible and the envelope shape is unchanged — but a // client that pattern-matched `server_error` to detect this // condition would stop matching. That client was retrying a // permanent failure. // - "0.26" — IDEA-2756: `pad_workspace.action=create` is now // REFUSED with a 403 when the calling OAuth connection's grant has // `may_create_workspaces=false`. Previously that flag gated only the // post-creation auto-add, so the create succeeded — and on a // connection with an EXPLICIT workspace allow-list the agent was // handed a workspace it could not then see. (Not universal: a // connection with `all_current_workspaces=true` is not gated per // slug, so it could see what it made. The consent mismatch is the // constant across both; the invisibility was only its most visible // symptom.) // // BEHAVIOR bump, not a shape one — no tool name, action enum, or // parameter changed. Same grounds as v0.25 (activation destination), // v0.16 (empty-string clear) and v0.10, which is the closest // precedent: a server-side gate that starts refusing a call it used // to permit, with the refusal surfaced as a structured error. // // Unlike v0.10 there is no `allow_draft`-style escape hatch, and // deliberately so: the gate expresses a decision the USER made on // the consent screen, so a parameter that let the caller bypass it // would be the app overriding its own grant. Both remedies are the // USER's and neither is the app's: re-authorize, or enable the flag // on the existing connection via PATCH /connected-apps/{id}/flags // (the console page). The refusal message names both, and // instructions.md tells the agent not to retry and not to reach for // the claim flow (there is nothing to claim when nothing was // created). // // Ruled by Dave on IDEA-2756: the consent checkbox is a permission // on whether the connected token may CREATE, and has to be true to // what a user would honestly expect from the option. Applied to // `POST /workspaces/import` as well as `POST /workspaces` — import // mints a workspace through store.ImportWorkspace, so it is the // same permission at a second door — though import has no MCP // action today and so is invisible from this surface. // // - "0.25" — TASK-2657: `pad_library.activate` resolves its // DESTINATION collection from the target's declared artifact kind // (SPEC-5 collection traits) rather than from the literal slugs // "conventions" / "playbooks". // // BEHAVIOR bump, not a shape one — no tool name, action enum, or // parameter changed. Same grounds as v0.9 (list return shape) and // v0.16 (empty-string clear semantics): what the tool DOES changed // while its signature did not, and a consumer reasoning about where // an activation lands needs to know. // // Before: activation posted to the literal slug, so a workspace that // had renamed either collection got a not-found with the collection // sitting right there (BUG-2702). After: it posts to whichever // collection declares that artifact kind, falling back to the // canonical slug ONLY when the lookup SUCCEEDS and finds no // declaration — the genuine pre-backfill case. A lookup ERROR is now // surfaced rather than silently falling back, because falling back on // an error means writing to a slug nothing was confirmed about. // // Also in this change, not itself a surface bump: `pad_item.list` // against a renamed conventions/playbooks collection still needs the // current slug — the trait moves the KERNEL behaviors, not the // addressing of an explicit collection query. instructions.md says so // now, at the point an agent would otherwise trust the literal. // // - "0.24" — #1066: the pad_item `fields` OBJECT is now a // real write form, and undeclared input keys fail loudly. Two // halves, one contract change: // // (1) `fields` alias. Since the BUG-991 normalization, reads return // `fields` as a native object, so writing back the structure you // just read was the obvious call — and it was a silent no-op: not a // declared param, nothing set additionalProperties, so the object // was accepted, never mapped by BuildCLIArgs, and dropped while the // PATCH still ran (success + bumped updated_at + unchanged value). // create/update now fold `fields` into the same path as `field` / // the dedicated params (catalog_item_fields.go). The same key with // CONFLICTING values in two places is REFUSED with a structured // error — refuse-on-ambiguity, same disposition as clear_parent / // clear_assigned_user; equal duplicates collapse to one write. // Non-writer actions refuse a `fields` param loudly rather than // letting the now-declared key be dropped at dispatch. // // (2) Strict input validation. The fan-out handler now rejects any // top-level key outside the tool's declared schema (action, // workspace, declared params, and a small documented compat list — // pad_item's v0.16 assigned_user_id / agent_role_id clear form) // with a structured validation_failed naming the offending keys. // This turns every future undeclared-param variant of this bug into // a loud error instead of a silent no-op, across ALL catalog tools. // // Bump rationale: (1) is additive (new param), but (2) changes the // contract for inputs that previously "succeeded" — any consumer // relying on an undeclared key being ignored now gets an error. // That reliance was indistinguishable from a bug in the caller // (the key never did anything), so the break is the fix. Single // bump covers both halves; they are one contract change. // // 0.27 — BUG-2850. Field values are TYPED SERVER-SIDE, the `fields` // object reaches the remote door with its JSON types intact, and // the fields/field merge refuses several ambiguities it used to // resolve silently. // // (1) Coercion. A string arriving for a declared number/json field // is coerced against the collection schema before validation, at // all eight Validate* call sites. The remote /mcp transport builds // its field map in ingestFieldKVP, so EVERY value reached the // server as a string and a declared number or json field was // unwritable from that transport — a 400 on a correct value in the // wrong clothes. Uncoercible values are still refused with the // validator's existing message. // // (2) The `fields` OBJECT carries native types. The catalog no // longer stringifies it; the HTTP mappers apply it last so the // types survive. The key=value doors (CLI --field, stdio MCP via // the CLI, remote field:[…]) are string-by-construction and // unchanged — the fidelity is available wherever the encoding // carries it, not everywhere. // // (3) Undeclared keys are NAMED. Item create/update responses may // carry `warnings.undeclared_fields`; the CLI prints the same list // to stderr, never stdout. Additive and omitempty, so a clean // write is byte-identical to before. Keys are ACCEPTED, not // refused: a census of 1012 items found 14 undeclared keys across // 168 live values (priority alone at 127), so refusing would have // broken read-modify-write on items nobody had edited wrongly. // // (4) ONE conflict check over a canonical view of every source. // The `fields` object, the `field:[]` entries, the promoted named // params and the v0.16 compat IDs are resolved to a canonical key // and adjudicated once. What this REFUSES that 0.26 permitted: // // - two names for one target in a single call — parent/plan, // assign/assigned_user_id, role/agent_role_id — refused even // when the values match, because the names address one thing // through different vocabularies (a slug and a UUID are not // comparable), and the doors resolved them differently; // // - the same key supplied through the `fields` object and // another source with DIFFERING values (equal ones collapse to // one write); // // - a non-string `assign` / `role`, which one door silently // dropped and the other rejected; // // - an empty hierarchy value inside `fields`, which promoted // onto a param both doors read as "not supplied" and so // reported success having detached nothing. Use clear_parent. // // NOT refused, deliberately — but NARROWER than it first reads. A // SCHEMA-DECLARED param colliding with a `field:[]` entry under the // same name, with no `fields` object, still resolves: the CLI has a // real flag for such a param, so stdio receives BOTH forms and its // overlay order resolves them exactly as the HTTP mapper does. It // is visibly a duplicate, so last-write-wins is a resolution the // caller can predict. Pinned per door so they cannot drift apart. // // The v0.16 compat IDs are NOT exempt, and being undeclared is // precisely why. BuildCLIArgs emits the CLI's real flags and there // is none behind `assigned_user_id`, so the top-level value is // DROPPED: stdio sees only the field entry while HTTP reads the // param. Two different people assigned from one call, with no // `fields` object anywhere. Last-write-wins cannot be the answer // when the doors do not receive the same writes, so that pair // refuses. // // Bump rationale: (1)-(3) are additive or fix outright breakage, // but (4) refuses calls 0.26 accepted. Same grounds as 0.26, 0.25, // 0.16, 0.10 and 0.9 — no tool name, action enum or parameter // shape changed, and the behaviour did. Every refusal added here // replaced a call that SUCCEEDED while doing something other than // what it said, so the break is the fix in each case. // // 0.28 — IDEA-2641 / GitHub #1010. Two ADDITIVE actions on // `pad_item`: `remind` arms a one-shot reminder at an RFC3339 // instant (`remind_at`), and `ack-reminder` acknowledges a fired // one by id (`reminder_id`). Two new params, both optional, both // ignored by every other action. Purely additive — no existing // name, enum or shape moved, and a 0.27 consumer that enumerates // neither action is unaffected. Same disposition as v0.13, v0.11 // and v0.8, which likewise wired existing CLI verbs onto the // catalog. // // WHY AN AGENT NEEDS THIS AT ALL, since agents already RECEIVE // reminders without it: the poll surface is `pad_project.action: // next` / `ready`, which were already exposed, so a reminder was // already reaching agents. What was missing is the other half — // deferring a piece of work is exactly the moment an agent knows // when it wants to be asked again, and it had no way to say so. // // `remind_at` REFUSES a bare date rather than reading it as // midnight. That is a refusal at the edge of a brand-new param, so // it breaks nothing, but it is stated here because the `date` // schema type accepts `YYYY-MM-DD` and a caller will reasonably // try it: a bare date names a 24-hour span, and choosing an hour // inside it would be the server firing at a time nobody picked. // // Re-arm and disarm are deliberately CLI-ONLY for now. Both address // a reminder by an id the agent would have to list first, and no // listing action exists on this surface — a door with no handle. // Adding them later is additive. // // 0.30 — BUG-2870. One `--field key=value` entry means ONE thing // at every door. Six sites parsed that entry independently — `item // create`, `item list`, `item update`, `item move` and `item copy` // in cmd/pad, plus ingestFieldKVP here — in four spellings, and // they disagreed: the CLI sites used both halves verbatim, so // `field:[" effort=l"]` stored an undeclared field literally named // " effort" and left the declared `effort` untouched, while this // door used to TrimSpace both halves and write `effort`. Same call, two // stored keys, decided by nothing but the transport. All six now // call items.SplitFieldEntry. // // Two rules, deliberately asymmetric. A padded KEY is REFUSED at // every door rather than trimmed at one: trimming silently // retargets the write to a different field than the caller typed, // and a workspace with a field key that genuinely contains a space // would have its data quietly moved. A VALUE is carried VERBATIM // at every door: trimming reinterprets a caller's bytes, and on a // text field the padding is content. A padded value against a // typed field is refused one layer down by validation, naming the // field — the same answer at both doors, since 0.27 types declared // fields server-side. // // The catalog's conflict pass is re-grounded on the same change, // because its rules were derived from this door's trimming: the // 0.27-era "COMPARED TRIMMED, EMITTED RAW" comparison now compares // RAW (raw is what both doors write), a padded entry is refused in // the pass rather than skipped, and parseFieldArray's refusal is // propagated rather than swallowed — reshapeItemFields returns // early with no `fields` object, so on the no-`fields` path that // refusal had no second owner. Canonicalization and the // re-emission path are gone with it: nothing rewrites a caller's // key any more, it is refused. // // Bump rationale: a BEHAVIOR bump on the 0.29 / 0.27 / 0.26 / 0.16 // / 0.10 / 0.9 grounds. No tool name, action enum or parameter // shape changed. EVERY door refuses a padded key now, and each was // accepting it differently: /mcp trimmed it and wrote the declared // field, the CLI stored a ghost field beside it. So both refuse // something they used to accept — the phrase "only /mcp changes" // was in an earlier draft of this entry and is wrong (codex round // 1). What is /mcp-ONLY is the value half: it used to trim a // padded VALUE and type the result, and now carries it through to // the same validation the CLI has always applied. // // A caller writing canonical entries sees no difference at either // door. A second, separately noticeable fix rides along: // detectFieldConflicts swallowed parseFieldArray's refusal as "the // caller owns this error surface", which held only while the sole // possible error was a shape error — reshapeItemFields returns // early with no `fields` object, so on the no-`fields` path four // existing refusals were landing as successes. // // No escape hatch, for 0.29's reason — there is no legitimate call // this refuses, only calls whose two readings a door used to // choose between silently. // // 0.29 — PLAN-2857 U1 / TASK-2878. A `relation` field value must // now NAME A LIVE ITEM in the collection that field declares. // `internal/items` has only ever checked the SHAPE of a relation // ("must be a string"), because deciding whether a string names an // item is a database question and that package is DB-free; so any // string at all was accepted and stored, and the client then had // no honest way to render it. // // What this REFUSES that 0.28 permitted, on every write door // `pad_item` reaches — create, update (fields), update // (fields_patch), bulk update, and a `field:`/`fields:` override // on move or copy. (a) A value naming nothing in the workspace // (`not_found`). (b) A value naming a live item in the WRONG // collection, which is the case a workspace-wide lookup silently // accepted. (c) A value in a field whose schema declares no target // collection at all (`target_missing`), surfaced rather than // treated as permission to store anything. (d) A SLUG — a // deliberate divergence from `ResolveItem`, which tries UUID, then // ref, then slug: a slug is neither an ID nor stable, and free // text like "red" resolving to whatever is slugged `red` today is // exactly the corruption this closes. // // Ordinary `validation_error`, no new error code and no new // details key — the stdio transport classifies by matching CLI // stderr prose, so a structured field it cannot see would help // nobody there, and a new error shape is a contract change for // every client. // // What this DROPS that 0.27 carried, on the migrate doors: a // CARRIED relation value — one already on the item, asserted by // nobody — is never refused, because refusing would make every // legacy item un-updatable, un-movable and un-copyable. Within a // workspace it resolves and SURVIVES; across a workspace boundary // it is dropped without a lookup, since a source-workspace id // cannot mean anything in the destination, and reported through // the same `warnings.dropped_fields` channel BUG-2674 established. // `pad_item.action=copy` therefore returns a `dropped_fields` // entry where 0.28 silently landed a dangling reference. // // Item write responses may additionally carry // `warnings.dropped_fields`, naming schema-declared keys the write // DISCARDED. At 0.29 that was one case: a relation field whose // schema DEFAULT is not a reference at all, reaching the blob // because `ValidateFields` assigned a default and skipped its own // type check. 0.35 generalised the cause and the case — see that // entry — so this key can now name a field of any type. Dropped // rather than refused because nobody in the request typed it, and // refusing would make every write into that collection fail on a // schema defect its author must fix elsewhere. Additive and // omitempty, exactly like 0.27's `undeclared_fields`, so a clean // write is byte-identical. // // Bump rationale: a BEHAVIOR bump on the 0.27 / 0.26 / 0.16 / 0.10 // / 0.9 grounds — NOT on 0.28's, which was purely additive. No tool name, action enum or parameter shape // changed. A caller that was writing a resolvable value sees no // difference; one that was writing an unresolvable value was // storing something no surface could render, so the break is the // fix. No escape hatch, deliberately: unlike 0.10's `allow_draft` // there is no legitimate call this refuses — the carried-value // case, which is the one with a real claim to leniency, is // already exempt by provenance rather than by a flag. // // 0.31 — PLAN-2857 U6 / TASK-2996. A `relation` field value may now // be an EXACT TITLE, scoped to the collection that field declares, // alongside the UUID and ref U1 accepted; reads carry a new // `relation_targets` member hydrating each stored id to // {id, ref, title}; and `pad_collection.create`'s `fields` DSL takes // the target collection slug as a relation's third part. // // A BEHAVIOR bump on the v0.30/v0.29 grounds — no tool name, action // enum or param shape changed. Two of the three parts are purely // additive: a title is a THIRD accepted spelling, so nothing a 0.30 // consumer sends stops working, and `relation_targets` is additive // and omitempty. The bump is owed by the DSL: `fields="owner:relation"` // used to be ACCEPTED and is now REFUSED. // // That refusal is the fix, not a regression. The old parser put the // third part into Options for every type, so a relation declared // through the DSL got Options=["colors"] and NO target collection — // and every subsequent write to that field was refused with // `target_missing`. The DSL was building a field that could never // accept a value, and a caller relying on that was relying on a // field that did not work. Same disposition as v0.24's strict-input // half: the reliance was indistinguishable from a caller bug, so the // break is the fix. // // Title resolution is COLLECTION-SCOPED, and that is the whole // point rather than an implementation detail. A title unique only // workspace-wide REFUSES, naming the collection searched; resolving // it workspace-wide would let free text land on whatever happens to // carry that title today. Two or more matches inside the declared // collection get a new `ambiguous` reason rather than `not_found`, // which would state the opposite of what happened. The ladder is // UUID, then ref, then title, so an item literally TITLED "COLO-3" // is unreachable by title while the ref COLO-3 resolves — deliberate, // since refs are canonical and the ambiguity is the title's doing. // // An id-only `relation_targets` entry means the target is gone OR // the caller may not see it, and consumers must not render it as // either: the two are made indistinguishable on purpose, and the // write side collapses `wrong_collection` to `not_found` for the // same reason. // // - POST-0.30, NO BUMP — BUG-2995. A successful content write through // the designated applier used to answer with the item's PREVIOUS // content: on that path the markdown goes to a live browser tab's // Y.Doc and the row write runs with Content nil, so the response, // built from that row, described the item as it stood before the // request. It now carries the content as SENT, plus an additive // omitempty `warnings.content_outcome: "applied_pending_flush"` // naming where the content actually is. // // Deliberately NOT a bump, and the line is worth stating because // 0.9 looks like a counter-example. 0.9 bumped because list rows // LOST fields a consumer was reading — `content` replaced by a // preview, UUID plumbing dropped. Here nothing is removed and // nothing changes type; what changed is the VALUE of one field on // one path, and that is worth stating plainly rather than filing // under "additive": a caller does observe different bytes than it // did before. // // The claim is that no DOCUMENTED OR SUPPORTED reliance breaks, not // that no reliance can possibly exist. A consumer could in // principle have detected the applier path by noticing that the // response echoed something other than what it sent, and that // detection stops working. But it was never documented, the same // mismatch was equally produced by a genuinely lost write, and // `warnings.content_outcome` now answers the question that hack was // approximating — so the supported replacement ships in the same // change. Against that: every consumer taking the response at face // value was being told its write was lost. That balance is the // BUG-2304 disposition (a wrong answer corrected, no names/enums/ // shapes changed, hence no bump) rather than 0.9's. // // What a consumer SHOULD change is reading, not parsing: during // the window a `get` (or `list` with full=true) still answers from // the row and returns the previous content, so the warning is the // signal to re-read later rather than to re-send. Whether the row // is ever updated is not established — the flush belongs to a // browser tab and BUG-3000 carries the open half — so no surface // here states a duration. // // 0.56 — BUG-3244. ADDITIVE bump: `content_state` gains a second value, // `superseded_set_aside`, which no write produces. It reaches this // surface everywhere the POST-0.33 entry below says `content_state` // does: `pad_item` get, `pad_item` list with full=true, `pad_playbook` // run, and the line above the body in the // `pad://workspace/{ws}/items/{ref}` resource. It means an editor // schema-version rebuild SET ASIDE edits that items.content never // received: they are kept, but no tab can restore them, so the // `applied_pending_flush` advice (open the item, re-read later) is // wrong for it and the resource line says so. It wins when both would // apply. A consumer that treats any `content_state` as "the body is // stale" stays correct; one that branches on the word must add a case. // The same value makes every content write refuse, token or not, with // the existing `content_pending_flush` code plus an additive // `details.set_aside_rows`; the structured hint branches on it (it // names `pad item set-aside` and `overwrite_pending_edits`, never // opening the item). `overwrite_pending_edits` still lifts it, // discarding the rows, and they are counted in // `warnings.pruned_pending_edits`. Reading or discarding the rows // alone (GET/DELETE collab-set-aside, `pad item set-aside`) is NOT on // this catalog. No name, enum or param shape changed; the bump is // owed because an agent that learned the one-word vocabulary now // meets a word it does not know, whose remedy differs. // // 0.55 — BUG-2659. BEHAVIOR bump, on the v0.36 / v0.40 grounds: the same // `pad_item.action=search` call can now answer from a DIFFERENT // collection. Its `collection` param reaches /search exactly as sent, // and /search resolves it the way the item routes resolve a collection: // exact slug first, then the singular/alias fallback, an archived // collection still claiming its name, once per workspace in scope. The // remote mapper used to normalise it to the canonical plural first, so // in a workspace holding BOTH `task` and `tasks`, `collection: "task"` // answered from `tasks`; it now answers from `task`. In every other // workspace the result is unchanged, because the shorthand still // resolves, now server-side. An unscoped search resolves per workspace, // so one call can read `task` in one workspace and `tasks` in another. // Local stdio reaches the same behaviour through the CLI, which sends // the name as typed only to a server advertising // `search_collection_resolution` in GET /server/capabilities, and // otherwise normalises exactly as before. No name, enum or param shape // changed. // // 0.54 — BUG-3217. A `fields` number is decoded from the raw request // bytes with its literal kept, on BOTH transports: mcp-go fills the // tool arguments with a plain json.Unmarshal, so 9007199254740993 used // to arrive as 9007199254740992 and 1.10 as 1.1. Remote /mcp stored the // rounded value; local stdio stringified the rounded float64 into its // `--field` entry, so it was affected too (the filing believed it was // not). Only `fields` is re-read; every other numeric param is unchanged. // The BUMP is owed by one NEW REFUSAL (lead-ruled on BUG-3217's trail): // on `pad_item.action=create` / `update`, a `fields` number beside a // `field: ["n=…"]` entry for the same // key is compared by exact numeric value, so two numbers that differ // only above 2^53 — which rounded to one float64 and were silently // collapsed into ONE rounded write — are now refused with // `validation_failed`, message "pad_item.: fields.n conflicts // with the field array entry "n=…" (… vs …) — pass one of them, or the // same value in both", hint "Check the input shape against the tool's // schema." A matching pair still succeeds as one write however it is // spelled: 3.0 beside n=3, and 1.10 beside n=1.1, collapse as they did. // // 0.53 — BUG-2819. ADDITIVE: every attachment row returned by // `pad_attachment.action=list` gains `filename_source` (caller | // normalised | substituted | derived | unknown), on BOTH transports — // remote /mcp passes the list handler's JSON through, and local stdio's // `pad attachment list --format json` carries each row as raw JSON. The // key is ALWAYS PRESENT (not omitempty): `unknown` is itself the answer // for a legacy row, so its absence would say nothing. The // stored filename alone cannot say whether the caller sent it or the // server substituted it — a file can really be named "upload.bin" — and // this is the field that can. Rows written before it read `unknown`, // never `caller`; a bundle import stores the bundle's claim when the // name arrives unchanged. Nothing moved, retyped or disappeared, so a // consumer that ignores the key is unaffected: the 0.28 / 0.13 // disposition (lead-ruled on BUG-2819's trail: bump if and only if the // field reaches a response, which it does). `pad_attachment.action=show` // synthesises its shape from HEAD headers and does NOT carry it. The // sibling `mcp_audit_log.tool_name_source` reaches no catalog action // (the catalog's `audit-log` is the workspace activity log), so it owes // nothing here. // // 0.52 — BUG-2367 item 4. `pad_item.action=move` (and bulk move, the // cross-workspace copy and its preflight) REFUSES a move that would // change the item between open, done and abandoned unless the caller // names the destination done field. It used to carry whichever value // survived migration, or inject the destination default, so a closed // task moved into ideas silently reopened as `new`, and an abandoned // item could land counted as shipped. Only the STATE is compared: two // different values that both mean done carry without asking. Move // answers 400 `state_change_requires_value` with details {field, // options, from, to} and a message naming both; bulk move puts the same // in a `failed[]` row; the copy answers 400 validation_error with the // same sentence; the preflight asks through a needs_value row with // reason `state_change`, which the web dialog's picker already renders. // A destination with no done select field has no state to change and is // exempt. BEHAVIOR bump on the 0.43 / 0.29 grounds (lead-ruled). // // 0.51 — BUG-2367. `pad_item.action=move` (and the copy and bulk-move // doors behind the same field pipeline) stops carrying a value into a // field the destination schema marks COMPUTED, and stops carrying a // value that collides on a destination `unique_scope` field. Before it, // a unique collision answered 500 on single move for invocation_slug // (the only indexed one), leaked the SQL error text through bulk move, // and stored a duplicate on any unindexed unique field. The rule is // provenance, as TASK-2878 made it for relation referents: a CARRIED // value that collides is DROPPED and reported, and a supplied override // or an injected default that collides is REFUSED with 409 conflict. A // required field emptied by the drop is refused as required, the // existing needs_value route. Reporting is additive and omitempty: the // move response gains `warnings.not_unique` [{key, value, holder?, // message}] (its keys also sit in the copy's `dropped_fields`), bulk // `updated[]` rows and the copy's `warnings` carry the same list, and the // copy preflight gains the drop reasons `target_computed` and // `not_unique` plus a `detail` sentence on the dropped row. `holder` (the // ref of the item holding the value) is present only when the caller may // see that item, so the drop is not an existence oracle. Local stdio // `item move` prints ` dropped: ` lines on stdout after its // success line. BEHAVIOR bump on the 0.43 / 0.29 grounds (lead-ruled): a // move or copy stores something different from what it stored before. // // 0.50 — BUG-3014. A `relation_targets` entry gains an ADDITIVE, // omitempty `stored_as_text: true` when the stored value is not // UUID-shaped: a title a bundle import carried verbatim, or legacy free // text. Such a value was never an id, and it used to hydrate as a plain // id-only entry, which reads as "the target is gone or not visible" (a // claim about a target when there was none). An id-only entry WITHOUT // the flag keeps exactly that meaning, and a UUID-shaped value, resolved // or not, marshals byte-identically to 0.49. The predicate is syntactic // and decided from the stored bytes alone, never from a lookup, so the // flag discloses nothing about any item (lead-ruled). It is deliberately // not coupled to the resolver's UUID/ref/title ladder, whose evolution // would otherwise change the flag's meaning without a bump. The CLI // renders such a value as `"" (text, not a reference)`, and the web // chip says the same. ADDITIVE bump (the 0.28 / 0.13 disposition). // // 0.49 — BUG-2410. `pad_project.action=report` with NO `collections` // param now leaves out SYSTEM collections (Collection.IsSystem: today // Conventions and Playbooks), so they stop counting in created, WIP and // status totals. A call that NAMES one (`collections: "conventions"`) still // includes it. BEHAVIOR bump on the 0.40 / 0.42 grounds (lead-ruled): no // tool name, action enum or param shape changed, but a default call's // content changed for every caller. The rule is the store's // (resolveReportCollections), so the web Insights page, the print report, // `pad project report` and both MCP transports change together. Keyed on // the flag, never on slugs, so a future system collection follows it. // // 0.48 — BUG-3163. `pad_item.action=create` REFUSES a `field` (or // `fields` object) entry naming any reserved metadata key — // implementation_notes, decision_log, github_pr, convention — on every // transport, with 400 validation_error; nothing is created. It was the // last door that stored one: a `field` value is typed by schema lookup, // these keys are in no schema, so it landed as a string no extractor // reads, and for notes/decisions it also made note/decide refuse on the // item until the row was repaired. The remedy is to create the item and // then use note / decide on it; convention metadata comes from library // activation. BEHAVIOR bump on the 0.47 grounds: a write door refuses a // call it used to accept, and nothing that worked is lost. // // Library activation, the one system writer that used the create door, // moves to a new ADDITIVE, typed item-create member, `convention` (the // ItemConventionMetadata object; an empty one is refused), on every // surface: CLI, remote library activate, and the web library and // Conventions pages. It is NOT on this catalog; activation is // `pad_library.activate`, whose shape is unchanged. The CLI verifies from // the stored fields that the metadata landed, because a server older than // it ignores the member and answers 201. // // Not reachable from this catalog, recorded because it is the same door // family: a FULL `fields` blob on the item-update HTTP endpoint may now // only CARRY stored reserved metadata unchanged. A differing value, or an // omitted stored key (which would delete it), is refused with 400, checked // against the row under the write lock. `pad_item.update` lowers every // field write into fields_patch and never sends a full blob. // // 0.47 — BUG-2696. `pad_item.action=update` REFUSES a `field` (or // `fields` object, or fields_patch) entry naming `github_pr`, on every // transport, with 400 validation_error naming `pad github link` / // `unlink` — the same refusal the other three reserved keys have had since // 0.23. `github_pr` was exempt then as remote MCP's only writer; that door // never worked (a field value is stored as a string, so the PR landed // double-encoded, no link rendered, and `=null` stored the string "null"). // BEHAVIOR bump on the 0.46 / 0.44 grounds: a write door refuses a call it // used to accept. Nothing that worked is lost: no remote agent could link a // PR before, and the noRemoteEquivalent hint already said so. // // The writer is a new, ADDITIVE, typed item-update member, `github_pr` // (a PR object: number > 0 and an absolute http(s) url required) plus // `clear_github_pr`, which `pad github link` / `unlink` / `project // reconcile` use. It is NOT on this catalog: a structured remote PR-link // action is deferred to the MCP catalog-trim decision. What it buys is // VALIDITY, not sender identity: any HTTP caller may send it, but only a // well-formed PR object gets in. The CLI verifies from the response that // the link landed, because a server older than it ignores the member and // answers 200. Item CREATE, which still accepts hand-written reserved keys, // is BUG-3163 (0.48). // // 0.46 — BUG-3156. `pad_item.action=bulk-update` over LOCAL STDIO and // REMOTE /mcp now REFUSES, per item, a status or priority change on an // item whose collection does not declare that field, and writes nothing // for that item; the rest of the call still applies. That is the answer // WebMCP has given since 0.44, so the three transports now AGREE on // whether the write happens. The row reports it in each transport's own // vocabulary, unchanged in shape: stdio's `failed[]` row carries code // `validation_error`, remote's `results[]` row carries code // `validation_failed` with the server's message in `hint`. BEHAVIOR bump // on the 0.44 / 0.43 grounds: no tool name, action enum or param shape // changed, but a write that used to be accepted with a warning is now // refused. // // The mechanism is a new, general, ADDITIVE request member on item // update, `refuse_undeclared_fields: true` (public API, documented in // CLAUDE.md): with it, the PATCH refuses with 400 validation_error // exactly the keys `warnings.undeclared_fields` would otherwise have // named. Both bulk-update transports set it, because status and priority // are the operation's keys rather than ones the caller typed (the // BUG-3154 line). `pad_item.update` does NOT set it and still // accepts-and-warns (0.27). SKEW, stated: a server that predates the // member ignores it, which is exactly the old accept-and-warn, so a newer // client against an older server behaves as 0.45 did. // // Separately, and without a bump, #1462 (the same bug's part a) stopped // remote bulk-update PATCHing the whole fields blob, which reverted // concurrent writes to other fields (BUG-3049's defect on the transport // that fix missed). // // 0.45 — BUG-2829. The error CODE a caller receives for an HTTP 413, on // BOTH transports: `too_large` (details.reason carrying the server's own // code) where it used to be `server_error`. BEHAVIOR bump on the 0.42 // grounds; no name, enum or param shape changed. Three producers are // reachable from the catalog, all deliberate caps: a title rename's // wiki-link cascade (`rename_cascade_too_large`, pad_item.update), an // oversized outbox event row (`event_payload_too_large`, any mutating // action), and an oversized artifact (`too_large`, pad_item.import). The // old code read as transient and invited a retry that fails identically; // the new one says shrink or split. Not `validation_failed`, because no // input value is wrong. REMOTE: classifyHTTPStatusKind maps 413 directly. // STDIO: the CLI root writes a structured marker for any 413 // (cli.WriteTooLargeError), and `too_large` joins // allowedStructuredErrorCodes. Every other unmapped 4xx still falls to // `server_error`; the enumeration on BUG-2829's trail found none of them // reachable from a catalog action. 0.44 is BUG-3154's (#1450). // // 0.44 — BUG-3154. `pad_item.action=bulk-update` over the WebMCP // browser transport now REFUSES, per item, a status or priority change // on an item whose collection does not declare that field. The item is // listed in the bulk response's `failed` array with code // `validation_error` and a message naming the collection and the field, // and nothing is written; the other items in the call still apply. The // door is `POST /workspaces/{ws}/items/bulk` (`set-priority`, and `move` // with a `status`, with or without a `collection`; for a collection move // the TARGET's schema decides). Its request carries an operation and a // value, and the SERVER picks the field key. It used to store the value // as an orphan field no schema renders while reporting the item as // updated. BEHAVIOR bump on // the 0.43 / 0.40 / 0.29 grounds: no tool name, action enum or param // shape changed, but a write door refuses a call it used to accept. // // THE TRANSPORTS DISAGREE after this, and it is stated rather than // implied. Local stdio (`pad item bulk-update`) and remote /mcp // (dispatchItemBulkUpdate) never reach the bulk endpoint. They issue a // per-item PATCH, which is the single-item update door and ACCEPTS an // undeclared key, naming it in `warnings.undeclared_fields` (0.27 / // BUG-2850). So the same catalog call is refused per item on WebMCP and // stored with a warning on the other two. The line between them is the // one this entry draws, server-chosen key versus caller-typed key, and // it is a property of the two HTTP doors, not of the catalog call. On // every transport the catalog caller supplies the same `status` or // `priority` param; only the door the transport happens to reach differs. Whether stdio and remote should call // the bulk endpoint is tracked separately; it is not converged here. // (CONVERGED in 0.46, BUG-3156, without moving them onto the bulk // endpoint: they refuse through the single-item door's new // `refuse_undeclared_fields` member.) // // 0.43 — BUG-2379. `pad_item.action=move` REFUSES a field override // naming a field the DESTINATION collection's schema does not declare, // with 400 `malformed_override` — the same check (UndeclaredOverrideKeys, // against the same stripped schema), code and message the cross-workspace // copy has always used. BEHAVIOR bump on the 0.40 / 0.36 / 0.29 grounds: // no tool name, action enum or param shape changed, but a write door // refuses a call it used to accept. It used to merge the key into the // moved item as an orphan field no schema renders, so the two sibling // endpoints disagreed about one request field. // // Refused although create and update ACCEPT undeclared keys and name them // in `warnings.undeclared_fields` (0.27 / BUG-2850). That acceptance // exists because callers round-trip the whole fields blob; an override is // never round-tripped state but a fresh assertion about the destination // schema, which is the copy's reasoning. Orphans already stored on the // moved item are carried by the migration, not by the override map, and // are unaffected. No escape hatch, for 0.29's reason. // // 0.42 — BUG-3142 + BUG-3147, one entry for one contract change: the // error CODE a LOCAL STDIO caller receives for two families of failure, // both of which used to arrive as `server_error` — a code that reads as // transient, so an agent retried a deterministic refusal forever. BEHAVIOR // bump on the 0.35 / 0.30 grounds: no tool name, action enum or param // shape changed, and the remote transport is unaffected by either half. // // BUG-3142. BuildCLIArgs emitted positionals FIRST with no `--`, so cobra // parsed any free-text value starting with `-` as a flag and refused the // call before the command ran (`item search "-ship it"`: "unknown // shorthand flag"). Every flag is now emitted first — root flags, // --workspace and --format included, which must stay ahead of the // terminator — then `--`, then the positionals. Calls that failed now // succeed; the 12 free-text positionals on the stdio surface are listed // on BUG-3142's trail. Separately, cobra's and pflag's argv refusals // (unknown flag, flag needs an argument, arg-count, required flag, unknown // command) now classify as `validation_failed`, and keep their message: // the usage strip used to truncate at "Usage:", which the root's // flag-error hook prints BEFORE the "Error:" line, so the message was // deleted and the envelope carried an empty hint. // // BUG-3147. A 429 on any command now reaches a stdio caller as // `rate_limited` — the code the remote transport already sent — through // the structured marker line the CLI writes at its root, carrying the // server's Retry-After as `details.retry_after_seconds` when present. The // remote envelope has no such detail: its dispatcher sees the body and // not the headers, so the two transports agree on the code and stdio // carries one additive detail more. // // 0.41 — PLAN-3114 unit 5 / TASK-3120. Adds a `match` action to // `pad_playbook`: given free text, asks the workspace's configured // typed-decision provider which of the workspace's ACTIVE playbooks (if // any) it best matches — a Choice question over their // ref/title/summary/trigger/invocation_slug, plus a reserved "none" // option so text that asks for none of them gets an honest answer // instead of a forced pick (and, for free, keeps a workspace with // exactly one active playbook askable — Choice needs >= 2 options). // Returns the choice, confidence, the provider's per-option // probabilities exactly as received, the model, and each considered // playbook's ref/title/invocation_slug. New optional param: `text`. // Pure ADDITIVE bump, same disposition as 0.13/0.28/0.11 — nothing // existing moved, and a consumer that never sends `action: match` is // unaffected. No provider configured -> 404 // `decision_provider_unavailable` (callers fall back to slug/trigger // routing); a provider error, OR an answer naming an option it was // never offered, -> 502 `decision_provider_error` — kept distinct from // the 404 case so a caller can tell "not configured, use the fallback" // from "configured but failing, maybe retry". Read-only and // side-effect-free: no job is enqueued, nothing is stored. // // 0.40 is BUG-3028's (below) — landed on main while this unit was in // flight; 0.41 takes the next number rather than contest it. // // 0.40 — BUG-3028. BEHAVIOR bump on the 0.35 / 0.36 grounds: a write // door refuses a value it used to accept. A scalar `relation` field had // three stored spellings of "no target" — the key absent, `""`, and // whitespace-only — and `required` was enforced only against the first, // so a required relation holding `""` validated. The canonical form is now // the one multi_relation already uses (0.33): the key ABSENT. A blank a // write SETS — on create, in `fields_patch`, as a changed value in a full // `fields`, or as a move/copy override — is normalised to absent BEFORE // validation, so a required one is refused as required, the same answer // deleting it already got. A legacy blank a write merely CARRIES (a patch // that does not touch it, a full-`fields` read-modify-write sending it back // unchanged, a move, a copy, an artifact import) is normalised to absent // AFTER validation and never refused, by the provenance rule 0.29 set for // carried relation values. `pad_item.list` with an EMPTY filter value on a // scalar relation key of the listed collection (`owner=`) now matches all // three spellings, where it matched only `""`. No param or shape changed; // no escape hatch, for 0.29's reason. // // 0.39 — BUG-3133. BEHAVIOR bump on the 0.35 / 0.36 grounds: a // write door now refuses a call it used to accept. `pad_item.update` // that sets content AND carries expected_seq or expected_updated_at // is refused with 409 `content_pending_flush` (details: ref, // pending_rows) while the item's op-log holds content-bearing rows // above its flush watermark — edits a browser tab has not saved // back, which the token cannot see because they are not in the row // it guards. Before this the write was accepted: through the // designated applier it replaced the tab's document, and with no tab // open the direct path's prune DELETED those edits, which existed // nowhere else. A new optional boolean `overwrite_pending_edits` // (additive, same bump) lifts the refusal; it is the only way out // without a browser, which is why it exists on this surface at all. // `content_pending_flush` is its own code, not update_conflict: // re-reading returns the same row and seq, so a read-and-retry loop // would never end. Unchanged: a write with no token, a write with no // content, and any write to an item with nothing pending. The first // of those now carries `warnings.pruned_pending_edits` (additive, // omitempty) naming how many pending rows a direct write deleted. // No escape hatch beyond the param, for 0.29's reason. // // 0.38 — PR #1238 (b4rk13) / GitHub #1221, execution lease. Two // ADDITIVE actions on `pad_item`: `claim` takes an atomic lease on // an item (`holder`, `ttl`; 409 `lease_held` with the holder and // expiry in `details` when another live holder has it; a re-claim // by the live holder extends the expiry), and `release` drops it // (idempotent). Two new params, both optional, both ignored by // every other action. Purely additive — same disposition as 0.28, // which wired `remind` / `ack-reminder` the same way, and a 0.37 // consumer that enumerates neither action is unaffected. // // Stated because the PR thread first ruled NO bump under DR-7: // DR-7 (tool_surface.go) is about a consumer tolerating an unknown // KEY in the serialized surface, not about the action enum. Adding // an action is the contract change that owns the bump — the entry // below (post-0.37) says exactly that, and 0.28 is the precedent. // The PR merged at 0.37; this bump follows it as its own commit so // the contributor's work lands as written. // // Also on this line: `ttl` shorter than one second is REFUSED at // the mapper. `int(d.Seconds())` truncates, so a sub-second ttl was // positive enough to pass the `<= 0` guard and still reach the // server as `ttl_seconds: 0`, which the handler reads as absent and // defaults to fifteen minutes — the exact silent default the guard // exists to prevent. A refusal at the edge of a brand-new param // breaks nothing that 0.37 accepted. // // 0.37 — PR #1337. Added the optional `agent` boolean to // `pad_item.get`. When true, both transports return a compact // projection that keeps the full body and work-relevant metadata // while dropping internal UUID plumbing and duplicate join fields. // Default `get` output is unchanged. The CLI exposes the same // projection as `pad item show --agent`. // // A catalog bump is required because `agent` is a new declared tool // input. The behavior is additive and opt-in; callers that do not send // it receive the prior response shape byte-for-byte. // // 0.36 — BUG-3082. A REF-SHAPED `relation` value no longer falls // back to matching by item NUMBER when its prefix names a collection // that is live in the workspace. Such a value is now `not_found`. // // BEHAVIOR bump on the 0.35 / 0.30 / 0.29 / 0.16 grounds — no tool // name, action enum or param shape changed, but a write door refuses // a value it used to accept. Closest precedent is 0.29, which this // finishes: that entry closed free-text corruption ("red" resolving // to whatever is slugged red today) and left the same corruption // open for anything shaped like a ref, which is the spelling the // docs tell callers to prefer. // // What it closes. Item numbers are workspace-unique and sequential // ACROSS collections, so `CONVE-1` and `SECRE-1` are never both // real. The resolver's fallback dropped the prefix and matched on // the number alone, and the item it found was usually inside the // field's declared collection — so the wrong-collection check above // it passed and NOTHING was raised. A caller wrote `CONVE-1`, the // item stored a different item's UUID, and the write answered 201 // with the substitution visible only in `relation_targets`. // // What it PRESERVES, which is why the predicate is about the prefix // rather than the item: a collection RENAME changes its prefix, and // a relation already written as the old ref has to keep resolving // (BUG-2873). That ref's prefix names nothing afterwards, so the // fallback still fires for it. A prefix that IS live and simply has // no item at that number is the caller naming a real collection that // does not contain what they said, and is refused. // // RESIDUAL, stated because it is not closed: a ref pasted from // ANOTHER workspace whose prefix also names no collection here still // resolves by number. Nothing in the value distinguishes it from a // rename's leftover, so closing it would cost the case the fallback // exists for. Pinned by a test rather than left implied. // // No escape hatch, for 0.29's reason. Out of scope and unchanged: // `GetItemByRef`, the navigational read path, where landing on the // moved item is the helpful answer. // // 0.35 — BUG-3079. An injected schema DEFAULT now takes the same // `validateFieldType` check a caller-supplied value takes. A default // that fails it is DISCARDED and named in `warnings.dropped_fields` // instead of being stored. // // BEHAVIOR bump on the 0.30 / 0.29 / 0.16 grounds — no tool name, // action enum or param shape changed, but a write door now stores // something different from what it stored before. Unlike 0.29, which // turned an accepted value into a refusal, this turns a stored value // into a reported drop: a create that used to land WITH a bad field // still lands, without it. // // What it closes is two doors disagreeing about one value. The same // bytes were refused when supplied and stored when injected, decided // only by who put them there — so a `status` retyped to `multi_select` // whose scalar default survived answered 400 to // `{"status":"open"}` and 201 to `{}`, storing `{"status":"open"}`. // A scalar in a list field is what BUG-3016, BUG-3057, BUG-3067, // BUG-3068 and BUG-3074 have each been defending one surface against. // // No escape hatch, for 0.29's reason: there is no legitimate call // this changes, only calls whose two readings the server used to // choose between silently. A consumer that reads `dropped_fields` // already handles the shape; one that ignores it sees a field it // never asked for stop appearing. // // ONE EXCEPTION, and it is the rule finishing its sentence rather // than a carve-out: dropping leaves a REQUIRED field absent, which is // exactly what the required check reports, so such a write is refused // — with a message naming the default as the cause, because a bare // "field is required" points its reader at a request that never // mentioned the field. // // 0.34 — BUG-3037. `pad_item` gains an `expected_seq` param on // `action: update`: the STRONG optimistic-concurrency token, preferred // over `expected_updated_at`. // // ADDITIVE — no tool name, action enum or existing param shape changed, // and `expected_updated_at` still works exactly as it did. Same // disposition as 0.13 / 0.11 / 0.8, which likewise wired an existing // server capability onto the catalog. A 0.33 consumer that never sends // `expected_seq` is unaffected. // // WHY IT IS NEEDED RATHER THAN NICE: `updated_at` is stored at // ONE-SECOND resolution, so two writes to a row inside one second both // match the token and NEITHER conflicts — the loser is accepted and // silently overwrites the winner, with both callers seeing success. That // is precisely the shape of agent traffic (read, decide, write, fast), // so the token an agent was offered could not refuse the race it exists // to refuse. `seq` is bumped on every mutation of the row under the // server's write lock, and is an integer, so it can. // // Raising `updated_at`'s resolution instead was measured and REJECTED: // the column is TEXT on both dialects and is compared LEXICALLY in SQL // (the since-cursor read plus eight ORDER BY sites), and Go's // RFC3339Nano omits trailing zeros, so a sub-second value sorts BEFORE a // whole-second value of the same second and a cursor would start // skipping rows. // // The read side is what makes the token reachable: `seq` is now // serialised without `omitempty` on `models.Item` AND added to the item // SUMMARY shape (`cli.ItemSummary`), which is what `pad_item.list` // returns by default since 0.9. A token absent from the shape a caller // reads is a token that caller cannot send. // // A value below 1 is refused at the HTTP boundary with a 400 rather than // a 409: the server never issues a seq below 1, so such a value is a // caller bug, and a 409 would read as contention that never clears. // // 0.33 — PLAN-2857 U4 / TASK-2999. A new `multi_relation` field type: // an ORDERED LIST of references, each element resolving through the // same UUID -> ref -> exact-title ladder a scalar `relation` uses. // // 0.32 was reserved for PR #1337, which was opened first. This unit // took 0.33 on a lead ruling rather than contest the number. PR #1337 // remained open while 0.33 through 0.36 landed, so its additive // contract now lands as 0.37 rather than moving the advertised version // backward. Version 0.32 remains skipped; a gap is cheaper than a // collision. // // A BEHAVIOR bump on the 0.31/0.30/0.29 grounds — no tool name or // action enum changed. Two halves, and they have opposite // compatibility stories: // // ADDITIVE: the type itself. A 0.31 consumer that never declares a // `multi_relation` field is unaffected by every rule below, because // all of them are reached only through a field whose schema says // `type: "multi_relation"` — a type no existing schema can contain. // `fields="owners:multi_relation:people"` takes the target collection // as its third part, the same shape 0.31 gave `relation`, and a bare // `owners:multi_relation` is REFUSED at parse time rather than // building a field with Options=["people"] and no target, which is // the unwritable field 0.31 was bumped to stop minting. // // A READ-SHAPE CHANGE, and the reason this is a bump rather than a // silent addition: `relation_targets[key]` now carries EITHER the // scalar object 0.31 emitted, OR a JSON ARRAY of those objects when // the key names a `multi_relation` field, in stored order. One map, // not a parallel `multi_relation_targets` key — a consumer rendering // "what does this field point at?" should not have to ask two // questions and merge the answers, and the field's key already says // which shape to expect because the consumer has the schema. // // A SCALAR ENTRY IS BYTE-IDENTICAL to 0.31's, which is the whole // compatibility claim, and a test pins it against a LITERAL rather // than against another call into the same code. A consumer that // parses `relation_targets` and has no `multi_relation` fields // therefore sees nothing new. One that acquires such a field must // handle the array; a strictly-typed client will fail to decode it, // which is why this is a bump and not a no-bump. // // Value rules, all ruled on TASK-2999's trail and all NEW rather // than inherited, because scalar `relation` had nothing coherent to // inherit (BUG-3028): exactly ONE stored form for "none", which is // the key being ABSENT, with `[]` normalising to it at the write // door; an empty or whitespace-only ELEMENT is REFUSED, not skipped // the way an empty scalar is; `required` means at least one RESOLVED // element; order is part of the value and duplicates are REFUSED // with a new `duplicate_referent` reason. // // `duplicate_referent` is detected AFTER resolution, which is why it // is not a shape check: two elements that duplicate a target are // usually DIFFERENT STRINGS — a UUID and a ref, a ref and an exact // title — so nothing that cannot resolve them can see it. Refused // rather than silently de-duplicated: a caller who sent one target // twice either erred or believes multiplicity means something, and // storing one copy answers neither honestly. // // Cross-workspace carry drops a `multi_relation` value WHOLE, never // partially, and reports it once through the same // `warnings.dropped_fields` channel BUG-2674 established — so an // import can never change an element count. // // - POST-0.33, NO BUMP — BUG-3000 / BUG-3033, the READ half of the // BUG-2995 entry above. // // Authored against 0.31 and rebased over U4's 0.33 bump, which is // why it is filed here rather than beside its sibling: it needed no // bump then and needs none now, and nothing in 0.33 touches it. The // two changes are independent — `multi_relation` is a field TYPE, // this is a row PROPERTY on every item. // // A read that serves a body the server knows is // behind the item's live collaborative document now says so, via // an additive omitempty `content_state` carrying the SAME value // vocabulary as `warnings.content_outcome`. A distinct name // because a write warning describes what a REQUEST did and this // describes what the ROW is; the shared vocabulary so an agent // that learned the word from a write response recognises it on a // read without a second lesson. // // BUG-3000 put it on models.Item, so every MCP surface that // serialises the struct — `pad_item` get, `pad_item` list with // full=true, the summary projection's preview — inherited it and // needed no entry here. BUG-3033 is why there is one: the doors // that build their OWN shape inherited nothing, and two of them // are on this surface. `pad_playbook` run responses gain the // field, and the `pad://workspace/{ws}/items/{ref}` RESOURCE gains // a line of markdown above the body, because a text resource has // no additive key to carry. // // The resource line is the one an integrator should read twice: a // consumer diffing that markdown byte-for-byte sees a new block // when — and only when — the body it is about is stale. That is a // changed VALUE on one path, the BUG-2304 disposition the entry // above argues at length, not 0.9's field removal. Nothing is // dropped or retyped, and a current item renders exactly as before. // // What a consumer should do with it is the same on both halves: // re-read later. The marker means the op-log is ahead of the row, // not that a write failed, and nothing server-side ever moves that // content into the row — so no surface here states a duration for // this one either, and it is not a promise that the row will catch // up at all. // // 0.60 — TASK-2198 U4, op-log recovery. BEHAVIOR bump on the v0.59 / // v0.39 grounds: no tool name, action enum or param shape changed, but // `content_state: "applied_pending_flush"` — on `pad_item` get / full // list, `pad_playbook` run and the item resource's line above the body — // now CLEARS ON ITS OWN. When the item's collab room closes (60s after // its last tab leaves) or a one-minute sweep finds it dormant, the server // rebuilds the body from the op-log in a worker process and stores it: // the body changes, `seq` moves (a held `expected_seq` then answers // `update_conflict`, where it used to answer `content_pending_flush`), // and the history gains a version by `system` / `recovery`. Before it the // row caught up only when a tab next opened the item. Not a guarantee: a // failed job, an item with set-aside rows and PAD_MATERIALIZE=off all // leave the old behaviour, so "re-read later, never re-send" stands. // // 0.59 — BUG-3252, the tombstone (ruled over cascade). BEHAVIOR bump // on the v0.58 / v0.45 grounds: no name, enum or param shape changed, // but `pad_item.action=delete-comment` on a comment that still has // replies now SUCCEEDS where v0.58 refused it with 409 // `comment_has_replies`. The comment stays as a tombstone: body // emptied, author and timestamps kept, reactions removed, and every // serialised comment gains an additive `omitempty` `deleted: true` for // one. Its replies keep their parent, and deleting its last reply // removes it in the same transaction. `edit-comment` addressed to a // tombstone answers 409 `comment_deleted` (details `comment_id`, and a // hint) on both transports; so do a reply or a reaction, which have no // catalog action. Deleting a tombstone again answers not_found. // `comment_has_replies` stays in the allow-list and the CLI marker for // servers that predate the tombstone. // // 0.58 — BUG-3252. BEHAVIOR bump on the v0.45 / v0.42 grounds: no // name, enum or param shape changed, but `pad_item.action=delete-comment` // on a comment that still has replies now answers 409 // `comment_has_replies` (details `comment_id`, `reply_count`, and a hint) // on every transport, where it used to be a retryable-looking // `server_error`: the parent_id foreign key has no ON DELETE, so the // delete failed inside the store. Nothing is deleted either way; a retry // refuses identically until the replies are gone. Stdio gets the code // from a marker the CLI root writes (cli.WriteCommentHasRepliesError), // remote from the 409 allow-list. Whether such a delete should cascade // or leave a tombstone is an open product decision; this refusal is the // floor under either. // // 0.57 — TASK-2695. ADDITIVE bump (the v0.28 / v0.13 disposition): two // `pad_item` actions, `edit-comment` (ref, comment_id, message) and // `delete-comment` (ref, comment_id), and the `comment_id` param. Web // users could edit and delete comments since PLAN-1662; agents could // only append corrections. Both ride new item-scoped routes, // PATCH/DELETE /items/{ref}/comments/{id}, which answer 404 when the // comment is not on `ref`. That check is the server's on every // transport, and the ACL is the workspace route's unchanged: edit is // author-only (a bearer admin gets no bypass, BUG-1919), delete is // anyone who may edit the item. Remote packages a delete's 204 as the // CLI's JSON, `{deleted, ref, comment_id}`. Every serialised comment // (list-comments on all transports, and the API) now carries a derived // `edited` bool, true once the body changed (updated_at > created_at, // the web timeline's rule). That key is new, not a change to an // existing one. Stdio reaches the actions through the new CLI verbs // `pad item comment-edit` / `comment-delete`, which refuse against a // server that does not advertise `item_scoped_comment_writes`. // // 0.61 — TASK-3321 U0b. BEHAVIOR bump on the v0.20 grounds (an // annotation change; no tool name, action enum or param shape moved). // `pad_workspace` now advertises destructiveHint:true and // openWorldHint:true, where v0.20 called it all-additive and every // tool closed-world. Its `invite` action SENDS an email to an address // the caller names: a send cannot be taken back, which the MCP spec's // destructiveHint (and OpenAI's plugin review) counts as destructive, // and an arbitrary recipient is outside the server's own data, which // is what openWorldHint means. `pad_playbook` also becomes // openWorldHint:true (still read-only and non-destructive): its // `match` action sends the caller's text to the configured decision // provider, a third-party model API. Every other tool's block is // unchanged. A client that confirms destructive tools will now // confirm pad_workspace calls. // // 0.62 — BUG-3326. ADDITIVE bump on the v0.53 / v0.28 grounds: no // tool name, action enum or param shape moved. Over REMOTE /mcp, // `pad_set_workspace` now embeds the workspace bootstrap under // `bootstrap`, as the stdio transport always did and as the tool's // description already promised: registerRemoteMCP passed no // BootstrapFetcher, so the response carried only {workspace, status: // not_persisted, note}. The embed is the bootstrap resource's own // in-process read (HTTPResourceFetcher), run as the token's user // through the dispatcher's scope/consent perimeter, so a workspace the // caller cannot read embeds nothing; a failed fetch stays non-fatal, // as on stdio. Stdio is unchanged. // // 0.99 — BUG-3790, starred truncation. ADDITIVE bump on the v0.53 // grounds: `pad_item.action=starred` returns at most 300 items, most // recently starred first, and when more exist says `has_more: true` // on the structured result plus the same truncation note as list. It // never pages (lead ruling, as for list). Both transports ask for one // row past the cap: remote as GET /starred?limit=301, stdio as // `pad item starred --limit 301` (a new CLI flag; without it the CLI // walks every page). Starred used to return everything. The `limit` // param's description names starred; no name, enum or param moved. // // 0.98 — BUG-3704, item list truncation. ADDITIVE bump on the v0.53 // grounds: `pad_item.action=list` asks for one row past its limit and, // when that row comes back, returns `limit` rows with `has_more: true` // on the structured result plus a second text block saying more items // match (narrow the filters or raise limit, max 300). It never pages. // A list that fits is byte-identical to before. No name, enum or param // moved; the default 50 / max 300 are unchanged. // // 0.97 — TASK-3613, lease worker id. ADDITIVE bump on the v0.53 // grounds: new optional `worker_id` on `pad_item` (claim, release) // and `pad_project` (next, ready). A lease is held under a worker id // plus the account; the holder label is display only. A remote // call's default worker id is derived from the account and its MCP // session, a claim answers with it, and passing it back resumes the // lease from another connection; a malformed one is refused // validation_failed. The remote default holder label drops BUG-2772's // "#xxxxxxxx" suffix. `lease_held` details gain `worker` (short id). // No name, enum or existing param moved. // // 0.96 — PLAN-3598 slice (c). ADDITIVE bump on the v0.53 grounds: // `pad_item.action=context` (read-only) returns one brief for working // an item, the body of GET /items/{ref}/context, with new optional // params `intent` and `budget`, and `role` now also read by it. New // codes `unknown_intent` (400, with details.valid_intents) and // `unknown_role` (400). No existing name, enum or param moved. // // 0.95 — TASK-3611, PR linking. ADDITIVE bump on the v0.53 grounds: // `pad_item.action=link-pr` (`ref`, `pr_number`, `pr_url`, optional // `pr_title` / `pr_state` / `pr_branch` / `pr_repo`) links a pull // request from metadata the agent SUPPLIES (nothing is fetched from // GitHub), through the typed github_pr member (BUG-2696), and // `action=unlink-pr` (`ref`) removes it, refusing an item with no link, // as the CLI does. Both transports: `pad github link --pr-number N // --pr-url U ...` (new flags; without them the command still reads the // branch and `gh`) and `pad github unlink`; remote PATCHes the typed // members. The `field` description no longer sends PR linking to a // human. No name, enum or param moved. // // 0.94 — TASK-3611, reminder list and cancel. ADDITIVE bump on the // v0.53 grounds: `pad_item.action=reminders` (`ref`) lists an item's // reminders with their ids (armed, fired, acknowledged), and // `action=unremind` (`reminder_id`) disarms one, on both transports // (`pad item reminders` / `pad item unremind`; GET // /items/{ref}/reminders, DELETE /reminders/{id}). Listing is // read-only; disarming needs edit permission on the item, as on the // CLI. Re-arm stays CLI-only. No name, enum or param moved. // // 0.93 — TASK-3611, structured fields on stdio. ADDITIVE bump on the // v0.53 grounds: `pad_item.action=create` and `update` over the local // stdio transport now WRITE a `fields` value that is an object or // array, where they refused it (BUG-2850). The stdio door passes those // keys, and every other non-string value (a number or boolean, which // --field key=value would deliver as text), to the CLI as one // `--fields-json` object (a new, additive CLI flag on `pad item // create` / `update`), which keeps their JSON types, the shape the // remote door already sends. Any other action still // refuses one. The `fields` description says so. No name, enum or // param moved. // // 0.92 — TASK-3585, appends. ADDITIVE bump on the v0.53 grounds: // `pad_item.action=comment`, `note` and `decide` accept `op_key`. // Retrying the SAME request with the same key answers the comment or // entry the first attempt made instead of appending again; the answer is // recorded in the write's own transaction. On the item-update route a // key is accepted only with an append (and source); any other member // alongside it is refused 400. `idempotency_target_gone` (410: the item // or comment the key wrote has since been deleted) is now a structured // code on both transports, with its own hint; at 0.91 it reached MCP as // a generic error. No name, enum or param moved. // // 0.91 — TASK-3585, create. ADDITIVE bump on the v0.53 grounds: // `pad_item.action=create` accepts `op_key` too. Retrying the SAME // create with the same key answers the item the first attempt created // (as it stands now) instead of creating a second one; the answer is // recorded in the create's own transaction. A key reused for a // different create is refused `idempotency_key_reused`; one still in // flight answers `idempotency_in_flight` (both codes already exist, and // are structured on both transports for any route). A replay whose item // has since been deleted answers HTTP 410 `idempotency_target_gone`, // which is NOT on the structured allow-list and reaches MCP as a generic // error. No name, enum or param moved. // // 0.90 — TASK-3585. ADDITIVE bump on the v0.53 grounds: // `pad_item.action=patch` gains `op_key`, an idempotency key: retrying // the SAME request with the same key returns the first answer (marked by // the Idempotent-Replayed header on the remote transport) instead of // applying again. New structured codes on both transports: // `idempotency_key_reused` (422: the key was used for a different // request), `idempotency_in_flight` (409: retry with the same key), // `patch_outcome_unknown` (409: an earlier attempt reached an editor // that never confirmed it and is gone; details carry seq and the diff // since; nothing is re-applied). // // 0.89 — PLAN-3578 step 3. ADDITIVE bump on the v0.53 grounds: // `pad_item.action=patch` gains `undo` (a patch_id from an earlier patch // answer), which inverts that patch, anchored on the text around each // edit as it landed, and answers like any patch plus `undo_of`. It is // refused, writing nothing, when that text has changed since // (patch_anchor_missing / patch_anchor_ambiguous). New structured code on // both transports: `patch_not_found` (404): the id is not a patch on this // item. // // 0.88 — PLAN-3578 step 2. BEHAVIOR bump on the v0.84 grounds (a // call's outcome changes; no name, enum or param moved): // `pad_item.action=patch` on an item open in a browser editor used to be // refused `patch_room_open`; now a patch-capable editor applies it to its // live document as a narrowed replace and the answer adds // `content_state: "applied_pending_flush"`. New structured code on both // transports: `patch_unstable_roundtrip` (`details.line`, `details.reason`): // applying it there would change content outside the edit. // `patch_room_open` now means only an editor that cannot apply patches // (an older tab) holds the item. // // 0.87 — PLAN-3578 step 1. ADDITIVE bump on the v0.38 grounds (a new // action is the contract change): `pad_item.action=patch {ref, edits: // [{old, new, line_hint?}], expected_seq?}` replaces exact text in an // item's body. Each old must occur exactly once; edits apply in order, // all or nothing; the answer is {ref, seq, patch_id, version_id, // rebased, intervening?, diff}. A moved expected_seq does not refuse: // the patch applies and reports rebased with the intervening writes // ({from_seq, to_seq, changes, complete, truncated}). New structured // codes on both transports: patch_anchor_missing, // patch_anchor_ambiguous (details.lines), patch_room_open (the item is // open in an editor; not supported yet). `pad_item.action=get` gains // `lines` ("A-B"), which answers numbered body lines with seq and // content_state instead of the item. The catalog gains the // `array` param type for `edits`. // // 0.86 — PLAN-3555 phase 1. ADDITIVE bump on the v0.53 grounds: a new // param and new response members, and no call that was accepted is // refused. `pad_item.action=update` gains `replace` (bool), the // deliberate-overwrite opt-out for a content write that carries no // version token. It is refused with `expected_seq` or // `expected_updated_at`, and without `content`, on both transports. A // content write with neither `replace` nor a token now carries // `warnings.unguarded_content_write: true` (omitempty) on both // transports; a later release refuses it (phase 2, a BEHAVIOR bump). A // seq `update_conflict` on a content write gains additive details // (`content_write`, `changed_at`, `changed_by_kind`), a message naming // both seqs that says not to resend the body, and the hint // UpdateConflictContentHint on BOTH transports: stdio carried no hint // for this code before, and the remote one said "retry". The code and // every other conflict are unchanged. // // 0.85 — BUG-3544. BEHAVIOR bump on the v0.58 / v0.45 grounds (an // error that read as transient now says what it is; no name, enum or // param moved): `pad_workspace.action=invite` on an instance with no // account yet (the setup window, reachable over local stdio, where // requests run without auth) answers 409 `setup_required`, "This Pad // instance must be initialized with pad auth setup", and writes // nothing. It answered 500 (`server_error`): the invitation insert // failed its inviter foreign key. Retrying cannot help; `pad auth // setup` does. The remote transport cannot reach the setup window. // // 0.84 — BUG-3542. BEHAVIOR bump on the v0.39 / v0.43 grounds (a // write door refuses a call it used to accept; no name, enum or param // moved): a `pad_item` update that sets content on an item open in a // browser tab is answered 409 `content_not_applied` with // `details.apply_reason: "unconfirmed_edits"` when that tab still holds // typing the server has not stored. The v0.39 check reads the op-log // when the PATCH arrives and cannot see typing still on the wire; the // tab can, and refuses the apply instead of replacing it. Tokenless // writes are refused too, by ruling. Other fields in the call still // land, and the content is not applied. Wait a few seconds, re-read, and // resend the content; `overwrite_pending_edits: true` is the only way // to replace the typing. // // 0.83 — TASK-3539. BEHAVIOR bump on the v0.43 / v0.48 grounds (a // write door refuses a call it used to accept; no name, enum or param // moved): `pad_collection` create and update REFUSE a `schema` in which // a field has no key, two fields share a key, or a select field lists // an empty option or the same option twice, with 400 `validation_error` // naming it (`field key "status" is defined more than once`, `field // "status" lists the option "open" more than once`). Every client // renders a collection's fields and options as lists keyed by those // values, and one repeat blanked the view (BUG-3538). A WRITE check // only: a schema already stored with a repeat still reads, and an // update that sends no schema still applies. // // 0.82 — PLAN-3535 PR 2. BEHAVIOR bump on the v0.49 grounds (a // default call's content changes; no name, enum or param moved): the // items of a REFERENCE collection (`tracks_work: false`; system // Conventions and Playbooks, and new template Docs) stop counting as // open work on every agent surface. `pad_project` next / ready / stale / // standup / changelog / dashboard and the bootstrap dashboard leave them // out (active items, attention, suggestions, by_role, active-plan // progress); report with no `collections` leaves the collection out, as // it already did a system one; item progress and the open-children close // guard (`open_children`) skip them; the attention decision set is not // asked about them. Sidebar counts, item lists, search and links are // unchanged. A collection without the key still counts as work. // // 0.81 — PLAN-3535 PR 1. ADDITIVE: `pad_collection` create and update // take a `tracks_work` boolean. false makes the collection REFERENCE // material (docs, conventions, playbooks): its items will stop counting // toward parent progress, close blocking, Insights and open-work counts // (PR 2 and 3 apply that; this version only adds the switch). On update // it sets that one settings key server-side and leaves the rest alone. // Omitted, create picks work iff the done field can finish, and update // leaves it unchanged. Over stdio it reaches `pad collection create|update // --tracks-work=`, a STRING flag so false survives (a bool flag is // emitted only when true). An older server ignores the member; the CLI // checks the response and says so. // // 0.80 — BUG-3533. ADDITIVE bump on the v0.62 grounds (a remote door // starts doing what the catalog always promised; stdio unchanged): // `pad_item.action=import` over REMOTE /mcp now imports the artifact. // It answered `validation_failed` "item import: artifact is required" // on every call, because the catalog action dropped `artifact` from the // input the HTTP dispatcher reads. No name, enum or param moved. // // 0.79 — TASK-2863. BEHAVIOR bump, ADDITIVE in effect: a read-only // grant (`read` / `pad:read`, PAT or OAuth) may now run a playbook. // `pad_playbook.action=run` used to be refused for one, because run is // a POST and read scopes allowed only GET/HEAD/OPTIONS, although run // stores nothing. Over remote /mcp and stdio alike (the CLI's own // `pad playbook run` with a read PAT) it now returns the bound body. // It is the ONE POST a read grant may make: an exact-path allowlist in // tokenScopeAllows whose variable segments must be plain names. // `pad_playbook.action=match` and every write stay refused. No name, // enum or param moved. // // 0.78 — TASK-2308. BEHAVIOR bump on the v0.45 / v0.42 grounds: over // REMOTE /mcp, an OAuth connection whose grant cannot write (pad:read) // that calls a WRITE action (any catalog action not in readOnlyActions), // in a single tools/call request of up to 1 MiB, is answered HTTP 403 with `WWW-Authenticate: Bearer realm="pad", // error="insufficient_scope", error_description=..., scope="pad:read // pad:write", resource_metadata=...` before dispatch, where it got a // `permission_denied` tool result. That challenge is what lets a client // (Claude Code among them) re-authorize with the wider grant. Nothing // that used to succeed is refused: every write action was already // refused for a read token, and pad_item.import fails over HTTP for // every token (BUG-3533) (pinned by // TestScopePopulation_WriteActionsAreRefusedForReadTokens). A read-scoped // PAT, which cannot re-authorize, keeps the tool error, as does a larger // request (the server does not read past the bound), and the ChatGPT // mount keeps its own tool-result challenge. The 401 challenge also // gains `scope="pad:read pad:write"` ahead of resource_metadata. Stdio // is unchanged. No name, enum or param moved. // // 0.77 — BUG-2772 (lead ruling; IDEA-2791 Tier B). BEHAVIOR bump on the // v0.76 / v0.49 grounds: a write over REMOTE /mcp is now recorded as an // AGENT write (created_by / actor "agent"), named by the client's declared // MCP clientInfo.name ("claude-code"), where it was recorded as the // human. The name comes from a modern request's own _meta clientInfo, // else the clientInfo the session sent at initialize (remembered per // instance by the Mcp-Session-Id pad issued and the account that // presented it, 30-minute idle TTL), else // none: an unnamed agent write, never the human and never a guessed name. // The default lease holder of a remote call is that name (or // "mcp-client") plus a short id from the session, e.g. // "claude-code#3f9a12c4", so two connections of one client do not share a // lease; a reconnect gets a new session id, so the agent's own earlier // lease reads as foreign until it expires (at most 15 minutes by // default). Stdio is unchanged. No name, enum or param moved. // BUG-3496 rides with it: a claim refused because another holder has // the lease reaches the caller as `lease_held` (details ref, holder, // acquired_at, expires_at, and a hint not to retry before expiry) on // both transports, where remote answered `conflict` and stdio // `server_error`. // // 0.76 — TASK-1809 (lead ruling). BEHAVIOR bump on the v0.70 / v0.49 // grounds: no tool name, action enum or param shape moved. Two // changes, one identity: // // A `pad_item.action=claim` / `release` with no `holder` now holds the // lease under the request's agent name (X-Pad-Agent, which the CLI sets // from the session registry, .pad.toml, $PAD_AGENT or the detected // runtime), falling back to the account's email as before. It used to // be the email alone, so several agents on ONE account (the common // local case) silently shared a lease: measured, a second agent's // claim answered 200 by refreshing the first one's lease, and both // believed they had it. Now the second claim gets 409 `lease_held` // naming the first agent, and cannot release it. The lease stays bound // to the account (BUG-3341), so cross-account behaviour is unchanged. // Local stdio reaches this through the CLI's header; remote /mcp sends // no agent name, so there the default is still the email (TASK-3495). // // `pad_project.action=next` / `ready` (and the dashboard's // suggested_next they are cut from) leave out an item whose LIVE lease // a claim by the caller would lose, with that same default identity // and the claim's own predicate. The filter runs before the cap of // three, so a leased item frees its slot. Reminders are not filtered. // A caller that claimed under a custom `holder` sees those items left // out of its own next / ready (stated in the param and CLI help). // // A lease taken before the upgrade under the email default reads as // another holder's to the same agent until it expires (at most its TTL, // 15 minutes by default). // // 0.75 — BUG-3480 (lead ruling). BEHAVIOR bump on the v0.43 / v0.29 // grounds: `pad_item.action=list` on both transports REFUSES a filter // key (a `field` entry, or `priority` on a collection without one) // that no schema in scope declares and no item in scope stores, with // 400 / `validation_failed` and a message starting "invalid list // filter". It used to answer an empty list, indistinguishable from "no // rows match". A key that is not a field key (only letters, digits, // `_`, `-`) is refused too; the store used to DROP it and answer the // unfiltered list. Scope: on a collection list, that collection's // schema; on the workspace-wide list, any collection the caller can // see. A key no schema declares but items in scope store is still // honoured (the BUG-2850 census), and the REST response names it in // `X-Pad-Undeclared-Filter-Keys`, which the CLI prints to stderr; the // MCP result does not carry it. No name, enum or param moved. // // 0.74 — TASK-2188 (lead ruling). ADDITIVE bump on the v0.67 grounds // (an omitempty warning on a write result): `pad_collection.action=update` // answers, on both transports, with `warnings.orphaned` // [{field, option?, items}] when its schema removed a field, or removed an // option from a field that stays a select or multi_select, that live items // still hold. Read after the commit, so a value an option rename migrated // is not listed. Nothing is refused and nothing else moved; an update that // orphans nothing reads byte-identical to before. 0.73 is BUG-3480's; if // this lands first, that one takes 0.75 and 0.73 stays a gap. // // 0.72 — TASK-2189 (lead ruling). ADDITIVE bump on the v0.8 / v0.13 // grounds (existing CLI verbs wired onto the catalog): `pad_collection` // gains `list-archived` (read-only: the workspace's archived // collections with slug, id, prefix, item_count and archived_at) and // `restore` (owner-only: un-archive a collection, named by `slug` or // id, with its items, which the archive never deleted). The `delete` // description no longer says "No restore endpoint exists": an agent's // archive is now recoverable by an agent. Nothing existing moved. // // 0.71 — TASK-3462 U3a (lead ruling). ADDITIVE bump on the v0.63 / // v0.53 grounds: the bootstrap every bootstrap door returns // (`pad_set_workspace`'s embed, `pad_meta.action=bootstrap`, the // `pad://workspace/{ws}/bootstrap` resource) gains an `omitempty` // `builtin_updates` count: the conventions and playbooks Pad ships whose // library text changed since the workspace's copy was made (unedited, or // edited too), among the items the caller sees in full. The server // instructions gain a section telling the agent to mention it once and // never apply an update itself. No name, enum or param moved; a // workspace with nothing on offer reads byte-identical to before. // // 0.70 — BUG-3453 (lead ruling). BEHAVIOR bump on the v0.49 grounds // (a default call's content changed, with no name, enum or param // moved): `pad_project.action=next` and `ready`, which return the // dashboard's suggested-next list, now fill any slot the existing tiers // leave empty with ordinary open items, from collections that are not // system collections and declare `priority` or `severity`. Open means // the collection's done field is not terminal and the item is not in // progress, so an initial status like `new` counts. They rank by // priority (else severity), then age, below every item admitted before, // so a workspace that filled three slots gets the same list; one that // got "nothing ready" for a moved-in backlog now gets it. The `ready` // description is corrected: it is the same capped top three as `next`, // not the full backlog. // // 0.69 — TASK-2314. BEHAVIOR bump on the v0.42 / v0.45 grounds (an // error CODE a caller receives changed, with no name, enum or param // moved): on the REMOTE transport, every route that needs a workspace // and lacks one (a routeTable mapper, a special route, a link route, the // `assign` / `role` resolvers) answers `no_workspace` with // `available_workspaces` and a hint, the envelope stdio has always // given, where it answered `validation_failed` "workspace is required". // The single-workspace default (TASK-1076) now runs before the `assign` // / `role` resolvers, so such a caller who omits `workspace` gets them // resolved in that workspace instead of a refusal. The no_workspace // hint is never empty: with no list it names `pad_workspace // action=list`. Stdio is unchanged except for that fallback hint. // // 0.68 — BUG-3448 (lead ruling). BEHAVIOR bump on the v0.62 / v0.64 // grounds (an output shape a consumer reads changed, with no name, enum // or param moved): the item resource `pad://workspace/{ws}/items/{ref}` // renders a structured field (a json field's object or array) as compact // JSON instead of Go's map syntax (`[map[name:target …]]`), keeps the // digits of a number above 2^53, and renders a JSON null as `null`. The // agent projection (`pad_item` get with the agent shape) no longer rounds // a number above 2^53 when it drops the hydrated keys. Scalar fields // render exactly as before. // // 0.67 — BUG-3446 (lead ruling, day 89). ADDITIVE bump on the v0.62 / // v0.64 grounds: in a SYSTEM conventions or playbooks collection, a // create whose `trigger` / `scope` is a word the convention or playbook // LIBRARY itself uses now ADDS that word to the select's options instead // of being refused, so `pad_library.activate` and `pad_item.action=create` // succeed in a `blank` workspace, where every triggered library // convention used to be refused. The create's result gains an // `omitempty` `warnings.options_added` (keyed by field) on both // transports. A word the library does not use is refused exactly as // before, so the options grow only by library words. Absent when nothing // was added, so a consumer that ignores it is unaffected. No name, enum // or param moved. // // 0.66 — BUG-2136 (lead ruling, day 87). BEHAVIOR bump on the v0.52 / // v0.43 grounds: `pad_workspace.action=invite` no longer ADDS an existing // account to the workspace. Every invite now creates a pending // invitation that the invitee accepts or declines (in the web app, or // with `pad workspace invitations` / `accept` / `decline`, CLI-only by // ruling), on both transports. The result loses the direct-add shape // (`added`, `user_id`, `name`) and is always `{invited, code, email, // role, join_url?}`, so it also stops saying whether the address has an // account. A re-invite of an address with a pending invitation replaces // it (a fresh code, role and 14-day expiry; the old code stops working). // An agent that invited a user and then acted on them as a member must // now wait for their acceptance. No name, enum or param moved. // // 0.65 — TASK-3413 (SPEC-6 U9c). ADDITIVE bump on the v0.53 grounds: // an item an installed APP created, a comment an app wrote and a version // row an app's write made gain `omitempty` `via_app` (the install id) and // `via_app_name` (the app's bot display name, else its origin; it // survives uninstall), on both transports. They reach `pad_item` get, // list (summary and full), comments and history (summary rows too, both // projections pinned together). On an item they name the CREATOR's app // (items.created_via_app), never the last app writer. Absent on every // row a person or an agent wrote, so a consumer that ignores them is // unaffected. No name, enum or param moved. // // 0.64 — BUG-3379. ADDITIVE bump on the v0.53 grounds (bump if and only // if the field reaches a response): a comment and an attachment row a // WORKSPACE IMPORT wrote gain an `omitempty` `imported: true`, on both // transports, and so does an imported VERSION row: it reaches // `pad_item.action=comments`, `pad_attachment.action=list` and // `pad_item.action=history` with `full: true`. History SUMMARY rows are // projected to fixed keys and do not carry it, nor does // `pad_attachment.action=show` or the attachment resource, which build // their metadata from HEAD headers. Import keeps the export's author // strings and uploader ids verbatim (BUG-3372 ruling (1)); the flag says // they were not verified here. Absent on every other row, so a consumer // that ignores it is unaffected. Rows imported before this shipped are // not marked: nothing stored distinguishes them. // // 0.63 — TASK-3321 (lead-ruled as its own PR). ADDITIVE bump on the // v0.53 / v0.28 grounds: every `pad_item.action=history` summary row, // on both transports, gains an `omitempty` `actor_name`, the display // name of the user who made the change. Rows could say only // `created_by: "user"` or `"agent"`, although the server joins the name // on every read. Empty for rows with no user (recovery writes, versions // from before migration 103). No name, enum or param moved. // // Post-0.37, deliberately NO bump (BUG-3098): `workspace_member_limit` // joins the structured 403 allow-list, as ErrWorkspaceMemberLimit with // its own hint (not the plan-limit "upgrade" hint). The server emits it // when an INVITATION ACCEPT is refused by the workspace's member cap. // No catalog action reaches either accept door today, so no caller of // this surface can receive it; the entry exists so a future action does // not collapse it to permission_denied. When an action that can reach // it is added, that addition is the contract change and owns the bump. const ToolSurfaceVersion = "0.99" // MetaVersionURI is the canonical URI of the queryable version document. // Lives outside the pad://workspace/{ws}/... namespace because it's a // server-wide attribute, not a workspace-scoped resource. const MetaVersionURI = "pad://_meta/version" // The MCP wire protocol revision this server speaks isn't a constant // owned by pad — it's whatever mcp-go's `LATEST_PROTOCOL_VERSION` // resolves to at build time, since that's what NewMCPServer will // negotiate with clients that request the latest. The meta resource // reads it dynamically (see meta.go) so the value never drifts from // what the library actually advertises. // experimentalCapabilityKey is the JSON object key under // capabilities.experimental that carries the cmdhelp tier in the // initialize handshake. Namespaced so other servers' experimental // capabilities don't collide. const experimentalCapabilityKey = "padCmdhelp" // experimentalToolSurfaceKey is the JSON object key under // capabilities.experimental that carries the MCP tool-catalog tier in // the initialize handshake. Distinct from experimentalCapabilityKey so // the cmdhelp and tool-surface contracts can version independently. const experimentalToolSurfaceKey = "padToolSurface"