Commit Graph

1273 Commits

Author SHA1 Message Date
xarmian 833e309052 fix(cli): whoami says what a failed check established, and exits non-zero (BUG-2706) (#1901)
* 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
2026-10-08 15:03:28 -04:00
xarmian 8d02818fd6 feat: leases default to the agent's name, and next/ready leave out others' leased items (TASK-1809) (#1900)
* 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
2026-10-08 14:28:16 -04:00
xarmian 63e2830c75 fix: the Live badge reflects a dead SSE connection, and the reconnect catches up (TASK-2197) (#1895)
* fix: the Live badge reflects a dead SSE connection within 75s or at once on offline, and the reconnect catches up (TASK-2197)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): never open the SSE stream while the browser is offline; pin heartbeats on a busy stream (TASK-2197 codex r1)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 13:41:39 -04:00
xarmian 811206d54f test(server): run the heaviest serial tests in parallel, and stop three of them migrating a fresh store (TASK-3493) (#1891)
internal/server's -race step measured 1692s after #1885, 62.7% of its
45m cap, and the package ran almost entirely serially. Of the 30 slowest
serial tests:

- three (the *IsNilInProduction seam checks) built their store with
  store.New(":memory:"), which runs the whole migration chain, about 17s
  each under -race, to check one field. They now use testServer(t),
  which copies the once-built template.
- 25 more call t.Parallel(). Each was checked with a go/ast pass over
  the test and every same-package function it reaches: none writes a
  package-level variable, sets the process environment, or listens on
  a fixed port. The shared caches two of them use are mutex-guarded or
  sync.Once.
- two stay serial: TestRateLimit_DisabledByEnv uses t.Setenv, which
  t.Parallel refuses, and TestBUG3474_RevalErrorFailsClosedOnWrite
  writes the package-level collabMembershipRevalInterval.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-08 12:51:37 -04:00
xarmian 0bdf539455 feat(release): the Claude Code plugin follows stable releases; the release workflow pins the marketplace (TASK-3487) (#1890)
* feat(release): the Claude Code plugin follows stable releases; the release workflow pins the marketplace (TASK-3487)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(release): the plugin pin never moves backwards; force the tag fetch (TASK-3487 codex r1)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 12:46:18 -04:00
xarmian 8c64f3bd30 docs(mcp): trim the server instructions to 2 KiB under the initialize budget (TASK-3489) (#1889)
* 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>
2026-10-08 12:40:51 -04:00
xarmian 585bd27991 test(buildtools): freePort picks outside the ephemeral range, so a parallel package's :0 bind cannot take it (BUG-3491) (#1884)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 11:33:41 -04:00
xarmian ee288cbebd fix: item lists refuse a filter key that names no field (BUG-3480) (#1880)
* 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>
2026-10-08 10:43:33 -04:00
xarmian 7b41a0298e fix(auth): account deletion's 2FA code goes through login's limiter and single-use claim (BUG-3482) (#1881)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 09:09:20 -04:00
xarmian 7b68125a30 feat: schema edits say what they do to items; removing an option is never a rename (TASK-2188, TASK-2187) (#1877)
* feat: schema edits say what they do to items, and removing an option is never a rename (TASK-2188, TASK-2187)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix: import the field-usage type; keep the collection tool description inside the tools/list budget (TASK-2188)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix: the counts landing no longer close an open schema-edit confirm; pin that an option added after a removal is no rename (TASK-2188 codex r1)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(e2e): the BUG-2129 migration-race spec confirms the rename before its PATCH (TASK-2188)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 08:29:44 -04:00
xarmian c81b5c0726 fix: owners cannot lock themselves out by mistake; accounts with no password can set one and turn 2FA off (TASK-2190) (#1879)
* fix: owners cannot lock themselves out by mistake; accounts with no password can set one and turn 2FA off (TASK-2190)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(web): the settings identity gate re-reviews the member handlers; PadApiError on its own import line (TASK-2190)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 07:41:35 -04:00
xarmian 9d2572eb7b feat: adopt pre-origin conventions and playbooks as built-ins, once, by exact title (TASK-3462 U4) (#1878)
* 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>
2026-10-08 06:55:33 -04:00
xarmian 99aac1687b feat(cli): pad library diff and pad library update for built-in conventions and playbooks (TASK-3462 U3c) (#1875)
* 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)
2026-10-08 05:05:04 -04:00
xarmian 15c9f76366 feat: archived collections can be listed, restored and undone; the archive confirm shows the count (TASK-2189) (#1874)
* feat: archived collections can be listed and restored (TASK-2189, server, CLI, MCP v0.72)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* feat(web): archive shows the item count, guards big collections, and can be undone or restored (TASK-2189)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix: list-archived is read-only, an unknown count needs typing, instructions say v0.72; store tests on both dialects incl. a restore race (TASK-2189)

codex r1.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test: README tool-surface version 0.72; classify the archive Undo's restore continuations (TASK-2189)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 03:59:26 -04:00
xarmian 28501155b1 feat(web): review and accept built-in updates from the Library page and the item pane (TASK-3462 U3b) (#1873)
* feat(web): the Library page matches built-ins by key and badges an update on offer (TASK-3462 U3b)

GET /builtins joins the page's load. An entry is Active when an item made
from it exists (by key, so a rename no longer re-offers it), or by the old
title match for items with no recorded origin (legacy, until U4). An entry
with an update on offer shows 'Update available' (unedited copy) or
'Library changed' (edited), linking to the item. The identity gate rows
are re-reviewed, and builtinEntries is page data replaced on both arms.

* feat(web): the item pane offers a built-in's newer library text for review (TASK-3462 U3b)

BuiltinUpdateNotice reads GET /items/{ref}/builtin for items in a system
collection and, when an update is on offer, shows a badge that opens a review
dialog:
- an unedited copy: its text against the library's;
- an edited copy: what the library changed and what you changed, side by side;
- unknown origin: a plain two-way comparison that says it may have been edited.
It lists the settings accepting would replace and says only the body survives
in history.

Accept sends the previewed expected_seq. content_pending_flush turns the
button into an explicit discard; update_conflict refreshes the preview.
Nothing updates on its own.

The notice is fenced with identityFence(), joins the child-population guard,
and is mounted under the identity key with onUpdated refusing a stale key.
Behaviour tests: 8, with two mutants killed.

* fix(web): built-in update review never reloads over the pane, refuses with unsaved edits, and stays on its item (TASK-3462 U3b, codex r1)

- No callback into the pane after Accept. A loadData() there flushed a
  pending raw draft over the accepted text, and cancelled the collab editor's
  materialization flush. The item's own item_updated refreshes the pane, and
  the notice re-reads when seq moves.
- Accept refuses while the pane holds unsaved edits (a pending raw draft, a
  save in flight), asked synchronously at the press.
- A response for an item the pane has left commits nothing: the press checks
  its ref, and the mount is keyed by item id.
- The dialog closes when its offer goes away, so it is never left empty.
- After content_pending_flush the copy says the discarded edits are not kept.
- The Library page offers unknown_origin too ("Library version differs").

The four new behaviour tests fail on the previous component.

* fix(web): built-in update review reloads raw mode only, waits for unsynced collab edits, and survives a rename (TASK-3462 U3b, codex r2)

- Raw mode keeps its body on an SSE refresh, so an accepted update now
  reloads the pane there. The pane decides: not in collab mode, where a reload
  cancels the editor's flush, and not over a pending raw draft. The callback
  refuses a stale identity key (class-C table row).
- Unsaved edits now include local collab edits not yet flushed (localDirty)
  and a collab provider that is not synced: edits the server's
  content_pending_flush cannot see.
- A press tracks the item by id, so a rename during the request no longer
  leaves Accept disabled.

* fix(web): built-in update guard uses the provider's sync state, not localDirty; a conflict refresh uses the live ref (TASK-3462 U3b, codex r3)

localDirty is not cleared when a collab flush is deduplicated, so it could
block Accept until a reload. A synced provider's typing is already in the
op-log, where the server's content_pending_flush refusal sees it; an unsynced
one (buffered pre-anchor, or dropped offline) is what the client must refuse.
After a rename, the update_conflict refresh now reads by the current ref.

* fix(web): Accept saves the open collab editor first and stops if that changed the text (TASK-3462 U3b, codex r4)

A synced socket proves nothing about typing the server has stored: a frame in
flight is not in the op-log, so content_pending_flush cannot see it. Accept
now runs a foreground editor flush first (new CollabFlusher.flushAndWait).
- flushed: the editor held text the preview did not show, so nothing is
  replaced and the preview is read again;
- failed or skipped: nothing is replaced;
- deduped: the update proceeds.
The review dialog is a native modal, so no typing can follow the flush. The
not-synced refusal stays, for buffered frames that would land after the
update. Both new tests fail on the previous component.

* test(server): the built-in state carries the item's current text with its seq (TASK-3462 U3b, codex r5, red)

* fix: the built-in review compares against the item text read with its seq (TASK-3462 U3b, codex r5)

GET /items/{ref}/builtin now carries current: {content, fields}, the item's
own text from the same read as seq (additive, omitempty, present when an
update is on offer). The dialog compares against it, falling back to the
pane's copy for an older server. So after a 'flushed' stop, the re-read
shows the edits that caused it, and a fresh seq is never paired with a stale
body.

* fix(web): the built-in notice renders for any item the server says is built-in; document current (TASK-3462 U3b, codex r6)

The is_system gate missed built-ins moved, or activated, into a non-system
collection of the right kind: a Library badge then led to a page with no
review. The server's not_builtin answer now decides. CLAUDE.md's endpoint
bullet documents current.

* fix(web): the Library page's title fallback works again; the permission walk keeps an unactivated playbook (TASK-3462 U3b, CI)

The page listed conventions and playbooks with ?all=true. The server has no
such parameter and reads an unknown one as a field filter, so both lists came
back empty and the title match never matched. On main every library card
offers Activate, seeded ones included. Dropped.

U3b's key match then (correctly) marks all four template-seeded playbooks
Active, which left the permission walk's Library leg with nothing to
activate. Its setup now deletes one seeded playbook. It also creates a
convention with a library title and no origin (a legacy item), which the
leg asserts reads Active for every account that can see it. That assertion
failed with ?all=true and passes without it.
2026-10-08 02:53:41 -04:00
xarmian 79922d4bab feat: conventions checked on each comment alone, with the chip on its card (TASK-3119 U2b) (#1872)
* feat: conventions checked on each comment alone, with the chip on its card (TASK-3119 U2b)

A new decision set, conventions_comments (QuestionSet.PerComment): the
conventions questions asked about each of an item's last 10 live comments,
one comment per state (BuildCommentState: the item's title and collection,
and the comment, 16k-rune cap), rows keyed conv:<ref>@<comment-id>. Gate 2
measured this subject: 7 of 7 comment-borne breaks caught, 0 of 9 false, a
credential past the old 2k trail clip caught at 0.99.

- Asked vs current (lead ruling): only the newest 10 comments are asked or
  re-asked, but an answer stays current while its comment is unchanged,
  however many newer comments arrive. Edited, deleted or moved: not current.
  A title edit re-asks the window (the title is in the state, as measured).
- Idempotent per comment: a new comment costs one call, an item write none.
- Web: DecisionChips publishes per-comment chips from its existing read into
  a small store keyed by item; TimelineCommentCard shows 'Possibly breaks
  CONVE-N' on the comment (and on a reply) at p >= 0.9, full-reload link, no
  compliance state. Cleared when the item goes.
- CLAUDE.md: the third set.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): a late decisions response cannot repopulate cleared comment chips (TASK-3119 U2b)

codex r1.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 01:43:43 -04:00
xarmian 4532eecffd feat(decision): conventions ask about the item without its comment trail, 12 questions a call (TASK-3119 U2a) (#1871)
* chore(eval): TASK-3119 U2 gates in one command; decision.BuildCommentState

-gates runs three passes through the production question path:
A  U0's population on the U2a item state (links in, trail out), gated on
   precision not dropping below U0's on any convention that fired;
B  comment-borne breaks, each comment asked alone with its item's title
   and collection, gated on precision >= 0.9;
C  a measurement of 12+ questions in one call (MaxPerCall 12).

BuildCommentState is the comment subject's one serialisation, shared by
the harness now and the runner in U2b. Comments are clipped at the body
bound (16k runes), not the trail's 2k.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* feat(decision): the conventions set asks about the item without its comment trail, 12 questions a call (TASK-3119 U2a)

The conventions state carried the last 10 comments, clipped at 2,000 runes
each: on docapp that was most of every call's input (15.7k tokens for one
write across 3 calls), 720 items had a clipped comment, and a break past the
clip was invisible anyway. Comments get their own subject in U2b.

- QuestionSet.NoTrail leaves the trail out; the conventions set sets it. The
  state is exactly what the U2 gates measured (recent_trail [], links in):
  precision held at 100% on every convention that fires, recall rose (1286
  40->80%, 2693 70->90%), 0 truncated states, ~954 input tokens per call.
- MaxPerCall 5 -> 12, from measure C: up to 13 questions in one call gave
  identical precision and recall, 0 of 116 answers moved by more than 0.2.
- A comment write no longer changes this set's state, so it costs no call.
- Every conventions answer computed before this reads not-current until the
  item's next write (the state bytes changed): a fleet-wide re-ask, by design.
- CLAUDE.md: the registered sets (it still said attention was the only one,
  since U1a) and the per-set state.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 00:01:26 -04:00
xarmian daed6bb441 fix(server): removeUnusableWorkspace reports a failed delete as kept (BUG-3479) (#1870)
Its contract is nil when the workspace is gone, an error when it was kept,
but the branch where its own soft-delete FAILED returned nil. Every caller
then reported a removal that had not happened while a live, ownerless
workspace held its slug.

- That branch now returns an error naming the failed removal.
- Cloud auto-create (both arms) logs that the workspace could NOT be removed
  instead of leaving 'is being removed' as the last word.
- Create workspace's seed failure says in its 500 that the half-made
  workspace survived and holds its name.
- addOwnerOrCompensate's ABSENT arm now returns the KEPT error, which its
  callers already treat as a failure; the bundle keep path reads the row back
  (BUG-2709) and the interrupted-import rollback does too (BUG-3475).

A test per caller with a SQLite trigger refusing the soft-delete; each fails
on the old nil.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 23:39:39 -04:00
xarmian a23edba856 feat: built-in update count in the bootstrap, and accepted updates record replaced fields (TASK-3462 U3a) (#1868)
* 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)
2026-10-07 21:29:37 -04:00
xarmian 250b5c2c03 fix(server): an interrupted import rolls back, and its outcome can be asked for (BUG-3475) (#1867)
* fix(server): an interrupted import rolls back, and its outcome can be asked for (BUG-3475)

The web import dialog hung on 'Importing...' when the upload stalled. The
server already bounds a stalled body (60s per-Read deadline, BUG-3184), but
it then KEPT the partial workspace and said so in a 400 that cannot reach a
client whose connection died; a lost 201 read as a failure whose retry made
a duplicate.

- A transport failure reading the body (deadline, reset, client gone) rolls
  the partial workspace back and answers 400 import_interrupted with the
  cause. A mid-stream DATA error still keeps it (TASK-896): its 400 reaches
  a live client. Told apart by a tracker on the raw body, beneath the size
  cap: a truncated file that arrives in full reads to a clean EOF.
- ?import_key= names an attempt; GET /workspaces/import-status?key= answers
  running/complete/kept/removed/not_created/unknown for the caller's own key.
  In memory, 1h TTL, bounded; an unknown key is UNKNOWN, never 'nothing was
  created'. A running key cannot be reused (409).
- The validation-reject rollback and the new one share rollBackPartialImport.

The web dialog's progress, stall abort and outcome display follow in PR B.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(server): body-reader census counts the import's transport tracker (BUG-3475)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(mcp): import-status is a known static sibling of /{slug} (BUG-3475)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(server): a running import key is never evicted, and a panic records unknown (BUG-3475)

codex r1: eviction at the bound could drop a RUNNING entry, letting its key
begin again beside the upload still using it; it now drops only the oldest
settled entry, and an attempt over a bound full of running ones goes
untracked (reads unknown). A panic after mint skipped every report and left
the key running until the TTL; the handler now records unknown before
re-panicking, since the keep door does not say which way it went.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(server): a running import key outlives the settled TTL (BUG-3475)

codex r2: the TTL sweep expired RUNNING entries too, and an import may read
for up to its 1h ceiling, so a long upload's key could begin again beside it.
Running entries now last importOutcomeRunningTTL (2h), pinned above the read
ceiling.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(server): an import rollback reads back whether the workspace is gone (BUG-3475)

codex r3: removeUnusableWorkspace returns nil even when its delete fails, so
'removed' was inferred from a return that does not mean it. rollBackPartialImport
now reads the row back; a workspace still live reports unknown and the 400
says it could not be removed. Tested with a trigger refusing the soft-delete,
red without the read-back.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 21:03:24 -04:00
xarmian 698fe2ff6e fix(server): a collab revalidation tick that errors makes the conn read-only (BUG-3474) (#1866)
* test(server): a revalidation tick that errors leaves the collab conn writable (BUG-3474, red)

A real fault: workspace_members is renamed, so the tick's membership lookup
fails with a plain error. After the first failing tick, a frame still
persists. Red 3/3 on SQLite.

* fix(server): a collab revalidation tick that errors makes the conn read-only, not closed (BUG-3474)

Lead ruling: fail closed on WRITE, keep the connection. Both error branches
of the tick (GetItem, and a plain error from authorizeCollabAccess) now call
SetConnWritable(false) before logging. The next clean tick restores write if
the role still allows it. The test faults each branch for real (renamed
workspace_members, renamed items).

* test(server): a credential re-check store error keeps the collab conn writable; recovery is checked by content (BUG-3474, codex r1, red)

* fix(server): an unknown credential re-check makes the collab conn read-only and skips the tick (BUG-3474, codex r1)

streamCredentialStillValid reads a store error as valid (right for SSE,
which has no write), so the collab tick went on to set write from the role.
The collab loop now switches on credentialLiveness: invalid closes as
before; unknown sets read-only, logs at warn and re-arms.
2026-10-07 19:45:14 -04:00
xarmian cc1aa6902c feat(decision): log each evaluation's spend (TASK-3119) (#1861)
* feat(decision): log each evaluation's spend (TASK-3119)

One info line per evaluation that reached the provider: set, workspace,
item ref and id, questions, calls, input and output tokens, truncated,
outcome (stored|failed). Counts and identifiers only: no question, answer,
item text or error message. A failed call's tokens are counted too.

The dogfood box's /metrics is loopback-only and :7777 binds the LAN IP, so
the server log is where its spend is readable.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(decision): a liveness stop between split calls still logs the call made (TASK-3119)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 13:43:42 -04:00
xarmian deec62235e feat(decision): executable conventions, the server half (TASK-3119 U1a) (#1859)
* wip(decision): conventions set (TASK-3119 U1a)

* chore(eval): executable-conventions eval through the production question path (TASK-3119 U0)

Measures, before any chip exists, how often the decision provider says a
hand-labelled item breaks a convention at the 0.9 threshold: per
convention precision and recall (all cases and real-only), the false
'Breaks' rate of a behaviour convention, and the calls and tokens spent.
State is decision.BuildItemState, the question a decision.Noul worded as
the U1 contract says, the model decision.DefaultModel.

The hand labels were posted to TASK-3119 before any score existed; the
trail comment carrying them is dropped from every state, and the dry run
refuses a state that still carries them. Reads TYPESAFE_API_KEY only.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* chore(eval): -links and -only for the conventions eval (TASK-3119 U1a)

-links builds each state as the conventions set does, with the item's
links (from the dump: the parent link only; blocking links are not in
pad item show's JSON). -only runs one convention. Used for the lead's
U1a gate: CONVE-1286 rerun with links.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(decision): liveness before EACH split call (TASK-3119 U1a, codex r1)

A split set makes several provider calls; the liveness check ran once
before them, so an item or workspace deleted between calls was still
sent in the later ones. It now runs immediately before every call. A
test deletes the item from inside the first call and the second is not
sent; checking only once fails it.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(decision): decision_check has no default; allow-list the eval's decode (TASK-3119 U1a)

A default "on" was written into every new convention: imports gained a
decision_check field (artifact import parity) and an activated built-in
no longer matched its seed (TASK-3462 would read every one as diverged).
Absent already reads as on, so the field carries no default, in the
template and both migrations.

The conventions eval's readJSON is a raw decode the reserved-field guard
counts; allow-listed with its reason, like the attention harness.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 12:42:32 -04:00
xarmian 90a304765f fix(cli): copy --dry-run previews a big number with every digit (BUG-3456) (#1858)
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
2026-10-07 12:37:49 -04:00
xarmian 3aff8e388f fix: pad keeps an edited /pad skill and never downgrades a newer one (BUG-3466) (#1857)
* 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)
2026-10-07 12:26:28 -04:00
xarmian b3cf4f5292 fix(server): an ordinary open backlog fills Up next and pad project ready (BUG-3453) (#1854)
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
2026-10-07 08:36:58 -04:00
xarmian 1c23a1bcbc feat: onboarding asks where an existing project's open work lives and offers to bring it in (TASK-3454) (#1853)
* test: onboarding an existing project asks where its open work lives and brings it in after saying there is no importer and confirming (TASK-3454)

* feat: the onboard playbook asks where a project's open work lives and offers to bring it in, saying there is no importer and confirming first (TASK-3454)

* test: open work is untrusted data, its destination is confirmed when B2 did not run, and the paste fallback does not assume gh (TASK-3454, codex r1)

* fix: open-work text is data not instructions; the destination is confirmed when B2 did not run; the paste fallback is keyed on being able to list issues (TASK-3454, codex r1)

* test: a full page of issues is reported as possibly truncated (TASK-3454, codex r2)

* fix: a full page of issues is reported as possibly truncated rather than silently cut (TASK-3454, codex r2)
2026-10-07 08:18:53 -04:00
xarmian c17865462f feat: built-ins record their origin; an item made from one can be offered Pad's current text (TASK-3462 U1+U2) (#1852)
* 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)
2026-10-07 06:40:06 -04:00
xarmian cba6379c4d test: the stalled-transport test waits for the worker to be inside Publish before clearing the transport (BUG-3464) (#1850) 2026-10-07 05:09:33 -04:00
xarmian e774268d63 fix(plugin): drop the pinned version so installs follow the commit (BUG-3463) (#1848)
A version in plugin.json pins every installed copy until the string
changes (code.claude.com/docs/en/plugins/loading, "Versions and
updates"). 0.3.3 stood from 2026-08-27 while 8 commits changed plugin/,
so marketplace installs kept the August skills. With no version, a
relative-path plugin in a git-hosted marketplace is versioned by its
commit, and `claude plugin update` picks up every change.

Measured in a scratch CLAUDE_CONFIG_DIR against a git marketplace with a
commit to plugin/ after install: pinned 0.3.3 answered "already at the
latest version (0.3.3)" and kept the old copy; unpinned updated
be0122112e5a -> d6312f1d8c6d with the change in the cache.

A guard test refuses a version in plugin.json or the marketplace entry.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 03:33:30 -04:00
xarmian 0f932b5880 fix(mcp): remote routes answer a missing workspace with no_workspace and the list (TASK-2314) (#1846)
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
2026-10-07 03:14:22 -04:00
xarmian 05c039f131 fix: agents load items with --agent; --format markdown stays body-verbatim and names an empty body (TASK-3451) (#1842)
* 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)
2026-10-07 00:02:09 -04:00
xarmian 4fdc6ed625 feat: in-app tutorials on Pad Cloud, links to getpad.dev/learn elsewhere (TASK-3452) (#1839)
Design and rulings on TASK-3452 (lead, day 89).

- Placement: a first-time card on /console's empty state (Start a new
  project) and in the workspace launchpad's step 2 (Set up your workspace
  with /pad onboard); each vanishes with its screen. The stable way back is
  a Tutorials entry in the user menu's Resources block, right after Docs.
- Dismissal: users.ui_dismissals, a JSON set of keys behind an allow-list
  (models.UIDismissalKeys), read-modify-written under the users row lock,
  so a dismissal holds on every device. GET/PUT /me/ui-dismissals, web only.
- Pad Cloud: the server fetches https://www.getpad.dev/learn/catalog.json
  (a constant; 1h cache, last good copy kept, 5 min back-off) and serves it
  same-origin at /tutorials, with posters proxied at /tutorials/posters/
  {slug} from www.getpad.dev only. The page CSP gains frame-src for
  youtube-nocookie, Cloud only. /console/tutorials hosts the click-to-load
  player and chapters; transcripts link to getpad.dev.
- Self-hosted: both routes 404, no outbound request, no CSP change; every
  surface links to getpad.dev/learn.

Migration 127 / pg 101, after #1837's 126 / 100 (rides rc.7). No CLI or MCP surface.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-06 22:23:29 -04:00
xarmian a27df265bc fix(store): concurrent re-invites no longer report a successful replace as an error (BUG-3460) (#1840)
* test(store): an invite replaced right after it committed still returns its own invitation (BUG-3460)

* fix(store): CreateInvitation reads its row back inside its transaction, so a concurrent re-invite cannot make a successful replace report an error (BUG-3460)
2026-10-06 21:23:38 -04:00
xarmian c204dd3683 fix(cli): json fields print as JSON, metadata labelled for the item's kind; MCP 0.68 (BUG-3448) (#1838)
* 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)
2026-10-06 18:54:37 -04:00
xarmian 1d94d038a4 fix: the first-run launchpad follows an onboarding agent live (BUG-3447) (#1837)
* test: collection create publishes an event; the launchpad and connect banner follow agent writes live (BUG-3447)

* fix(web,server): collection create publishes collection_updated; the launchpad and connect banner follow agent writes live (BUG-3447)

* test(web): a debounce armed in one workspace does not fire into the next (BUG-3447, codex r1)

* fix(web): the live reload and the banner re-check fire only for the workspace they were armed in (BUG-3447, codex r1)

* test(web): an event during the first load or check still triggers the live refresh (BUG-3447, codex r2)

* fix(web): an event before the first load or check commits still triggers the live refresh (BUG-3447, codex r2)

* test(web): a failed quiet re-check that overtook the first check leaves the banner showing (BUG-3447, codex r3)

* fix(web): a failed quiet re-check from the unknown state falls back to showing the banner (BUG-3447, codex r3)

* test: an agent's collection is agent activity (dashboard, banner, e2e incl. the Agent-connected step) (BUG-3447, lead)

* fix: a collection records its source; an agent-created collection is agent activity for the launchpad and the banner (BUG-3447, lead)

* test(e2e): the launchpad follows an onboarding agent from outside the tab, red on main for each outcome (BUG-3447, lead)

* test(store): collection provenance survives export/import; the census sees the new column (BUG-3447, codex r5)

* fix(store): collection provenance travels in the workspace bundle, validated on import; census records the column (BUG-3447, codex r5)

* test(store): an unrecognised bundle source is stored as '' (BUG-3447)
2026-10-06 18:31:29 -04:00
xarmian a91fcce7f9 fix: blank workspaces accept the library's own triggers and scopes (BUG-3446) (#1836)
* 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
2026-10-06 16:48:50 -04:00
xarmian f74f626fcb fix(server): register-with-code claims the invitation atomically; a gone invitation refuses and rolls the account back (BUG-3438) (#1834)
* test(server): register-with-code through an invitation gone before its claim joins nothing (BUG-3438)

* fix(server): register-with-code claims the invitation in the membership's transaction; a gone invitation refuses and rolls the account back (BUG-3438)

* test(server): a cloud signup refused for a gone invitation is rolled back; a failed claim beside a membership is not a join (BUG-3438, codex r1)

* fix(server): the signup rollback removes the account's verification tokens with it; a membership the claim did not write is not a join (BUG-3438, codex r1)

* test(server): a session minted in the window does not block the signup rollback; a refused signup keeps the emailed proof for the retry (BUG-3438, codex r2)

* fix(server): the signup rollback removes the account's sessions under its row lock and restores a spent invitation proof (BUG-3438, codex r2)

* test(server): a reset token minted in the window does not block the signup rollback (BUG-3438, codex r3)

* fix(server): the signup rollback is the full account deletion; a proof whose consume reported an error counts as possibly spent (BUG-3438, codex r3)

* fix(server): an invitation proof whose consume errored is never restored from what the caller sent (BUG-3438, codex r4)

* test(server): an invited signup reports the role the account holds (BUG-3438, codex r5)

* fix(server): an invited signup reports the role the claim kept (BUG-3438, codex r5)

* fix(server): the signup rollback kicks the account's connections, as every account deletion does (BUG-3438, codex r6)

* fix(server): the signup rollback kicks even when the deletion reports an error (BUG-3438, codex r7)

* test(server): on open registration a refused invited signup keeps the account, joins nothing and says why (BUG-3438, lead ruling)

* test(web): a registration kept without its invitation says so on the join page and lands nowhere (BUG-3438)

* fix(server,web): a refused invited signup is rolled back only when the invitation was its admission; on open registration the account is kept and told (BUG-3438, lead ruling)

* test(server): a claim refused before commit needs no reconcile read; a failed rollback is not reported as one (BUG-3438, codex r9)

* fix(server): a claim refused before commit skips the reconcile read; a failed rollback answers 500; docs name the admin case (BUG-3438, codex r9)
2026-10-06 15:48:30 -04:00
xarmian 2f3cf402b1 fix(timeline): 'commented on update' only for a comment an update carried (BUG-3437) (#1832)
* fix(timeline): 'commented on update' only for a comment an update carried (BUG-3437)

TimelineCommentCard showed 'commented on update' whenever the comment had
an activity_id, and every comment has one: a comment or reply written on
its own links to its own 'commented' activity, so a plain
'pad item comment' read as if it were attached to a change.

The store's comment list join (the one that already reads the agent name
off the linked activity) now also reads its action into
Comment.LinkedActivityAction (json:"-"), and the timeline sets an
additive, omitempty TimelineEntry.comment_on_update when that action is
anything but 'commented'. The card keys the label on it. The comments
API, the CLI and MCP keep their shape (the new field is never serialized
outside the timeline, which MCP does not expose).

Tests: the server test writes a standalone comment, a reply and an update
carrying a comment through the real routes (web, CLI and MCP share the
comments route) and checks the flag, with the old rule and an always-false
mutant each red; the web test pins the card and the entry list's
pass-through, each red with its half reverted.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(timeline): a comment that arrives live keeps 'commented on update' (BUG-3437)

The label comes from the server-set comment_on_update. Comments that arrive
while the timeline is open come through the SSE refresh, which re-reads the
timeline endpoint (nothing builds a comment entry client-side), so they carry
the flag. This pins it: red if the entry list stops passing the flag.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-06 12:57:14 -04:00
xarmian 8c8deeeb89 fix: an invite asks an existing user instead of adding them; decline; CLI invitations (BUG-2136 U1) (#1831)
* 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)
2026-10-06 12:16:32 -04:00
xarmian f9d79aea94 fix(store): relationship reads exclude soft-deleted collections (BUG-3434) (#1828)
* test(store): relationship reads exclude soft-deleted collections; the unparented rule stays structural (BUG-3434, red)

* fix(store): relationship reads (links, parent, parent map, lineage, blocks edges) exclude soft-deleted collections; getItemLinkQ, the write paths' read-back, stays inclusive (BUG-3434)

* test(store): exercise both ends of parent and blocks links (BUG-3434)

* test(store): the parent filter ignores a parent in a soft-deleted collection (BUG-3434, codex r1, red)

* fix(store): the parent filter treats a parent in a soft-deleted collection as no parent, on both list paths (BUG-3434, codex r1)
2026-10-06 09:59:38 -04:00
xarmian b67e78343a test(materialize): fire the idle timer mid-job on purpose, not by wall-clock luck (BUG-3435) (#1827)
TestSupervisorIdleTimerDuringJob staged 'the idle timer fires while a job
runs' with a 300ms timer, a 200ms pause and a 400ms job. On a loaded macOS
runner the pause stretched past the timer, the worker went idle BETWEEN the
jobs, and the test failed with '2 spawns'. Widening the pause to 320ms
reproduces that exact failure 3/3.

Now nothing fires on its own (a one-hour idle). A new script command,
await:<path>, holds the job inside the worker until the test releases it;
the test fires the timer itself (back-dating lastJob, Reset(0)) once the
job has provably started, and waits on a new test hook, onIdleBusy, which
idleStop calls when it finds the slot taken. Then it releases the job,
checks one spawn and that the job's end re-armed the timer on the same
worker, and fires once more to see the worker stop.

Mutants: idleStop proceeding without the slot check (red 10/10), and
armIdle not recording the job's end (red). 30/30 green with every CPU
saturated, and under -race.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-06 09:25:18 -04:00
xarmian 10a1683b83 fix: the access epoch tracks the live collection set; deleted collections leave the local index; the open-children guard ignores them (BUG-3428 phase 2) (#1826)
* test(server): the epoch tracks the live collection set; the guard ignores deleted collections' children (BUG-3428 phase 2, red)

* fix: the access epoch fingerprints the live collection set; the index and delta drop deleted collections' rows; the open-children guard ignores them (BUG-3428 phase 2)

* test(server): an item door's epoch is never newer than its rows (BUG-3428 phase 2, codex r1, red)

* fix(server): item doors read the live collection set before their rows, so a response's epoch is never newer than its rows (BUG-3428 phase 2, codex r1)

* test(server): a grant-filtered delta's epoch is never newer than its rows (BUG-3428 phase 2, codex r2, red)

* fix(server): the delta door resolves its live grant set before its rows too (BUG-3428 phase 2, codex r2)
2026-10-06 07:59:20 -04:00
xarmian e3668f0fea fix(server): the item-grant filter keeps an app request's read ceiling (BUG-3424) (#1825)
A caller holding item grants is filtered by guestResourceFilter's fullCollIDs
and grantedItemIDs instead of visibleCollectionIDs, and only
visibleCollectionIDs applied an app request's read ceiling (SPEC-6 U6a). So an
app principal with item grants would have been served rows outside its
ceiling on /items-index, /items-changes, the SSE snapshot and every other
grant-path door. Unreachable today (app tokens are refused on /api/v1 and the
app router exposes none of those doors), so this is hardening.

guestResourceFilterCoreQ now narrows both lists for an app request: full
collections to the ceiling, granted items to those whose collection is in it
(store.ItemIDsInCollectionsQ). nil/nil stays nil/nil, and a nil fullCollIDs
beside grants is not widened to the ceiling.

The test drives the shared filter with an app context: control (no app
context) shows the fixture's out-of-ceiling rows; with a ceiling they are
gone and the in-ceiling grant stays. Mutants: dropping the ceiling, and
skipping only the item filter, each go red.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-06 01:33:24 -04:00
xarmian a7f98c01fc fix(store): search, tags, children and progress exclude soft-deleted collections (BUG-3428, phase 1) (#1824)
* test(store): search, tags, children and progress must exclude soft-deleted collections (BUG-3428, red)

* fix(store): global search, tags, children and progress exclude soft-deleted collections; the open-children guard keeps them (BUG-3428)

* test(store): bare-number search excludes soft-deleted collections (BUG-3428)

* test(store): has_children agrees with the children list; tighter progress and facet assertions (BUG-3428, codex r1, red)

* fix(store): has_children leaves out children in soft-deleted collections (BUG-3428, codex r1)
2026-10-06 01:04:29 -04:00
xarmian 1e75d19d1f fix(store): rebindQuery skips SQL comments and quoted identifiers (BUG-3430) (#1822)
* test(store): rebindQuery must not bind inside SQL comments or quoted identifiers (BUG-3430, red)

* fix(store): rebindQuery skips SQL comments and quoted identifiers (BUG-3430)

* fix(store): rebindQuery counts nested block comments, as Postgres does; edge-case tests (BUG-3430, codex r1)

* docs(store): rebindQuery names the Postgres string forms it does not lex (BUG-3430, codex r2)

* test(store): fail loudly when store SQL adds a $$ or E'' string, which rebindQuery does not lex (BUG-3430)
2026-10-05 22:54:52 -04:00
xarmian 1082c4aa16 fix(store): reads exclude soft-deleted collections' items (BUG-3425) (#1820)
* test(store): ListItems, its search and the graph edges must exclude soft-deleted collections (BUG-3425, red)

* test(server): graph nodes exclude soft-deleted collections; the account export keeps them (BUG-3425)

* fix(store): ListItems, its search and the graph exclude soft-deleted collections; the account export opts back in (BUG-3425)

* test(store): role counts exclude soft-deleted collections; search honours the opt-out (BUG-3425, codex r1, red)

* fix(store): role counts exclude soft-deleted collections, as the role lanes do (BUG-3425, codex r1)

* docs(store): doneFiltersForWorkspace's comment no longer names GetRoleBreakdown as counting deleted collections (BUG-3425, codex r2)

* fix(store): keep SQL comments out of the role-count query (an apostrophe broke PG rebind); BUG-3333 expects a deleted collection's doc unlisted (BUG-3425)
2026-10-05 21:18:56 -04:00
xarmian 8bb4616a3d fix(urlimport): marshal OpenAPI examples with libopenapi's own yaml package (TASK-3422) (#1818)
libopenapi 0.41 moved its Node type to github.com/pb33f/go-yaml. Marshalling
a MediaType example with go.yaml.in/yaml/v4 saw a plain struct, and the
example block no longer rendered the author's YAML (TestConvertOpenAPI_Petstore:
markdown missing "name: Rex"). The petstore markdown is byte-identical to
libopenapi 0.40's output with this change. The test now pins the whole
example block.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 20:14:17 -04:00
xarmian a1c36fca04 fix(server): the upgrade diff states what a widened delegated access grants (BUG-3427) (#1817)
A widened delegated-access row told the owner the change was recorded and
granted nothing until delegated access is enabled (TASK-3399), but U5b-2
opened delegated sign-in. appAdmitDelegated grants the lesser of what the
person consented to and what the manifest offers now, and a consent is
capped at the level offered when it was given, so the row now says that
existing sign-ins keep their consented access and people who sign in after
approval can consent to the new level. Stale 'refused until TASK-3399'
comments in app_api.go and install_clients.go are corrected.

Tests pin the detail string and the behaviour behind it (a read consent
stays read after the widening; a write consent given after it writes).
Mutants: the old string, and admission granting the manifest's level, each
go red.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 19:45:01 -04:00
xarmian b3e6815e0c fix(store): wiki backlinks exclude sources in soft-deleted collections (BUG-3344) (#1814)
* test(store): wiki backlinks must exclude sources in soft-deleted collections (BUG-3344, red)

* fix(store): wiki backlinks exclude sources in soft-deleted collections (BUG-3344)
2026-10-05 18:38:46 -04:00