mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 06:19:49 +00:00
ed89604ad8
* fix(mcp): a refused call names its cause on both transports (BUG-3864) A 403 from one of the shared gates now carries details.reason (role, token_scope, no_access, workspace_not_authorized), with code and message unchanged. MCP builds one permission_denied envelope from it on both transports, with a hint that says what to do next. It used to lead with "Insufficient role" for every 403, so a bot with a read-only API token was told to ask for a role. An unverified Cloud email is reported as the server's own email_not_verified code. Stdio gets the same envelope from a structured marker the CLI root writes for a 403 coded forbidden, permission_denied or email_not_verified. Before it, the token-scope and unverified-email refusals said server_error there. ToolSurfaceVersion 0.99 -> 0.100 (behavior). Claude-Session: https://claude.ai/code/session_01MPeShv3DBfM9C94PTvMnqi * fix(server): the inline workspace-role gates name their reason too (BUG-3864) The bulk endpoint, the owner-only member doors and the owner-only app install and upgrade doors say role, or no_access for a guest. Claude-Session: https://claude.ai/code/session_01MPeShv3DBfM9C94PTvMnqi * fix(mcp): the permission hint is the same on both transports (BUG-3864) The remote hint appended the route, which stdio has no way to know, so one refusal reached the two transports with different hints. The tests compared prefixes only and missed it; both now assert the transports agree exactly. Claude-Session: https://claude.ai/code/session_01MPeShv3DBfM9C94PTvMnqi * fix(server): no role hint for a caller with no role to change (BUG-3864) requireMinRole and the grants list said role for a guest, who holds grants rather than a role, and for a caller with no workspace role at all. Both now go through roleGateReason, which answers no_access for either. Claude-Session: https://claude.ai/code/session_01MPeShv3DBfM9C94PTvMnqi
2235 lines
134 KiB
Go
2235 lines
134 KiB
Go
// 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 <agent>` 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 <tmpfile>`. 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=<uuid>` 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=<uuid>` 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
|
||
// `<name>_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.<action>: 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: <message>` 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>" (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.100 — BUG-3864. BEHAVIOR bump on the 0.45 grounds; no name, enum
|
||
// or param shape changed. A refused call names its cause, on BOTH
|
||
// transports. `permission_denied` gains `details.reason` when the server
|
||
// says which gate refused (`role`, `token_scope`, `no_access`,
|
||
// `workspace_not_authorized`), and a hint per reason that says what to
|
||
// do next. With no reason, or one this build does not know, the hint
|
||
// leads with the server's message and claims no cause. The old hint led
|
||
// with "Insufficient role" for every 403, so a bot with a read-only API
|
||
// token was told to ask for a role. New code `email_not_verified` (the
|
||
// server's own, on every door that enforces it) replaces the
|
||
// `permission_denied` the remote gate reported for an unverified Cloud
|
||
// email. REMOTE: classifyHTTPStatus reads the body's reason, and the
|
||
// dispatcher's own scope and email gates build the same envelopes.
|
||
// STDIO: the CLI root writes a structured marker for a 403 coded
|
||
// forbidden, permission_denied or email_not_verified
|
||
// (cli.WritePermissionDeniedError); before it, a refusal whose message
|
||
// lacked "forbidden" (token scope, unverified email) said server_error.
|
||
// Both lifts build the hint in permissionDeniedPayload. "0.100" follows
|
||
// "0.99": the version is an opaque dotted string, compared by no code.
|
||
//
|
||
// 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 <ref> --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<object>` 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=<bool>`, 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.100"
|
||
|
||
// 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"
|