POST /workspaces/{ws}/items/{ref}/patch {edits:[{old,new,line_hint?}],
expected_seq?} replaces exact text in the body. It runs through updateItem,
so it takes every PATCH guard; the body is computed inside the store
transaction from the row it replaces. Each old must occur exactly once;
edits apply in order, all or nothing; every patch writes a version. The
answer carries a unified diff of what changed.
expected_seq does not refuse: a moved seq is applied over when the
anchors still match, and answered rebased with the intervening writes,
read from a seq chain stamped into item activity entries ({prev_seq, seq,
version_id}). complete is decided at runtime, so any write that left no
stamp reads incomplete; a guard test classifies every seq-moving store
statement. Refusals write nothing: patch_anchor_missing,
patch_anchor_ambiguous, patch_room_open (an open editor; step 2).
CLI: pad item patch, pad item show --lines. MCP 0.87 (additive):
pad_item.patch, get lines, three allow-listed codes. The timeline treats
the chain and patch keys as bookkeeping, not a change.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
Phase 1 of the ruling that agent content writes carry the version the agent
read. Nothing is refused yet; phase 2 (428 at the agent doors) follows a week
of these counts.
- server: `replace` request member; pad_content_writes_unguarded_total
{door,kind}; warnings.unguarded_content_write on an agent door's unguarded
content write; a seq update_conflict on a content write (applier route
included) carries content_write, changed_at, changed_by_kind and a
"do not resend your body" message.
- CLI: `pad item update --replace` (refused with a token or without a body);
a stderr warning on an unguarded body write; the merge line on conflict.
- MCP 0.86 (additive): pad_item.update `replace`; the content-conflict hint
on both transports; the update description leads with read, edit, write
with the seq.
- Docs and prompts: SKILL.md (both copies), instructions.md, api.md,
mcp.md, deployment.md, README, update help, plan and onboard playbooks.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
An invitation records who sent it. On a fresh install with no account yet,
requests run without auth, so pad workspace invite reached CreateInvitation
with no inviter, failed the inviter foreign key, and answered 500, which
reads as transient and retryable. handleInviteMember now refuses a request
with no resolved user: 409 setup_required with sign-in's own message
("This Pad instance must be initialized with pad auth setup") when no account
exists, else 401. Nothing is written.
ToolSurfaceVersion 0.85, a BEHAVIOR bump on the v0.58 / v0.45 grounds:
pad_workspace.action=invite over local stdio now gets that refusal where it
got server_error. docs/api.md notes it on the invite route.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
A self-hosted boot converts free and empty plans to `self-hosted`, which has
no limits. Nothing recorded which kind of instance owned a database, so one
non-cloud boot of a Pad Cloud database (a missing PAD_MODE, a restore tested
locally, a debug boot) made every free user unlimited for good: a later
cloud boot fills only empty plans.
- Every cloud-mode boot marks its database cloud-owned
(platform_settings.instance_mode = cloud), sticky and before its backfill.
- A database with no marker counts as cloud-owned when any user carries
Pad Cloud billing state (a stripe plan source or a stripe customer id).
- The non-cloud boot's conversion (store.BackfillSelfHostedPlans) checks
ownership in its own transaction and converts nothing on a cloud-owned
database. The boot logs an error and continues: a debug or restore boot is
not an outage, and free users keep free-plan limits.
- `pad db release-cloud [--force]`, on the server host, is the deliberate way
to move a Cloud database to self-host. It refuses while billing state
remains unless forced; a later cloud boot marks the database again.
The plan step of `pad server start` moves into bootPlanBackfill so it can be
tested as the server runs it. docs/backup.md documents the behaviour.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* docs(cli): an example for every pad item leaf command (TASK-2865, part 2: item)
One common invocation per leaf, for agents first: the shape a caller
would actually type, with TASK-5-style refs. The drift validator checks
every one against the live tree. Each was also run against a throwaway
server with refs mapped to real items; the only errors were
preconditions (no signed-in user on a fresh install, a link the
previous example had made, a reminder that had not fired).
* docs(cli): an example for every pad workspace leaf command (TASK-2865, part 2: workspace)
Validated by the drift validator. Run against a throwaway server: create,
list, link, members, storage, export, import, deleted and switch succeed;
invitations, audit-log and init refuse for want of a signed-in user or an
initialized instance; invite surfaced BUG-3544 (a 500 on a fresh install).
* docs(cli): examples for every pad project, auth and session leaf (TASK-2865, part 2)
Validated by the drift validator, and run against a throwaway server
where non-interactive: every project read, session list/prune/status and
auth configure succeed (configure needs --mode without a terminal, so
the example names it); whoami answers 'not logged in' on a fresh client.
login, setup, the 2fa pair and reconcile (needs gh) were not run.
* docs(cli): examples for the remaining leaf commands (TASK-2865, part 2)
agent, bootstrap, collection, db, github unlink, mcp, playbook, push,
role, server, tag, token, watch and webhook. Validated by the drift
validator; run against a throwaway server where safe: collection
create/delete/restore, role create/list/delete, playbook list/show/run,
tag/webhook list, bootstrap, agent guide/status, mcp status, server info,
db scan-nul and db backup succeed; watch list needs a signed-in user.
* test(cli): every leaf command carries an example (TASK-2865)
TestRealPadTree_EveryLeafHasAnExample walks the real tree; a group node
is exempt. On part 1's tree it lists the 114 leaves this part covers.
38 commands carried an 'Examples:' block inside Long. They now set
Example, which cobra renders under its own heading in --help and cmdhelp
reads first.
cmdhelp's Example parser now drops a trailing '# comment' the way the
Long parser always did, so the move changes no emitted example: a
before/after diff of 'pad help --format json' over all 169 commands
shows identical examples, identical args/flags/summary, and descriptions
that differ only by the removed block.
TestRealPadTree_ExamplesLiveInTheExampleField keeps Long free of example
blocks. Examples for the leaf commands that have none follow per group.
A content PATCH routed to an open tab (the designated applier) replaced
typing that was still on the wire. The BUG-3133 pending-edits check reads
the op-log when the PATCH arrives and cannot see it.
The server now marks the applier_request `guarded` unless the caller sent
overwrite_pending_edits. A tab whose edits may be missing
(WsProvider.editsMayBeMissing: unsent, catch-up owed, or sent without a
barrier ack) answers applier_refuse instead of applying. The server takes
that as final (no re-election) and the PATCH answers 409
content_not_applied with apply_reason unconfirmed_edits and content_landed
false; other fields land, as on every content_not_applied. Tokenless writes
are refused too, by ruling; overwrite_pending_edits is the only escape.
ToolSurfaceVersion 0.84 (BEHAVIOR bump). docs/api.md and docs/mcp.md updated.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* feat(ci): an advisory dependency-age check (cmd/depages) (TASK-1391)
Companion to Dependabot's cooldown (TASK-1390), which holds a release for 7
days before PROPOSING it but cannot see a version added by hand. cmd/depages
dates every Go module (go.sum), npm package (web/package-lock.json, registry
packages) and action pin (`uses: owner/repo@<sha>`) a change adds, from the Go
module proxy, the npm packument and the GitHub commits API, and fails when
one is younger than --days and not allowed in .github/dep-age-allow.txt
(which requires a reason per line). --base <ref> checks only what is new
since the ref; a dependency that cannot be dated is a warning, not a failure.
CI runs it on pull requests as "Dependency age (advisory)", against the
merge commit's first parent. NOT a required check (lead ruling): red means
wait or allow with a reason.
Rewritten rather than promoted: the /tmp scanner the task names is gone. Its
edge cases are kept (pseudo-versions included, scoped npm names, sub-actions
sharing a repo SHA, the full packument for per-version times). A real run
against main as of 2026-09-01 dated 185 dependencies and flagged one:
golang.org/x/net v0.60.0, #1922's security bump, the case the allowlist is for.
* fix(ci): depages covers aliases, composite actions and lockfile versions; the token stays on GitHub (TASK-1391)
Codex round 1:
- An npm alias is checked as the package it installs (the entry's name).
- A lockfile older than v2 is refused instead of read as empty.
- Every YAML file under .github is scanned, so a local composite action's
uses: pins count too. docker:// images are out of scope and documented.
- GH_TOKEN is sent only to api.github.com, whatever DEPAGES_GITHUB_API says.
- The base-diff test now covers a go.sum module, a bumped npm package and a
composite action's new pin in one change.
* fix(ci): depages scans action.yml anywhere in the repo; its README tables the coverage (TASK-1391)
Codex round 2: a local composite action can live outside .github. Every
action.yml / action.yaml in the repository is scanned too (node_modules and
.git skipped). Not changed, with the reasons in the README's coverage table:
docker:// images (no SHA, no publish time) and dating a pin by its commit
(the threat is fresh code, and an old commit is old code).
* feat(ci): depages prints the allowlist line to paste for each young version (TASK-1391)
Lead, on the first real run flagging #1922's x/net security bump: the job's
output now says the check is advisory and prints, per flagged version, the
exact .github/dep-age-allow.txt line with a <reason> placeholder.
* test(models): classify cmd/depages' getJSON in the raw-decode guard (TASK-1391)
CI: TestReservedFieldDecodeSitesAreClassified found cmd/depages/main.go's
getJSON decoding into an interface. It decodes registry and GitHub API answers
into their own structs, never a collection definition; allow-listed with that
reason, and the receipt count goes 26 -> 27.
* fix(server): refuse a collection schema that repeats or empties a field key or option (TASK-3539, PR B)
Lead ruling: a repeated or empty field key or select option is a broken
schema, and the web renders fields and options as lists keyed by them, so
one repeat blanked the view (BUG-3538). PR A made the web render such a
schema; this stops new ones being written.
- Collection create and update refuse, with 400 validation_error naming
it, 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
(models.ValidateSchemaKeysAndOptions, beside ValidateAbandonedOptions).
WRITE check only: a stored schema with a repeat still reads, and an update
that sends no schema still applies. Live census on :7777: 0 of 213
collections affected.
- Dynamic Client Registration registers a repeated redirect URI once (it is
unauthenticated, and the connected-apps page lists URIs keyed by value).
- ListWorkspaceTags groups tags as TEXT: on SQLite json_each kept [1] and
["1"] apart while both scanned to "1".
- Pins that convention commands and surfaces are deduped on read: they are
already (normalizeItemConventionMetadata); the census verdict that said
otherwise was wrong.
Not done, deliberately: refusing repeated app-manifest event names or
companion field keys. appmanifest.Parse also re-reads a STORED manifest on
upgrade, so a new refusal there would fail a read of an installed app; PR A
made both web lists render repeats.
ToolSurfaceVersion 0.82 -> 0.83: pad_collection create/update refuse calls
they used to accept (the v0.43 / v0.48 grounds). docs/api.md states the 400.
* fix(server): hold companion and template schemas to the key/option rule (TASK-3539)
Codex round 1: writers that bypass the collection handlers could still store
a schema that repeats or empties a key or option.
- An app's companion collection schemas are checked in the install/upgrade
preview, on the NEW manifest beside the reserved-key check, and refused
422 invalid_manifest. Not in appmanifest.Parse, which also re-reads an
installed app's stored manifest.
- Every template and default collection schema the repo ships is checked by
a test (they seed through the store, not the handlers).
- The legacy-schema test also reads and creates items in a collection stored
with a repeat.
Workspace and artifact import, and the library option widening (which
already dedupes), are unchanged: refuse on write only.
* docs(mcp): the v0.83 entry in docs/mcp.md's version history (TASK-3539)
Codex round 2: the current-version labels said v0.83, the history began at
v0.82.
* docs(mcp): docs/mcp.md's intro names the current catalog, v0.83 (TASK-3539)
Codex round 3: the intro still said v0.81, stale since before this change.
* fix(server): the option check covers select and multi_select only (TASK-3539)
Codex round 4: it refused empty or repeated options on every field type, but
only a select or multi_select's options are a rendered list. Another type can
carry a legacy options list (the old field DSL put a third part there), and
refusing it would block an unrelated schema save. Tested both ways.
* test(collections): the template schema check is independent of the validator (TASK-3539)
Codex round 5: calling ValidateSchemaKeysAndOptions from the template test was
circular. It now uses its own check. Also records, at the write, why the
library option widening is not held to the validator: it only appends missing
words (never a repeat), and refusing would fail an item create over a legacy
schema.
* fix(mcp): free tools/list headroom by reflowing and condensing descriptions (TASK-3536)
tools/list was 55,291 of its 55,296-byte budget (54 KiB, TestMCPPromptBudgets),
so the next parameter anyone added would fail it. TASK-3489's ruling holds: a
fixed ceiling, trimmed rather than raised. Now 50,010 bytes (9.6% headroom).
- U1, whitespace only (55,291 -> 52,748): tool descriptions are reflowed for
the wire by compactToolDescription. Indented continuation lines join the
line above; Required:/Optional: keep their own line at a 4-space indent;
action headers and column-0 lines are untouched. The Go source keeps its
hand-wrapped layout. A test pins that every word survives in order and
every action keeps its header. The pad_meta tool-surface dump serves the
same wire form.
- U2: the workspace param (the same 369 chars on 10 tools) is one sentence
that keeps the operative rule (explicit wins; pass it on every call to a
remote server; local falls back to pad_set_workspace, then .pad.toml).
- U3: pad_item update's content_outcome and expected_seq paragraphs are
condensed to their rules: re-read later, never re-send; prefer
expected_seq, whose conflict carries actual_seq.
The rules stay in the descriptions, not only in the server instructions,
because a client may never surface instructions to the model. The ChatGPT
mount has its own catalog and is unchanged. Wording only: no
ToolSurfaceVersion bump.
* test(mcp): regenerate the wire goldens for the reflowed descriptions (TASK-3536)
stdio and remote tools/list: 20 description strings change, nothing else
(compared with descriptions stripped: identical). The ChatGPT mount's
goldens do not move.
* fix(mcp): the reflow keeps lists, the dump keeps its source text (TASK-3536)
Codex round 1:
- A bullet, a numbered item or a one-word label ("Constraints:") now keeps
its own line (a list item at a 6-space indent); only a true continuation
joins the line above. pad_collection's delete constraints read as a list
again. A test pins it for every catalog description, and the mutant
dropping the list branch fails it.
- pad_meta's tool-surface dump serves the SOURCE text again: it is the input
for generated docs, which want the structure. tools/list alone is reflowed.
- pad_item update keeps the read rule U3 had dropped: re-read with get or a
full list, since a default list carries no content.
tools/list is 50,099 of 55,296 bytes (9.4% headroom). Goldens: stdio and
remote change descriptions only; the ChatGPT mount's are unchanged.
* docs(mcp): pad_item update keeps 'nothing guarantees the save' (TASK-3536)
Codex round 2: the condensed block dropped that the collab save is not
guaranteed, so an agent could wait on it. Restored as one sentence, with the
write-not-row qualifier on the warning.
The items of a collection with tracks_work=false are left out of:
- parent progress (GetItemProgress / GetAllItemProgress SQL, and
childProgressState for the restricted recompute and the dashboard);
- the open-children close guard (all four doors share runOpenChildrenGuard);
- a default report (resolveReportCollections: !tracksWork replaces IsSystem);
- the dashboard's active items, stalled / overdue / orphaned / blocked /
content attention, suggested_next and by_role (store and visible recount),
and so next / ready / stale and the bootstrap dashboard;
- standup and changelog completed work and in-progress, server and CLI;
- the attention decision set's eligibility;
- the admin items_open count.
Sidebar and summary totals, item lists, search, links, the graph, stars and
reminders keep counting every item.
A workspace bundle written before the key existed imports its system
collections as reference, as the migration marked them.
Guards: a three-phase behavioural fixture (work / reference / deleted) over
every excluded surface, each proven sensitive; and a name-based
classification of every lifecycle-helper call site in Go.
MCP ToolSurfaceVersion 0.82 (behaviour bump, v0.49 grounds).
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* feat: a collection can be reference material, not work (PLAN-3535 PR 1)
The switch, with no counting change yet (PR 2 server, PR 3 web).
CollectionSettings.tracks_work: false makes a collection REFERENCE
material; absent means work, today's behaviour, so old rows, old writers
and a client/server skew are unchanged. One Go predicate
(models.CollectionTracksWork) and one TS twin (collectionTracksWork).
- New collections: the creator's value, else system collections are
reference and others are work iff their done field can finish. New
template Docs collections are created as reference (Dave).
- Migration 135 / PG 107: existing Conventions and Playbooks become
reference; everything else, Docs included, stays work.
- Settings are written wholesale, so an update whose settings omit the
key KEEPS the stored value (CarryTracksWork), for every writer.
- A typed top-level `tracks_work` on POST/PATCH sets just that key
server-side. CLI: --tracks-work=true|false (a string flag, so false
survives stdio MCP). MCP: pad_collection tracks_work, v0.81. A
non-boolean is refused 400; the CLI checks the response, since an
older server ignores the member.
- Web: the Display tab's "Track as work" toggle; the editor now starts
its settings write from the STORED settings (buildCollectionSettings),
so keys it does not render (content_template, tracks_work) survive.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(store): carry tracks_work from the locked row; order the SQLite migration's JSON checks (PLAN-3535, codex)
The update now computes the carried settings from the row it re-read under
the write lock, so a concurrent settings write cannot be reverted by a carry
read before the lock. The SQLite migration evaluates json_type/json_extract
only inside a CASE guarded by json_valid, so malformed settings on a system
row cannot abort it.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(models): a collection whose options the default terminals close is work (PLAN-3535)
DefaultTracksWork required declared terminal_options, but every lifecycle
reader falls back to DefaultTerminalStatuses when a done field declares
none, so a `status: open, done` collection finishes items and was being
created as reference. Work now means one of the done field's options is
terminal by the same resolver the readers use. Found by PR 2's store tests,
whose fixture collection is exactly that shape. Also corrects api.md's
MCP version (0.81).
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* feat(collab): the GC tick compacts dormant op-logs and Join admits a covered resume (TASK-3531 U3-U5)
The second half of TASK-3531, behind PAD_OPLOG_COMPACT=on (default off).
U3: with a compactor set, each op-log GC tick first compacts every
dormant, flushed op-log into one Yjs snapshot (#1984's primitives): the
snapshot is built on the materializer worker WITHOUT the item lock, then
swapped in under the lock with no room open, and CompactItemOpLog
re-checks the log is the one it read. The sweep then runs as
PruneSweepKeeping and keeps compacted logs. Anything not compacted (a
failed job, a refused swap, set-aside rows, another schema) is left to
the sweep, which deletes it as before. cmd/pad builds the materializer
before the GC starts and hands it over as the compactor only when
PAD_OPLOG_COMPACT=on and the materializer is enabled.
U4: Join admits a resume cursor the snapshot covers (CompactedResumeCovers,
which requires the snapshot row to exist) instead of force_refreshing it;
the tab replays the snapshot and its own edits merge. A failed check
refreshes.
Telemetry for the default flip (lead ruling): pad_oplog_compactions_total
{outcome}, pad_oplog_compaction_snapshot_bytes_total against
pad_oplog_compaction_markdown_bytes_total, pad_collab_compacted_resumes_total.
U5: server tests (compact then keep, the no-compactor and failed-job
deletes, a room opened mid-job blocks the swap, a covered resume admitted
vs refreshed) and an opt-in e2e (a tab asleep across the sweep merges,
or with compaction off is handed its text back).
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(server): cap compaction jobs per GC tick and keep the deferred items (TASK-3531, codex)
Compaction shares the materializer's single job slot with op-log recovery,
which carries unflushed edits, so a dormant backlog must not hold it for a
whole pass. At most 25 snapshot jobs a tick; items past the cap are kept
from that tick's sweep (deleting them would lose their chance) and
compacted by a later tick. The opt-in e2e's mode=off timeout now names
the likely cause.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* test(materialize): updates built on pre-compaction structs merge onto the snapshot (TASK-3531)
The merge property in CI, not only in the opt-in e2e (lead): over the
corpus, a snapshot of the first k rows followed by the rows after k
materializes to the full document (150 cases). Those later rows depend on
the prefix's structs (lone, 114 of them are pending), so this is the
dependency resolution a deleted-and-reseeded log fails.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(mcp): remote pad_item.import keeps its artifact (BUG-3533)
The catalog action is written for stdio: it spills the artifact to a
temp file and dispatches the CLI's `file` positional. It also dropped
`artifact` from the dispatched input, and the remote transport's
HTTPHandlerDispatcher reads the artifact text from exactly that input,
so every remote import answered "artifact is required". `artifact` now
stays in the input. The CLI never sees it (BuildCLIArgs emits only
declared args and flags; the existing no-`--artifact` test still holds).
The dispatcher's own tests passed `artifact` to it directly, so the
binding was untested. The new test drives the catalog action through
the real dispatcher and server. The scope population test's
knownBrokenOverHTTP exemption for import is removed, so import now
carries the full write-control and read-refusal legs.
* chore(mcp): ToolSurfaceVersion 0.80, remote pad_item.import works (BUG-3533)
* feat(server): a read-scoped token may run a playbook (TASK-2863)
POST /api/v1/workspaces/{ws}/playbooks/{ref}/run is side-effect-free:
it binds the caller's args and returns the body, storing nothing. It is
now the one POST a read or pad:read token may make, through an
exact-path allowlist in tokenScopeAllows (lead ruling, option A).
/playbooks/match stays refused, as do every write and the item
claim/release routes.
Both variable segments must be plain names (RFC 3986 unreserved
characters, not a dot segment). The MCP dispatcher checks the path
string it built from caller input before http.NewRequest parses it, so
without that a ref of "match?" passed as eight segments ending in run
and routed as POST /playbooks/match. Pinned through the real catalog
action and dispatcher.
* test(server): key the read refusal on TokenAuth's scope message (TASK-2863, codex r1)
* chore(mcp): ToolSurfaceVersion 0.79, a read grant may run a playbook (TASK-2863)
* feat(web): gapped sort keys, so a reorder is one write (TASK-3525)
Lane order was dense integers, so a drop between two cards renumbered one
side of the lane: up to ~510 rows (and ~510 'reordered' activity rows) for
a middle drop in a 1,020-card lane. The planner now keeps the longest run
of cards already in order and writes every other card into the gap between
its kept neighbours: 1,024 past it at an end, spread evenly in the middle.
Moving one card in a gapped lane is one write. With no room (a dense lane
from before this, a never-dragged lane of 0s, or a gap closed by repeated
drops in one spot) it shifts the shorter side as before, but 1,024 apart,
so the next drops are single writes again. A plan leaving +/-2^30 re-spaces
the lane around 0.
Client only: the server stores whatever integers it is sent, so no schema,
API or MCP change, and API writers keep working with any values that sort.
The roles board shares the planner and gets the same behaviour.
Also: the timeline says "moved up/down" instead of raw sort values; the
share page's comment about its order is corrected (it is ListItems' default,
not sort_order); CLAUDE.md says a copy does not carry sort_order; the CLI's
--sort-order help says it is a key, not a position.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(web): a reorder writes the card the user moved when that is enough (TASK-3525)
A one-write plan can often write either card of a swap, and each written
card gets the 'reordered' activity row. The board, list, table, child
list and roles board now hand the planner the moved card's id, and it is
written alone whenever every other card already sorts and its gap has
room. The identity-gate rows for handleReorder and handleDndFinalize are
re-reviewed: the id is a synchronous planner argument, nothing moved.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(web): the reorder row shows a direction only for two differing numbers (TASK-3525, codex)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* feat(mcp): scope in the /mcp challenge, and a 403 insufficient_scope for read-only OAuth writes (TASK-2308)
The /mcp 401 challenge now names scope="pad:read pad:write", so a client
asks for the tiers the consent screen offers instead of falling back to
scopes_supported. resource_metadata stays the last parameter.
An OAuth connection whose grant cannot write that calls a write action
(any catalog action outside readOnlyActions) is now answered HTTP 403
with an error="insufficient_scope" challenge, before dispatch. Until now
the refusal could only be a permission_denied tool result, because the
dispatcher refuses after mcp-go owns the response. The challenge is what
lets a client re-authorize with the wider grant. A read-scoped PAT keeps
the tool error, since it cannot re-authorize, and the ChatGPT mount keeps
its own tool-result challenge.
Every write action was already refused for a read token, pinned by a
population test that drives every catalog action through the real
dispatcher under a write control and a read token. So nothing that
worked is refused now. pad_item.import is exempt there: it fails over
HTTP for every token (BUG-3533).
ToolSurfaceVersion 0.77 -> 0.78 (behavior bump).
* test(server): account for the /mcp scope pre-check's body peek in the reader census (TASK-2308)
* test(mcp): use the population test's result-text helper (TASK-2308)
* fix(server): challenge only tools/call requests mcp-go would run; check the import exemption's refusal (TASK-2308, codex r1)
* docs(mcp): the 403 covers a tools/call of up to 1 MiB (TASK-2308, codex r2)
* fix(collab): close a peer the op bus had to drop an op for (TASK-1273)
When a subscriber's buffer was full, MemoryOpBus.Publish dropped the op
with a warning and the peer stayed connected with a document missing it
until it happened to reconnect. The bus doc promised a force-close that
was never built.
Now the first drop of an op a subscriber needs (anything but awareness)
marks it overflowed: Publish delivers it nothing more, because an op
after the gap would carry an op-log cursor past the dropped row and the
reconnect would resume from there and never replay it. The bus signals
the overflow (SubscribeWithOverflow) once, and the room manager closes
the peer's socket, so its reconnect replays the gap from the op-log.
Awareness drops stay silent. Counted in pad_collab_overflow_closes_total
through an optional OverflowObserver.
cmd/loadtest-collab gains -stalled N (clients that never read). Measured
against a local instance: 5 editors at 200 ops/s of 4 KiB, 2 stalled:
both stalled clients closed by the server, editors 0 errors; with the
close disabled, both stayed open.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* docs(deployment): pad_collab_overflow_closes_total (TASK-1273)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* test(collab): a real overflow closes the peer once (TASK-1273, codex)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* feat(report): bucket the throughput series in the viewer's time zone (TASK-3524)
* feat(web): Insights asks for buckets in the viewer's time zone (TASK-3524)
* test(report): the tz parameter is echoed, and an unknown zone or Local is refused (TASK-3524)
* feat(auth): Deny and request context on the CLI sign-in approval page (TASK-2253)
* test(store): ScanNUL cost figures count the two cli_auth_sessions columns (TASK-2253)
* feat(import): a partly imported workspace is marked until its owner keeps or deletes it (TASK-896)
A bundle import that hits a DATA error after pad-export.json was imported
keeps the partial workspace (the rule is now written in docs/api.md:
transport failure, validation reject and pre-creation refusal roll back;
only a post-mint data error keeps). The 400 that said "kept" was the only
record, and a client may never read it.
- workspace_import_status (side table, SQLite 131 / PG 104): instance-
local, never exported, purged with the workspace.
- The keep path writes {status: partial, note}. The note is SANITIZED:
a fixed category and a correlation id (imp-...); the raw error goes to
the server log under that id.
- GET /workspaces/{slug} carries import_status (omitempty); the note only
for owners and instance admins. The list carries the status only.
- PATCH /workspaces/{slug} {"clear_import_status": true} clears it
(owner-only, like every workspace PATCH).
- pad workspace list marks it "partly imported" (text only; the JSON
mirrors the MCP workspace-list resource).
- Web: a banner on the workspace landing page and settings, with the
reason for owners, "Keep it" (clear) and "Delete workspace".
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(import): a rollback that cannot remove the workspace marks it too; the docs name what keeps (TASK-896, codex r1)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(import): only members who are admins read the note; the banner's note fetch is fenced on identity (TASK-896, codex r2)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(mcp): cloud refuses to start without an https origin for MCP OAuth; a blocked self-hosted MCP says so at startup and in health (TASK-1069)
* fix(mcp): pin the cloud origin combinations; health skips the setting read when MCP cannot be blocked (TASK-1069 codex r1)
* test(mcp): pad-cloud's production env (empty PAD_MCP_PUBLIC_URL, https PUBLIC_URL) starts, through the real env loading (TASK-1069)
* feat(collab): count resumes and force_refresh on resume, by reason, to measure offline editing (TASK-3501)
* fix(collab): resume reports run after itemLock is released (TASK-3501 codex r1)
* feat(mcp): remote /mcp writes are agent writes, named by the client's declared clientInfo (BUG-2772)
A write over remote /mcp reached the handlers with no X-Pad-Agent and
was recorded as the human. It is now an agent write, named by the
client's own MCP clientInfo.name: a modern request's _meta clientInfo,
else the clientInfo its session sent at initialize (remembered by the
Mcp-Session-Id pad issued; mcp-go's session state does not survive
pad's stateless transport), else unnamed; never the human, never a
guessed name. Each initialize logs the declared client (name, version,
protocol, transport; no tokens).
A remote call's default lease holder is that name plus a short session
id (claude-code#3f9a12), inside defaultLeaseHolder so claim and
next/ready agree; two connections of one client no longer share a
lease. Folds TASK-3495. ToolSurfaceVersion 0.77.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(mcp): a refused claim reaches both transports as lease_held with its holder (BUG-3496)
* test(mcp): register WriteLeaseHeldError in the stdio marker vocabulary table (BUG-3496)
* fix(mcp): a remembered client name is bound to the account that declared it (BUG-2772 codex r1)
* fix(mcp): the remote lease holder carries eight hex digits of the session id (BUG-2772 lead ruling)
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(cli): a bundle import shows progress, abandons a stalled upload, and resolves a lost answer (BUG-3476)
`pad workspace import <bundle>` printed nothing until the server answered,
sat silent on a dead TCP path until the kernel gave up or the 1h client
cap fired, and when the answer was lost could not say whether the
workspace existed, so a re-run could duplicate it.
- Each bundle import names its attempt with an import key (UUID), which
the bundle door records and GET /workspaces/import-status answers
(BUG-3475).
- Progress to stderr every 5s: bytes sent / total and percent, updated
in place on a TTY.
- A stall (the transport takes no bytes for 90s; the server's per-Read
deadline is 60s) abandons the upload.
- After the last byte: "Upload complete; the server is importing", then
import-status every 10s. A settled outcome seen twice while the answer
has not arrived ends the wait: the answer was lost.
- After any failure that is not the server's own JSON answer, it asks
the key (waiting up to 90s for a running attempt to settle) and
reports: complete -> success with the slug; kept -> the partial
workspace named; removed / not_created -> safe to re-run; no record or
unknown -> "outcome unknown: check 'pad workspace list' before
re-running, a re-run may create a duplicate". An older server ignores
the key and lands in that honest unknown branch.
The legacy JSON import is unchanged (its route takes no key).
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(cli): the stall message says what the counter knows (BUG-3476 codex r1)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(cli): whoami says what a failed check established, and exits non-zero (BUG-2706)
whoami mapped EVERY failed identity check to a rejection ("PAD_TOKEN is
set but the server rejected it." / "Session expired.") and exited 0.
Measured on main: PAD_TOKEN set, server unreachable, "rejected", exit 0.
whoami is the safe-check scripts and agents run first, so a network blip
sent operators to rotate a healthy token, and exit 0 told scripts they
were signed in (chatgpt-review-seed.sh's `whoami || die` could never
fire).
- Only a 401 or 403 is a rejection (PAD_TOKEN: "rejected it (HTTP n)";
stored session: "session expired").
- Any other status: "could not check your credentials: <url> answered
HTTP n ... They were not rejected; try again".
- No answer or an undecodable body: "no usable answer from <url>".
- Every failure, and "not logged in", is now an error (exit 1).
- cli.StatusError: a non-2xx with a non-envelope body (a proxy page) used
to be a plain fmt error with the status only in its text. It keeps the
same Error() text and now carries the status; cli.HTTPStatus reads it
from either error type.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(cli): a 403 on whoami's stored session is a refusal, not an expiry (BUG-2706 codex r1)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* feat: leases default to the agent's name, and next/ready leave out others' leased items (TASK-1809)
Several agents commonly share ONE account. A claim with no holder
defaulted to the account's email, so a second agent's claim matched the
first one's label and account and silently refreshed its lease: both
"won" (measured: two claims as rook and wren on one account both
answered 200 on the same lease).
- defaultLeaseHolder: the request's X-Pad-Agent name, else the email,
else the user id. An explicit holder still wins. The lease stays bound
to the account (BUG-3341). A second agent now gets 409 lease_held
naming the first, and cannot release it.
- next / ready (the dashboard's suggested_next) leave out items whose
live lease a claim by the caller would lose: same identity, the claim's
own predicate (store.ListForeignItemLeaseIDs, spelled positively
because NOT(...) over a NULL user compare is NULL), filtered before the
cap of three. Reminders are not filtered.
- Teaching: SKILL.md (claim before you start, pick another on
lease_held, release when done, register the session), instructions.md,
the pad_item holder param, and claim's CLI help, including that a
custom --holder's items leave your own next / ready.
- MCP ToolSurfaceVersion 0.76 (BEHAVIOR bump on the v0.70 grounds),
version.go entry, docs/mcp.md, README, golden bytes regenerated. The
holder description was trimmed to stay under the tools/list budget.
Remote /mcp sends no agent name, so there the default is still the email
(folded into BUG-2772). A lease taken before the upgrade is another
holder's to the same agent for up to its TTL.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* docs(mcp): the catalog paragraph names v0.76 (TASK-1809 codex r1)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
When Claude Code is detected and the pad plugin is not installed, the
skill offer ends with the plugin's two install commands and what it
carries (the /pad skill plus the panel and live notifications, updating
itself). Installed is read from Claude Code's
plugins/installed_plugins.json (under CLAUDE_CONFIG_DIR when set); a
missing or unreadable file shows the hint. No auto-install yet.
Co-authored-by: Claude <noreply@anthropic.com>
* docs(mcp): trim the server instructions to 2 KiB under the initialize budget (TASK-3489)
The initialize payload was 24,454 of its 24,576-byte budget, so the next
MCP change would trip TestMCPPromptBudgets. The multi_relation rules and
the relation title-matching detail are condensed to one line each, with
the full text moved to docs/mcp.md, and the resource list points at
resources/templates/list for descriptions. No rule an agent acts on is
dropped. Now 22,316 bytes (2,260 of headroom).
Co-Authored-By: Claude <noreply@anthropic.com>
* test(mcp): state the prompt-budget policy, a fixed ceiling with a trim rule (TASK-3489)
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: item lists refuse a filter key that names no field (BUG-3480)
The item list endpoints read every query parameter they did not know as
a field filter, so a parameter the server never had (the web Library
page's `all`, WebMCP's `collection`) answered 200 with an empty list.
A filter key must now be declared by a schema in scope (the
collection's, or any visible collection's on the workspace-wide list),
or be stored by an item in scope, which is honoured and named in the
X-Pad-Undeclared-Filter-Keys header (printed to stderr by the CLI).
Otherwise 400 validation_error, "invalid list filter ...". A key that
is not a field key is refused too; the store used to drop it and
answer the unfiltered list. WebMCP pad_item:list now lists a
collection through its path. ToolSurfaceVersion 0.73.
Co-Authored-By: Claude <noreply@anthropic.com>
* test: type the listByCollection mock so svelte-check can index its calls (BUG-3480)
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: adopt pre-origin conventions and playbooks as built-ins, once, by exact title (TASK-3462 U4)
Items made before built-in origins were recorded get the origin their exact
title names (several entries sharing a title: the one whose body matches; no
title match: a playbook's invocation_slug), with no seed version, so they
read current or unknown_origin and get the 2-way preview (Dave's ruling: no
historical hashes).
It runs once per instance at server start, recorded in platform_settings,
and only for items created before the origin migration was applied: a later
item with no origin deliberately has none (a copy, an older import). Live
items in convention/playbook collections only. Non-fatal on failure.
Tests on both backends: exact title adopts, and an unedited copy then reads
current; an edited one reads unknown_origin; a renamed playbook adopts by
slug; soft-deleted and post-cutoff items are not adopted; a second run does
nothing. Each of the four mutants (cutoff, once, deleted, slug) is killed.
The matcher has unit tests, including ambiguous titles.
* fix: legacy built-in adoption counts the migration's own second, refuses a title/slug conflict, and reads only convention/playbook items (TASK-3462 U4, codex r1)
- The cutoff is at or before the migration's second (timestamps are
second-precision), parsed rather than compared as text, since imported items
keep their own format.
- A playbook whose title names one built-in and whose invocation_slug names
another is ambiguous and is not adopted.
- The pass lists the convention and playbook collections first and reads only
their items, instead of every originless item on the instance.
Both new tests failed on the previous code.
* fix: legacy built-in adoption requires the workspace to predate the cutoff too (TASK-3462 U4, codex r2)
Import keeps each item's created_at from the bundle, so an old bundle
imported after the origin migration but before the pass first ran read
as legacy. Every import door mints a new workspace and none writes into
an existing one, so the workspace's created_at bounds arrival.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* test: pad library diff/update and the builtin_update capability (TASK-3462 U3c, red)
* feat(cli): pad library diff and pad library update for built-in conventions and playbooks (TASK-3462 U3c)
- diff shows what Pad's library changed since the item was made, what the
user changed (or a plain comparison when that is unknown), and the
settings an update would replace.
- update takes the library's text, guarded by the seq read just before. It
refuses content_pending_flush unless --overwrite-pending-edits, explains
update_conflict, and reports a current item without sending.
- The server advertises builtin_update; both commands refuse without it,
since an older server's bare 404 reads like not_builtin.
Not on MCP, per the design.
* test: pad library diff/update codex r1 findings (TASK-3462 U3c, red)
* fix(cli): pad library diff/update codex r1 findings (TASK-3462 U3c)
- The JSON diff carries the current text from a server that does not send it.
- A no-op under --format json is a JSON result.
- A failed capabilities probe says so; it is no longer reported as an older
server.
- An update prints the write's warnings: applied to an open editor, and
pending edits pruned.
- content_pending_flush is rendered by the shared helper, which names
set-aside rows apart from an open editor's; the flag's help covers both.
- The diff names an end-of-file newline change.
* test(cli): built-in diff keeps large numbers and refuses a mixed-version fallback (TASK-3462 U3c, codex r2, red)
* fix(cli): built-in diff keeps numbers as written and refuses a fallback from another version (TASK-3462 U3c, codex r2)
BuiltinText decodes its fields keeping numbers, and the fallback read of the
item does the same, with status and nulls dropped to match the server's
current. A fallback whose seq differs from the state's is refused instead of
mixed in.
* test(cli): built-in update reports undeclared fields (TASK-3462 U3c, codex r5, red)
* fix(cli): built-in update reports undeclared fields like any item update (TASK-3462 U3c, codex r5)
* test(server): the bootstrap counts built-ins with an update on offer (TASK-3462 U3a, red)
* feat(server): bootstrap carries builtin_updates, a count of built-ins with an update on offer (TASK-3462 U3a)
update_available plus diverged, among the items the caller sees in full:
the library listing's own visibility, through one shared helper. omitempty,
so a workspace with nothing on offer is byte-identical to before.
* feat(mcp): agents mention built-in updates once and never apply them; tool surface 0.71 (TASK-3462 U3a)
The skill (both copies) and the MCP server instructions describe the new
builtin_updates bootstrap count: mention it once per session, and leave each
update for a person to accept. ADDITIVE bump to 0.71, since the bootstrap
every door returns gained a member and the instructions changed. Golden wire
bytes regenerated: only the version and the instructions text moved.
* test(server): an accepted built-in update records the field values it replaced (TASK-3462 U3a, red)
* feat(server): an accepted built-in update names the field values it replaced (TASK-3462 U3a)
Version history keeps bodies only, so the summary of the update now lists
each non-body field whose value it changes, as key: old → new (JSON,
bounded), including typed convention metadata. Fields the update leaves
unchanged are not listed.
* test(server): every template seed and every library activation reads current at once (TASK-3462 U3a)
The guard the night-43 review ran by hand: the registry's hash must equal
what each seeding door stores. A seed that stores drifted text turns it red
(update_available straight after seeding).
* test(server): replaced-fields summary ignores numerically equal values and truncates on a rune boundary (TASK-3462 U3a, codex r1, red)
* fix(server): replaced-fields summary compares canonical numbers and cuts on a rune boundary (TASK-3462 U3a, codex r1)
The copy preflight decoded carried field values with plain json.Unmarshal
into `any`, so a number above 2^53 became a float64 and the dry run
previewed 9007199254740992 for a stored 9007199254740993. The copy itself
keeps the stored digits (server-side), so the preview misled exactly
where a user would check. Decode with models.DecodeJSONKeepingNumbers,
as BUG-3448 did for item show and the MCP resource.
The mutating copy's response is left on json.Unmarshal: it prints no
carried values (the item's fields are a string), and a decode error
there would misreport a committed copy.
Tests: the client keeps the digits, and the rendered dry run through the
real client shows them; on the old decode it renders
"= 9007199254740992".
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* test: pad init keeps an edited skill and does not downgrade a newer one (BUG-3466)
* fix: pad keeps an edited /pad skill and never downgrades a newer one; skill files carry a stamp, and --force replaces (BUG-3466)
* test: workspace init updates an older unedited skill, agent update reports a keep, --list names --force for a newer skill, and semver numbers have no size limit (BUG-3466, codex r1)
* fix: workspace init routes installed skills through the decision, agent update and --list report kept files with --force, and semver numbers compare at any size (BUG-3466, codex r1)
* test: --list flags an untracked edited skill, and agent update counts an untracked current one (BUG-3466, codex r2)
* fix: --list asks the decision for local skills, and every skill present after a write is recorded and counted (BUG-3466, codex r2)
* test: invalid SemVer never refuses as a downgrade; the frozen list pins a released text (BUG-3466, codex r3)
* fix: the frozen legacy list holds released output only, and SemVer is validated strictly before comparing (BUG-3466, codex r3)
* test: every unstamped release's skill output must be in the frozen list; the release workflow (which has the tags) fails loudly otherwise (BUG-3466, lead)
* test: whitespace after the stamp does not hide it; pinned real copies and per-tag formatter checks guard the frozen list independently (BUG-3466, codex r5)
* fix: whitespace after the stamp no longer hides it (BUG-3466, codex r5)
* test: a newer pad's identical text reads as current, not as a downgrade (BUG-3466, codex r6 declined with reason)
* chore: regenerate the legacy skill hashes with v0.18.0-rc.8 (BUG-3466)
A moved-in backlog of open tasks and bugs with no priority set, bugs in
their initial status `new`, answered "No ready items found": only the
literal status `open` counted, and an open item outside an active plan
needed a high/critical priority (BUG-1082's "without flooding", though the
list is capped at three).
Such items now fill the slots the existing tiers leave empty, from a
fallback tier below every item admitted before, so a workspace that
filled three slots gets the same list. The tier admits items whose
collection is not a system collection and declares `priority` or
`severity` (lead-ruled; across every built-in template that is Tasks,
Features, Backlog and Bugs), whose done field is not terminal, that are
not in progress and not blocked. It ranks by priority, else severity,
then age.
`ready`'s help and the MCP `ready` description said it returns the full
actionable backlog; it returns the same capped top three as `next`, and
now says so. The empty state says why and points at `pad project stale`.
ListCollectionsMinimal also returns is_system.
ToolSurfaceVersion 0.70 (behavior bump on the v0.49 grounds).
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* feat: built-ins record their origin, and an item made from one can be offered Pad's current text (TASK-3462 U1+U2)
U1: every convention and playbook Pad ships has a stable key and a content
hash; template seeding and the new server-side POST /library/activate record
{key, seed hash, seed text} in item_builtin_origin (migration 128 / pg 102).
CLI, remote MCP and the web Library page activate through the endpoint;
seeded and activated copies of an entry store the same fields. Export/import
carry the origin; copies get none; a request body cannot claim one.
U2: GET /items/{ref}/builtin derives current / update_available / diverged /
unknown_origin / unknown_entry; POST /items/{ref}/builtin/update takes the
library text through the ordinary PATCH path (expected_seq required, status
and title untouched, a forced version row keeps a diverged item's edits);
GET /builtins lists states in one join, showing only fully visible
collections.
* fix: the origin's seed text is NUL-protected (trigger migration 129); the MCP activate test asserts the endpoint call; activation follows the declared kind server-side (TASK-3462)
* fix: the built-in seed moves in the update's own transaction; a NUL-bearing imported origin is skipped; the listing computes visibility once; numbers hash by value (TASK-3462, codex r1)
* fix: on the applier path the built-in seed is recorded only after the apply confirms; a library change that only drops a field is offered (TASK-3462, codex r2)
* fix: a field the library added that the user already set reads diverged; an update clears convention metadata the library dropped (TASK-3462, codex r3)
* test: the lifecycle test finds the imported item by slug (the copy shares its title); ScanNUL's cost figures count the two new columns (TASK-3462)
* test(e2e): Library activate posts the entry to /library/activate and the item records its built-in origin (TASK-3462)
Every remote route that needs a workspace and lacks one (routeTable
mappers, special routes, link routes, the assign/role resolvers) now
answers the structured no_workspace envelope with available_workspaces
and a hint, the one stdio has always given, instead of validation_failed
"workspace is required". Mappers return one sentinel the dispatcher
converts centrally; special routes call noWorkspaceResult directly.
The single-workspace default now runs before the assign/role resolvers,
so a one-workspace caller who omits workspace is not refused. The
no_workspace hint is never empty.
ToolSurfaceVersion 0.69 (behavior bump on the v0.42/v0.45 grounds).
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* test: markdown stays body-verbatim and names an empty body; no agent-facing text loads an item with it (TASK-3451)
* fix: skills and playbooks load items with --agent; --format markdown stays body-verbatim and names an empty body on stderr (TASK-3451)
* test: the guard covers every flag spelling and agent-read doc; a stale empty body is not claimed empty (TASK-3451, codex r1)
* fix(cli): the empty-body hint is not printed over a stale body (TASK-3451, codex r1)
* fix: remaining skill load lines use --agent; the guard exempts only round-trip descriptions and covers quoted refs and the read alias (TASK-3451, codex r2)
* fix(skill): the convention-body fallback loads with --agent (TASK-3451, codex r3)
* test(cli): json fields print as JSON, the metadata block is labelled for the item's kind, the update summary sizes structured values (BUG-3448)
* fix(cli): json fields print as JSON, the metadata block is named for the item's kind, the update summary sizes structured values (BUG-3448)
* test: large numbers, null, the MCP resource and unguessed labels (BUG-3448, codex r1)
* fix: numbers keep their digits, null prints as null, the MCP item resource prints json fields as JSON, metadata labels are not guessed (BUG-3448, codex r1)
* test(cli): the agent projection keeps large numbers (BUG-3448, codex r2)
* fix(cli): the agent projection keeps large numbers (BUG-3448, codex r2)
* chore(mcp): ToolSurfaceVersion 0.68, a BEHAVIOR bump: the item resource renders json fields as JSON (BUG-3448, lead ruling)
* fix: a blank workspace's conventions and playbooks take the library's own triggers and scopes (BUG-3446)
The blank template seeds Conventions with trigger [always] / scope [all]
(Playbooks [manual] / [all]) and nothing widened them, so all 18 library
conventions were refused on every door: CLI and MCP library activate, the
web Activate button, and the onboard playbook's literal B3 create.
A create in a system collection declaring the convention or playbook
artifact kind now adds a missing trigger/scope value to the select's
options when the LIBRARY itself uses that word
(collections.LibraryOptionVocabulary, computed from the library). The write
goes through UpdateCollection with its concurrency token, ahead of
validation, and is reported in warnings.options_added (CLI: stderr) and in
the created activity's metadata, which the item timeline renders. Any other
value is refused as before. Editors may trigger it (lead ruling). The
onboard playbook's B3 now says how an owner adds an invented trigger.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* chore(mcp): ToolSurfaceVersion 0.67 for BUG-3446 (additive)
Lead ruling: a create in a system conventions/playbooks collection now
adds a library trigger/scope word instead of refusing it, and its result
gains an omitempty warnings.options_added. No tool name, enum or param
moved; tools/list golden bytes are unchanged, initialize moves for the
version and the instructions note.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* test(server): an invite asks an existing user; re-invite replaces; decline by id and by code (BUG-2136, red)
* fix(server): an invite asks an existing user instead of adding them; a re-invite replaces the pending invitation; decline by id and by code (BUG-2136)
* feat(cli,mcp): pad workspace invitations/accept/decline; invite output says pending; ToolSurfaceVersion 0.66 (BUG-2136)
* test(e2e): invitees accept their invitation before the specs use the membership (BUG-2136)
* test(store): concurrent re-invites of one address leave one pending invitation (BUG-2136)
* test(store): an accept that loses to a decline adds no membership (BUG-2136, codex r1, red)
* test(server): an unverified invitee can decline by code, as they can accept (BUG-2136, codex r1, red)
* fix: an accept that loses to a decline or replace adds no membership; decline by code passes the verified-email gate; honest oracle comment (BUG-2136, codex r1)
* fix(server): the verified-email exemption matches exactly /invitations/{code}/accept|decline (BUG-2136, codex r2)
* test(server): drop the plan-limit harness's now-unused newUser (BUG-2136)
* fix(webhooks): 408, 425 and 429 are transient on the owner and app delivery paths, through one shared RetryableStatus (TASK-3409)
* feat(webhooks): owner hooks keep per-endpoint delivery state: a retry goes only to endpoints still owed, a 4xx is final for the event, and endpoints still owed when retention gives up are counted as dropped (GET /webhooks, pad webhook list) (TASK-3409)
* fix(webhooks): an owner endpoint deactivated while an event is still owed to it is recorded and counted as dropped, not silently skipped (TASK-3409, codex r1)
* fix(store): the automatic deactivation settles the endpoint's owed deliveries as dropped and counts them, in its own transaction (TASK-3409, codex r2)
* feat(apps): human-facing JSON names the app that wrote an item, comment or version (TASK-3413, SPEC-6 U9c)
Items (the creating app), comments and version rows carry via_app (the
install id) and via_app_name (the bot's name, else the origin; kept after
uninstall) on the web API, the MCP get / list summary / history summary
projections, and the TS types, with WorkspaceApp naming the members apps
array. ToolSurface 0.65, additive.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(apps): attribution on search, /changes, playbook show and restore; chunked lookup (codex r1 on U9c)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(apps): attribution on the role board, a comment edit's response and the account export (codex r2 on U9c)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* docs: README tool catalog at v0.65 (TASK-3413)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS