Skip to content

Paperclip Updates - #1

Open
dirk-miller wants to merge 2499 commits into
dirk-miller:masterfrom
paperclipai:master
Open

Paperclip Updates#1
dirk-miller wants to merge 2499 commits into
dirk-miller:masterfrom
paperclipai:master

Conversation

@dirk-miller

Copy link
Copy Markdown
Owner

No description provided.

dirk-miller pushed a commit that referenced this pull request Mar 26, 2026
**#1 — Missing `description` field in fields table**
The create body example included `description` and the schema confirms
`description: z.string().optional().nullable()`, but the reference table
omitted it. Added as an optional field.

**#2 — Concurrency policy descriptions were inaccurate**
Original docs described both `coalesce_if_active` and `skip_if_active` as
variants of "skip", which was wrong. Source-verified against
`server/src/services/routines.ts` (dispatchRoutineRun, line 568):

  const status = concurrencyPolicy === "skip_if_active" ? "skipped" : "coalesced";

Both policies write identical DB state (same linkedIssueId and
coalescedIntoRunId); the only difference is the run status value.
Descriptions now reflect this: both finalise the incoming run immediately
and link it to the active run — no new issue is created in either case.

Note: the reviewer's suggestion that `coalesce_if_active` "extends or
notifies" the active run was also not supported by the code; corrected
accordingly.

**#3 — `triggerId` undocumented in Manual Run**
`runRoutineSchema` accepts `triggerId` and the service genuinely uses it
(routines.ts:1029–1034): fetches the trigger, enforces that it belongs to
the routine (403) and is enabled (409), then passes it to dispatchRoutineRun
which records the run against the trigger and updates its `lastFiredAt`.
Added `triggerId` to the example body and documented all three behaviours.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
futhgar and others added 29 commits August 17, 2026 14:02
…S temp dir (#8283)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work, and it can run as a self-hosted instance backed by an embedded
PostgreSQL database.
> - To let agents work in isolation, Paperclip supports worktree-local
instances, gated by `PAPERCLIP_IN_WORKTREE` / `PAPERCLIP_HOME` (see
`server/src/worktree-config.ts`, `cli/src/config/home.ts`).
> - Those worktree env vars can leak into a *primary* instance's
environment (inherited from an agent/worktree shell, or persisted into
the instance env file). When they do, `paperclipai run` resolves the
data root from `PAPERCLIP_HOME` and rewrites `config.json` to point the
DB/backups/logs/storage at `$PAPERCLIP_HOME/instances/<id>/…`.
> - If `$PAPERCLIP_HOME` is a throwaway dir under the OS temp dir, the
primary instance boots a brand-new **empty** database. Every login then
fails (`better-auth` logs `User not found`; the UI returns a generic
`401`), so it looks like a *password* problem while the real data sits
untouched in `~/.paperclip`. Nothing warns that the control-plane DB is
ephemeral.
> - This pull request makes that situation non-silent: the database
preflight check (also surfaced by `doctor`) emits a `warn` when a
worktree-mode instance's embedded-postgres data dir is inside the OS
temp directory, with clear remediation.
> - The benefit is that a confusing total lockout becomes an obvious,
actionable warning the operator sees at every `run` and `doctor`.

## Linked Issues or Issue Description

Refs #8282

Related PRs (not duplicates — complementary work on the same area):
- #3030 — *stop leaking server worktree env into unrelated local adapter
heartbeats* (tackles one **leak vector** of the same root cause; this PR
adds **detection** of the resulting bad state).
- #3899 — *fix(db): refuse side-started embedded migration instances*
(adjacent embedded-postgres safety hardening).

## What Changed

- `cli/src/checks/database-check.ts`: for `embedded-postgres` mode, emit
`status: "warn"` when the resolved data dir is inside `os.tmpdir()`
**and** `PAPERCLIP_IN_WORKTREE === "true"`. The message explains the
ephemerality + likely env leak; the repair hint says to unset
`PAPERCLIP_HOME` / `PAPERCLIP_IN_WORKTREE` (or pass `--data-dir`). Added
a small `isInsideOsTmpDir()` helper.
- Intentionally gated on worktree mode so deliberate ephemeral/CI
instances that use a temp data dir without `PAPERCLIP_IN_WORKTREE` are
**not** flagged.
- `cli/src/__tests__/database-check.test.ts` (new): covers pass
(persistent dir), warn (worktree-mode temp dir), and no-warn (temp dir
without worktree mode).

## Verification

```
# in cli/
pnpm exec vitest run src/__tests__/database-check.test.ts   # 3 passed
pnpm exec vitest run src/__tests__/doctor.test.ts           # passes (no regression)
pnpm exec tsc --noEmit                                       # no new errors in changed file
```

Manual repro of the underlying bug (no warning before this change):

```bash
PAPERCLIP_IN_WORKTREE=true PAPERCLIP_HOME="$(mktemp -d)/.paperclip-worktrees" paperclipai run
# -> boots an empty DB under /tmp; logins fail with "User not found".
# With this change, run/doctor now print a Database WARN pointing at the cause + fix.
```

Note for transparency: one unrelated test (`worktree.test.ts >
pauseSeededScheduledRoutines`) fails *locally only* because it shells
out to a real `pnpm install` that times out in a sandbox — it does not
touch `database-check`. Pre-existing `tsc` errors under
`server/src/services/plugin-*` (missing `@paperclipai/plugin-sdk` build
artifact) are also unrelated to this change.

## Risks

Low risk. Additive, non-fatal `warn` only — no behavior change to
startup or existing `pass`/`fail` paths, and gated on
`PAPERCLIP_IN_WORKTREE` so it does not fire for intentional ephemeral/CI
temp data dirs. A stricter follow-up (refuse to start the primary `run`
against a temp-dir data dir unless explicitly opted in) is possible but
intentionally out of scope here.

## Model Used

- **Provider / model:** Anthropic Claude — Opus 4.8
- **Exact model ID:** `claude-opus-4-8` (1M-context variant)
- **Context window:** 1M tokens
- **Reasoning mode:** extended thinking enabled
- **Capabilities used:** agentic tool use via Claude Code (repository
exploration, file edits, local shell, and running the vitest suite
locally before pushing)

The change was authored by @futhgar with this model as an assistant; the
diagnosis, fix, and tests were reviewed and verified locally.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have run tests locally and they pass (new test + affected
`doctor` suite; see Verification for one unrelated, environment-only
failure)
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots (N/A — no UI change)
- [x] I have updated relevant documentation to reflect my changes (the
warning message + repair hint are self-documenting; no separate docs
change needed)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (pending CI run)
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(pending review)
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: futhgar <futhgar@users.noreply.github.com>
#8546)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents run via per-adapter execute paths; the `cursor_cloud` adapter
runs the agent in Cursor's cloud (remote), orchestrated server-side via
the Cursor Agent SDK
> - Local adapters receive a run-scoped Paperclip JWT
(`supportsLocalAgentJwt=true`) injected as `PAPERCLIP_API_KEY` so the
agent can call the Paperclip API; `cursor_cloud` is intentionally
`supportsLocalAgentJwt=false` (no JWT minted for a remote worker)
> - But `buildPaperclipEnv` always sets `PAPERCLIP_API_URL` (defaulting
to the local runtime host), so the remote cloud worker is handed a
callback URL it can neither reach nor authenticate against
> - Any agent-initiated Paperclip API call from the cloud worker
therefore fails with a 401 (or is unreachable), producing log noise and
confusing failures
> - This pull request drops the callback wiring when there is no usable
key, so cloud-side Paperclip tools degrade to a clean no-op
> - The benefit is no spurious 401s from remote cloud runs, with run
results unaffected (delivered server-side via the Cursor Agent SDK)

## Linked Issues or Issue Description

No existing public issue — describing the bug inline (per
`.github/ISSUE_TEMPLATE/bug_report.yml`):

**What happened**

`cursor_cloud` runs emit 401s when the remote cloud agent attempts
Paperclip API calls. Root cause: `buildPaperclipEnv`
(`packages/adapter-utils/src/server-utils.ts`) always sets
`PAPERCLIP_API_URL` (local runtime default), while `cursor_cloud` has
`supportsLocalAgentJwt=false`, so no `PAPERCLIP_API_KEY` is minted — URL
present, key absent → 401 / unreachable from `buildWakeEnv` in
`packages/adapters/cursor-cloud/src/server/execute.ts`.

**Expected behavior**

A remote cloud worker that is not issued a run JWT should not attempt
(and fail) Paperclip API callbacks.

**Steps to reproduce**

1. Configure a `cursor_cloud` agent (runs in Cursor's cloud;
`supportsLocalAgentJwt=false`).
2. Trigger a run that causes the cloud agent to make a Paperclip API
call.
3. Observe a 401 (or unreachable) because `PAPERCLIP_API_URL` points at
an unreachable local runtime and no key is present.

**Paperclip version**

Reproduced on current `master` (cutover base `e68188c43`).

**Deployment mode**

Self-hosted control plane, `cursor_cloud` adapter (remote execution in
Cursor's cloud).

**Related PRs (searched; none duplicate this fix):**

- #8197 — `claude_local` opt-out of the sandbox *bridge* for
direct-reachable remote SSH targets. Related family, but the opposite
situation: that path keeps the callback because the remote is reachable
**and** has a run token. `cursor_cloud` has neither, so here the
callback is removed.
- #8130, #4794, #8025 — `PAPERCLIP_API_URL`/loopback injection for
**local** agents (distinct from the remote cloud worker case).
- #401 — alternative agent-auth scheme (run-ID header when no bearer
token); different approach, not overlapping with this targeted fix.

## What Changed

- `packages/adapters/cursor-cloud/src/server/execute.ts`: in
`buildWakeEnv`, when there is no usable `PAPERCLIP_API_KEY`, delete
`PAPERCLIP_API_URL` and `PAPERCLIP_API_BRIDGE_MODE` so the remote worker
performs no Paperclip API callbacks. Informational `PAPERCLIP_*` vars
(run id, agent id, company id, task, wake reason) still flow. When a key
*is* present (operator-provided), the URL is retained.
- `packages/adapters/cursor-cloud/src/server/execute.test.ts`: new test
asserting no callback vars are injected when no run JWT is present;
positive assertion that the URL is retained when a key is present.

## Verification

- `pnpm exec vitest run
packages/adapters/cursor-cloud/src/server/execute.test.ts` → **5/5
pass**.
- `pnpm --filter @paperclipai/adapter-cursor-cloud typecheck` →
**green**.
- Confirmed result delivery does not depend on this callback:
`execute()` reads results server-side via `Agent.getRun()` and
`run.wait()`.

## Risks

- **Low risk.** Only affects the env handed to remote `cursor_cloud`
workers. No schema/migration/behavioral change to result delivery (which
is server-side). When an operator explicitly provides
`PAPERCLIP_API_KEY`, the callback URL is retained, preserving
intentional callback setups.

## Model Used

- **Claude Opus 4.8** (Anthropic), extended/high reasoning mode, via the
Cursor agent with tool use + code execution. Diagnosis grounded in the
adapter/runtime code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above (none found)
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub references)
- [x] My branch name describes the change
(`fix/cursor-cloud-skip-unreachable-callback`) and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] I have updated relevant documentation to reflect my changes (N/A —
no documented behavior changes)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (pending CI run)
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(pending review)
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Sebastian Heyneman <sebastian@joinnova.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…callback bridge (#8978)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Managed agents run inside a sandbox and reach the Paperclip server
only through the sandbox callback bridge, which forwards a fixed route
allowlist (`DEFAULT_SANDBOX_CALLBACK_BRIDGE_ROUTE_ALLOWLIST`)
> - The `paperclip-create-agent` skill instructs an agent to call
adapter/icon discovery endpoints, compare existing agent configurations,
submit a hire request, and link the resulting approval to its source
issue
> - None of those routes were on the bridge allowlist, so a sandboxed
agent following the skill correctly hit `Route not allowed` on every
call — including the hire `POST` itself — making hiring impossible from
inside a sandbox
> - This pull request adds the six routes the skill uses to the bridge
allowlist, while keeping direct agent creation (`POST
/api/companies/:id/agents`) denied
> - The benefit is that hiring works end-to-end for sandboxed agents
through the approval-gated `agent-hires` path, without widening the
bridge beyond what the skill needs

## Linked Issues or Issue Description

No public issue exists; describing the bug in-PR (bug template fields):

- **What happened:** A managed agent running in a sandbox followed the
`paperclip-create-agent` skill and got `Route not allowed` from the
callback bridge on every endpoint the skill documents — adapter
discovery (`/llms/agent-configuration.txt`,
`/llms/agent-configuration/:adapterType.txt`, `/llms/agent-icons.txt`),
config comparison (`GET /api/companies/:id/agent-configurations`), the
hire submission (`POST /api/companies/:id/agent-hires`), and approval
linking (`POST /api/issues/:id/approvals`).
- **Expected behavior:** An agent with hiring permission can complete
the hire flow from inside a sandbox; the bridge forwards the skill's
routes and the server enforces authorization (`canCreateAgents`).
- **Impact:** Hiring by sandboxed agents was fully broken — the failure
is in the transport allowlist, not permissions, so no configuration
could work around it.

Related: #8981 (companion fix making the `paperclip-create-agent` skill
available to agents that can hire; supersedes #8823). The two changes
serve the same end-to-end hire flow but are independently mergeable —
this PR is purely the bridge transport allowlist.

Supersedes #8853.

## What Changed

- `packages/adapter-utils/src/sandbox-callback-bridge.ts`: add six
routes used by the `paperclip-create-agent` skill to
`DEFAULT_SANDBOX_CALLBACK_BRIDGE_ROUTE_ALLOWLIST` (three `GET /llms/...`
discovery routes, `GET .../agent-configurations`, `POST
.../agent-hires`, `POST /api/issues/:id/approvals`), with a comment
documenting why direct agent creation stays denied
- `packages/adapter-utils/src/sandbox-callback-bridge.test.ts`: assert
the six routes are allowed, and add negative cases proving the regexes
do not over-match (no `POST .../agents`, no non-`.txt` or arbitrary
`/llms` files, no `agent-hires` sub-resources)

## Verification

- `npx vitest run
packages/adapter-utils/src/sandbox-callback-bridge.test.ts` — 13/13
tests pass locally
- `npx tsc --noEmit -p packages/adapter-utils` — clean
- Manual: run a managed agent in a sandbox, invoke the
`paperclip-create-agent` skill, and confirm the discovery calls, hire
`POST`, and approval linking all pass through the bridge; `POST
/api/companies/:id/agents` still returns `Route not allowed`

## Risks

- Low risk: additive allowlist entries only; anchored regexes with
`[^/]+` segments prevent over-matching (covered by tests)
- The bridge allowlist bounds surface area but does not replace
server-side authorization — the hire `POST` remains approval-gated and
permission-checked (`canCreateAgents`) on the server

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude (Anthropic) — Fable 5 (`claude-fable-5`), extended thinking,
agentic tool use via Claude Code; original diff authored with Claude
Opus 4.8 (1M context)

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…on (#9225)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Local CLI adapters are responsible for starting agent runtimes and
validating that their configured models are usable before a run starts.
> - The OpenCode local adapter checks `opencode models` during model
discovery and preflight validation.
> - On hosts with a shared Ollama daemon, that lightweight metadata call
can transiently queue behind an active generation and time out or return
a short failure.
> - Treating that transient contention as a hard adapter failure
prevents otherwise valid local OpenCode runs from starting.
> - This pull request adds a small bounded retry/backoff around OpenCode
model discovery while keeping the existing per-attempt timeout and
surfacing a final failure when retries are exhausted.
> - The benefit is fewer false adapter failures during local Ollama
contention without changing shared Ollama configuration or hiding
genuinely stuck model discovery.

## Linked Issues or Issue Description

No public GitHub issue exists for this adapter reliability bug.

Bug description:
- What happened: `opencode models` can transiently time out or fail
while a shared local Ollama daemon is busy serving another OpenCode
generation, causing the adapter preflight to fail before the actual run
starts.
- Expected behavior: transient model-list contention should be retried
briefly before declaring the adapter unavailable.
- Steps to reproduce: run an OpenCode local adapter using an
Ollama-backed model while another `opencode run` is actively generating
against the same daemon, then trigger model discovery/preflight during
that contention window.
- Paperclip version/commit: observed on the current Paperclip
master-line OpenCode local adapter before this change.
- Deployment mode: local trusted / local CLI adapter execution with a
shared local Ollama daemon.

Related search:
- Searched public GitHub issues for `opencode models preflight retry`;
no matching issue found.
- Searched public GitHub PRs for `opencode models preflight retry`; no
matching PR found. The only search hit was unrelated OpenClaw gateway
authentication work (#6121).

## What Changed

- Added bounded retry/backoff to OpenCode model discovery: three total
attempts with 2s and 4s waits between failures.
- Preserved the existing 20s per-attempt `opencode models` timeout.
- Retry covers timeout and non-zero process exits, while spawn-level
failures still surface immediately.
- Added unit coverage for transient fail -> timeout -> success behavior
and exhausted retry behavior.
- Updated existing OpenCode environment diagnostic tests with explicit
timeouts for the intentional retry/backoff path.

## Verification

- `pnpm --filter @paperclipai/adapter-opencode-local exec vitest run
src/server/models.test.ts src/server/execute.test.ts` -> 2 files passed,
13 tests passed.
- `pnpm --filter @paperclipai/adapter-opencode-local typecheck` ->
passed.
- `pnpm vitest run
server/src/__tests__/opencode-local-adapter-environment.test.ts` -> 1
file passed, 3 tests passed.
- Branch diff against current `upstream/master` is limited to
`packages/adapters/opencode-local/src/server/models.ts`,
`packages/adapters/opencode-local/src/server/models.test.ts`, and
`server/src/__tests__/opencode-local-adapter-environment.test.ts`.

## Risks

Low risk. This only changes OpenCode model discovery behavior and keeps
the preflight bounded. A genuinely unavailable `opencode models` call
still fails after three attempts, and command spawn failures are not
masked.

## Model Used

OpenAI Codex, GPT-5.5 coding agent, tool-enabled repository editing and
shell verification in a local Paperclip workspace.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Test <test@paperclip.ing>
…10068)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work; issues get commented on by both humans and agents, and the
assignee is woken to act on new comments.
> - #10050 added human-attributed issue comments for chat gateway
plugins, with the host waking the issue's assignee the same way a board
user's comment does.
> - Greptile's review on #10050 flagged that the wakeup guard in
`plugin-host-services.ts` decides whether to wake the assignee using the
issue snapshot fetched *before* the comment was inserted.
> - If another request closes, cancels, unassigns, or reassigns the
issue in the window between that fetch and the wakeup call, the guard
still acts on the stale snapshot — it can wake an agent for a
now-terminal issue, or wake the old assignee instead of the new one.
> - The PR discussion noted the HTTP add-comment route
(`routes/issues.ts`) has the identical pattern outside its
reopen/auto-approval branches, and deferred a fix to a follow-up
covering both call sites — this PR is that follow-up.
> - The fix re-fetches the issue immediately before the wake decision in
both places, so the decision reflects the latest committed state instead
of a pre-insert snapshot.

## Linked Issues or Issue Description

Refs #10050

**Problem or motivation**

Both the plugin-comment wakeup guard (`plugin-host-services.ts`) and the
HTTP add-comment route's wakeup guard (`routes/issues.ts`, outside its
reopen/auto-approval branches) decide whether to wake the issue's
assignee using the issue state fetched before the comment was inserted.
A concurrent close/unassign/reassign landing in that window is invisible
to the guard, so it can enqueue a wakeup for a stale assignee or an
issue that is no longer open.

**Proposed solution**

Re-fetch the issue immediately before the wake decision in both call
sites, and base the assignee/status checks on that fresh read instead of
the earlier snapshot. This shrinks the race window to essentially
nothing (the fetch happens right before the fire-and-forget wakeup
call), and any residual window is already covered by the
heartbeat/checkout machinery re-validating issue status and assignee
ownership when a woken run actually starts.

**Alternatives considered**

Wrap the whole comment-insert + wake-decision sequence in a single
serializable transaction with row locking (rejected for this change —
much larger blast radius across two already-complex handlers for a
wakeup that is explicitly best-effort; the woken run's own re-validation
already makes a stale wake degrade to a no-op rather than incorrect
work). Leaving the plugin path fixed but not the HTTP route (rejected —
that was the exact gap the original PR discussion flagged as needing a
follow-up covering both call sites).

**Roadmap alignment**

Bug fix / hardening follow-up to #10050; no change to planned core
roadmap items.

## What Changed

- `server/src/services/plugin-host-services.ts`:
`issues.createComment`'s assignee-wakeup guard now re-fetches the issue
after the comment is inserted and bases the assignee/status checks on
that fresh read, instead of the snapshot fetched before the insert.
- `server/src/routes/issues.ts`: the `POST /issues/:id/comments` route's
wakeup guard (outside the reopen/auto-approval branches, which already
use post-mutation state) now does the same re-fetch before deciding
whether — and whom — to wake.
- Adds regression coverage for both:
- `server/src/__tests__/plugin-orchestration-apis.test.ts`: a new
embedded-Postgres test holds a row lock on the issue to
deterministically force the race (comment-insert's internal update
blocks until a concurrent transaction commits a cancellation), then
asserts no wakeup is enqueued.
- `server/src/__tests__/issue-comment-reopen-routes.test.ts`: two new
mocked-service tests assert the route skips the wakeup when the fresh
re-fetch shows the issue cancelled, and wakes the freshly reassigned
agent (not the pre-insert snapshot's assignee) when the fresh re-fetch
shows a different assignee.

## Verification

- `pnpm --filter @paperclipai/server typecheck` — clean.
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/plugin-orchestration-apis.test.ts` — 13/13 (1 new).
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/issue-comment-reopen-routes.test.ts` — 74/74 (2 new).
- `pnpm --filter @paperclipai/plugin-sdk exec vitest run
tests/host-client-factory.test.ts` — 14/14.
- Broader sweep of 38 `routes/issues.ts`-adjacent test files (447 tests)
— all passing, confirming the added re-fetch doesn't change behavior for
any existing
reopen/auto-approval/interrupt/scheduled-retry/dependency-wake scenario.

## Risks

Low risk. Both changes are additive guards around an existing
best-effort, fire-and-forget wakeup (failures already logged, not
thrown) — no change to the comment-write path itself, response shape, or
status codes. The HTTP route's fix only touches the plain (non-reopen,
non-auto-approval) wake-decision path; the reopen and auto-approval
branches already used post-mutation state for the reasons documented
inline and are unchanged. Adds one extra `SELECT` per comment on each
call site, negligible relative to the existing query volume in both
handlers.

## Model Used

Claude Sonnet 5 (`claude-sonnet-5`), extended thinking, tool use
enabled, via Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I searched the GitHub PR list (open and recently closed) for
similar PRs; found no duplicate — this is a direct follow-up to the
review discussion on #10050
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
ticket id
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: anicca <annica@Michaels-Mac-Studio.local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Local session adapters persist task sessions so later wakes can
resume the same conversation
> - Session reuse correctly resets when effective execution
configuration changes
> - The workspace fingerprint currently includes the issue row's
`updatedAt` timestamp
> - Adding a comment advances that timestamp even though workspace
configuration is unchanged
> - The next same-issue wake therefore discards a valid task session and
starts cold
> - This pull request excludes that volatile timestamp while retaining
actual workspace settings in the fingerprint
> - The benefit is reliable same-task continuation without weakening
configuration-freshness safety

## Linked Issues or Issue Description

No public issue exists. The inline report below follows the bug report
template.

### Pre-submission checklist

- [x] I searched existing open and closed issues and found no duplicate.
- [x] I reproduced the bug on the latest release and current `master`.
- [x] I confirmed the error originates in Paperclip core fingerprinting,
not an adapter, provider, or local configuration.

### What happened?

On Paperclip 2026.720.0 and current `master`, a comment on an issue
changes
`issues.updated_at`. Heartbeat session fingerprinting includes that
value under
`workspaceConfig.issueConfigRevisionAt`, so the next wake for the same
issue
reports a workspace-config change and refuses the saved task session.

### Expected behavior

Comment-only and other non-configuration issue updates should be
delivered as
wake deltas without invalidating the task session. Changes to the
execution
mode, issue workspace settings, project policy, environment,
instructions,
model, secrets, or other effective run configuration must still reset
it.

### Steps to reproduce

1. Complete a local session-adapter run for an issue and retain its task
session.
2. Add a comment to the issue without changing execution configuration.
3. Wake the same agent for the same issue.
4. Observe `changedCategories: ["workspaceConfig"]` and a fresh session.

### Paperclip version or commit

Reproduced on Paperclip 2026.720.0 and current `master`.

### Deployment mode

Self-hosted server.

### Installation method

npm global install; also reproduced from the current source tree.

### Agent adapter(s) involved

Codex exposed the symptom. The bug is in core fingerprint construction
and is
not adapter-specific.

### Database mode

External Postgres. The bug is not database-specific.

### Access context

Board comments trigger the timestamp change; the subsequent agent wake
exposes
the reset.

### Node.js version

Node.js 22.

### Operating system

Ubuntu 24.04.

### Relevant logs or output

The next run records `changedCategories: ["workspaceConfig"]` and starts
a
fresh session after a comment-only mutation.

### Relevant config

No unusual configuration is required.

### Additional context

The regression test exercises the fingerprint directly on current
`master`.

### Privacy checklist

- [x] I reviewed the report for PII, credentials, private paths, company
names, and instance-local identifiers.

## What Changed

- Copy and sanitize the session workspace-fingerprint input before
hashing.
- Exclude only `issueConfigRevisionAt`, which reflects general issue
mutation
  rather than workspace configuration.
- Add regression coverage proving comment timestamps preserve the
session while
  real workspace mode/settings changes still reset it.

## Verification

- `pnpm exec vitest run
server/src/__tests__/heartbeat-workspace-session.test.ts`
  - 120 tests passed.
- `pnpm --filter @paperclipai/server typecheck`
  - passed.
- `git diff --check`
  - passed.

## Risks

Low risk. A general issue update no longer rotates the adapter session
solely
because its row timestamp changed. The fingerprint still includes issue
workspace settings, issue adapter overrides, project workspace policy,
environment, instructions, runtime skills, secrets, model profile,
adapter
configuration, and agent runtime configuration, so actual
execution-config
drift continues to reset.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected - check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, `gpt-5`, context-window size not exposed, reasoning and
tool use
  enabled.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Uliana Savostenko <ulia@MacBook-Air.local>
## Thinking Path

> - Paperclip uses durable task sessions so local adapters can resume
work across sequential heartbeat runs.
> - `execution_review_requested` and `execution_changes_requested` are
issue-local execution-policy handoffs, not new task assignments.
> - The existing `agent_task_sessions` lookup, adapter session codec,
workspace resolution, and effective config freshness checks already
decide whether reuse is safe.
> - Treating those two handoff wake reasons as unconditional
fresh-session boundaries discards a valid saved task session before
adapter resume can be attempted.
> - This makes Dev → CodeReview → Dev loops repeatedly cold-start even
when task, issue, agent, adapter, workspace, and config identity are
unchanged.
> - The fix is to let normal review/change-request handoffs reach the
durable task-session path while preserving explicit fresh-session and
unsafe-boundary resets.

## Linked Issues or Issue Description

Fixes #8246.

cc @cryppadotta — this is the narrow handoff-session policy change
discussed there: normal `execution_review_requested` /
`execution_changes_requested` wakes no longer force a fresh task session
by wake reason alone, while assignment, approval, review-participant
recovery, timer wakes, explicit `forceFreshSession`, and
config/workspace/model/session freshness still keep their safety
boundaries.

## What Changed

- Removed normal `execution_review_requested` and
`execution_changes_requested` from the unconditional task-session reset
policy.
- Kept fresh-session boundaries for:
  - `issue_assigned`
  - `execution_approval_requested`
  - `execution_review_participant_recovery`
  - `heartbeat_timer`
  - explicit `forceFreshSession`
  - existing config/model/workspace/session freshness reset paths
- Updated heartbeat session-policy tests so execution handoffs are
resume-eligible by wake reason alone.
- Preserved PF-4 timer-wake behavior and its explicit reset reason.

## Verification

- `npx pnpm@9.15.4 exec vitest run
server/src/__tests__/heartbeat-workspace-session.test.ts
server/src/__tests__/heartbeat-timer-wake-session-reset-pf4.test.ts
server/src/__tests__/codex-local-execute.test.ts
server/src/__tests__/issue-comment-reopen-routes.test.ts
--reporter=verbose` — 220 tests passed.
- `npx pnpm@9.15.4 --filter @paperclipai/server typecheck` — passed.
- `git diff --check` — passed.
- `coderabbit review --agent -t committed --base origin/master` — 0
findings.

## Risks

- Moderate behavior change in session-boundary policy: normal
review/change-request handoffs may now reuse a saved per-task session
when the existing identity/freshness checks pass.
- Safety boundaries remain in place for new assignments, approval gates,
review-participant recovery, timer/discovery wakes, explicit
fresh-session requests, and config/model/workspace/session drift.
- If a saved session is stale or incompatible, existing freshness/resume
fallback behavior still handles reset/fresh execution.

> For core feature work, check ROADMAP.md first and discuss it in #dev
before opening the PR. Feature PRs that overlap with planned core work
may need to be redirected — check the roadmap first. See
CONTRIBUTING.md.

## Model Used

OpenAI GPT-5.5. Tool use and local verification were enabled.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
Paperclip ticket ID
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes — N/A,
server policy/test-only change
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: santastabber <184111696+santastabber@users.noreply.github.com>
)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Plugins can extend Paperclip with their own multi-step
workflow/graph engines that own an issue's lifecycle across many agent
handoffs
> - When such a plugin-managed issue legitimately stays `in_progress`
for a while (e.g. an anchor issue parked at a fan-out step, waiting on
child issues it spawned), Paperclip's generic recovery mechanisms have
no way to know that's intentional
> - The first commit on this branch fixed one such mechanism
(`decideSuccessfulRunHandoff`) to skip plugin-owned issues, and was
deployed to a real instance to verify the fix
> - Watching that same instance afterward, the identical symptom
(repeated "give a disposition" nags, the agent repeating "completed",
the plugin's own enforcement correctly reverting the status) recurred on
the same class of issue — meaning a second, independent code path had
the exact same gap
> - Traced it to `reconcileStrandedAssignedIssues` in `service.ts`: it
detects a stale successful-run-handoff corrective run via
`isExhaustedSuccessfulRunHandoff` and, once "exhausted" (default max
attempts is 1, so effectively immediate), escalates via
`escalateStrandedAssignedIssue` — with no check on who owns the issue's
lifecycle at all
> - Rather than duplicate the `originKind` check inline a second time
(which is exactly how it got missed the first time), extracted it into a
shared, exported, unit-tested helper (`isPluginManagedIssueLifecycle`)
that both recovery paths now call
> - The benefit is the same as the first commit, but closing the second
loop this pull request's earlier version left open: generic plugin-owned
issues (any workflow/graph-engine plugin, not just one specific plugin)
stop burning real agent-run cost in a loop that can never actually
resolve, across both recovery mechanisms that can trigger it

## Linked Issues or Issue Description

No existing public issue covers this — describing it directly, following
the bug report fields:

**What happened?**

An issue owned by a workflow-engine-style plugin (`originKind` starting
with `"plugin:"`) was correctly held at `in_progress` by the plugin
while it waited on spawned child issues to finish. The assigned agent's
heartbeat succeeded and posted a well-formed completion comment, but
`issue.status` stayed `in_progress` (the plugin's own enforcement
reverted it, correctly, since the underlying work wasn't done).

- **Path 1 (fixed in the first commit):** `decideSuccessfulRunHandoff()`
saw `status === "in_progress"` after a successful run and enqueued a
"missing disposition" corrective wake. The agent responded again, the
plugin reverted the status again, and the recovery re-triggered again.
- **Path 2 (fixed in the second commit, found after deploying and
verifying the first fix on a live instance):** separately,
`reconcileStrandedAssignedIssues` periodically re-scans `in_progress`
issues, sees the corrective run from Path 1 (or any prior
successful-run-handoff wake) as "exhausted" evidence, and escalates the
issue via `escalateStrandedAssignedIssue` regardless of plugin ownership
— producing the same nag-revert-nag cycle through a completely different
call path that the first commit's fix did not touch.

**Expected behavior**

Neither recovery mechanism should nag an agent for a disposition, or
escalate for one, on an issue whose lifecycle is already owned and
managed by a plugin — that plugin's own enforcement/recovery path is the
correct owner of "what happens next," not these generic core mechanisms.

**Steps to reproduce**

1. Install a plugin that creates/owns issues via the plugin host bridge
(`ctx.issues.create`/`ctx.issues.update`) with an `originKind` of
`"plugin:<pluginKey>"`.
2. Have the plugin's own graph/workflow logic hold an issue at
`in_progress` while some multi-step process it owns is still pending
(e.g. spawned child issues not yet complete).
3. Let an agent run a successful heartbeat on that issue that produces
visible progress (a comment) but does not change `issue.status` away
from `in_progress` in a way that sticks (the plugin's own logic reverts
any change back to `in_progress` on the next event).
4. Observe `decideSuccessfulRunHandoff` enqueue a corrective handoff
wake (Path 1), and/or `reconcileStrandedAssignedIssues` treat that
wake's run as exhausted and escalate (Path 2). Either one repeats
indefinitely on its own.

**Paperclip version or commit**

`eb2cb916be3271e3e7ab5f643ad3ca3eb7c34d01` (current `master` at time of
the first commit; rebased onto `ad961227f` for the second)

**Deployment mode**

Self-hosted server

## What Changed

- **First commit:** Added `originKind: issues.originKind` to the issue
query in `heartbeat.ts` that feeds `decideSuccessfulRunHandoff`, and a
skip condition there for `originKind` starting with `"plugin:"`.
- **Second commit:**
- Extracted the plugin-ownership check out of
`decideSuccessfulRunHandoff` into a new exported helper,
`isPluginManagedIssueLifecycle(issue)`, in `successful-run-handoff.ts`.
- Added the same check to `reconcileStrandedAssignedIssues` in
`service.ts`, immediately before it would otherwise escalate an issue
based on `isExhaustedSuccessfulRunHandoff` evidence — skipping
plugin-managed issues there too.
- Added unit tests for the new helper directly (plugin-prefixed origin
kinds → `true`; non-plugin/missing origin kinds → `false`), alongside
the existing `decideSuccessfulRunHandoff` tests (updated to import and
rely on the shared helper, behavior unchanged).

## Verification

- `npx vitest run
server/src/services/recovery/successful-run-handoff.test.ts` — 20/20
passed (18 pre-existing/from the first commit + 2 new for the extracted
helper).
- `npx vitest run
server/src/__tests__/heartbeat-comment-wake-batching.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/services/recovery/successful-run-handoff.test.ts` — full
suite still passes with the refactor.
- `pnpm --filter @paperclipai/server exec tsc --noEmit` — no new errors
introduced by either commit (confirmed against a pre-existing baseline
of unrelated `@paperclipai/plugin-sdk` module-resolution errors from the
workspace, present identically with the changes stashed out).
- Manually verified Path 1 against a real plugin-managed issue stuck in
that loop: after deploying the first commit and restarting the server,
the same agent posted the same completion comment again, and the
corrective-handoff recovery did not re-trigger.
- Path 2 was found live on the same instance after that first deploy
(the loop recurred through the second, independent mechanism) —
root-caused via direct inspection of
`heartbeat_runs`/`agent_wakeup_requests`/issue comment history, then
fixed in the second commit. Not yet re-verified live on the instance
(pending redeploy of this updated branch).

## Risks

- Low risk. Both changes are additive skip conditions — they only cause
a recovery decision to return early for a specific, narrow case
(`originKind` starting with `"plugin:"`) that previously fell through to
escalation/enqueue. No existing skip conditions are changed or reordered
in a way that affects non-plugin issues.
- Behavioral shift: plugin-managed issues that are genuinely stuck (not
just correctly mid-flight) will no longer get either of these corrective
nags. This is intentional — the plugin owning the issue is expected to
have its own recovery path — but it does mean these mechanisms are no
longer a safety net for buggy plugins that leave issues stranded. Plugin
authors should ensure their own enforcement handles stranded states.
- The refactor (extracting `isPluginManagedIssueLifecycle`) is a pure
code-motion change for the first commit's check — no behavior change
there, only a new call site added in `service.ts`.
- No migration required (query-shape and control-flow changes only, no
schema change).

## Model Used

Claude (Anthropic), model `claude-sonnet-5`, used within a coding-agent
harness (Claude Code) with tool use (file edit, test execution, git
operations, live production-instance debugging via SSH/SQL) and extended
reasoning across two sessions: the first implemented and deployed the
initial fix, the second discovered the second recovery path was still
looping on a live instance, root-caused it, and implemented/tested this
follow-up commit.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
#11565)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The release channel system publishes a nightly build only after the
release smoke suite passes against the newest canary
> - The scheduled nightly run has failed every night since August 12, so
no nightly, and therefore no beta candidate, has shipped for six days
> - The failures are stale test locators, not a product regression: the
chat-first onboarding rewrite (#11101) changed wizard copy and the
post-launch destination
> - This pull request updates the smoke spec to match the current wizard
> - The benefit is a green nightly lane and an unblocked beta promotion

## Linked Issues or Issue Description

**What happened?**

The scheduled `Release` nightly run fails in `smoke_nightly / smoke`
every night since 2026-08-12. The failing spec is
`tests/release-smoke/docker-auth-onboarding.spec.ts`. Four assertions no
longer match the product after the chat-first onboarding rewrite
(#11101):

- The step-1 heading is now "Name your organization", not "Name your
company".
- The step-4 hire button is now "Connect", not "Give it a heartbeat".
- The seeded first task is now titled "Paperclip onboarding".
- A successful launch navigates to the seeded task's thread
(`/issues/<ref>`), not `/dashboard`.

**Expected behavior**

The smoke suite passes against a canary that contains the current
onboarding wizard, and the nightly lane publishes again.

**Steps to reproduce**

Run `.github/workflows/release-smoke.yml` against `paperclipai@canary`
(any version at or after the rewrite), or dispatch `release.yml` with
`channel: nightly`. Example red runs: 32014452506 (Aug 17), 31938284401
(Aug 16).

**Paperclip version or commit**

`2026.817.0-canary.12`

Related (not duplicates): #11190 updated this same spec for the
mission-first wizard; this PR is the follow-up for the chat-first
rewrite that landed after it.

## What Changed

- Update the step-1 wizard heading locator to "Name your organization".
- Update the step-4 hire button locator to "Connect" and reword the step
comment.
- Update `FIRST_TASK_TITLE` to "Paperclip onboarding" (the wizard's
current `DEFAULT_TASK_TITLE`).
- Assert the post-launch URL is the seeded task's thread (`/issues/`),
not `/dashboard`.

## Verification

- Local run of the exact CI harness: `scripts/docker-onboard-smoke.sh`
with `PAPERCLIPAI_VERSION=2026.817.0-canary.12`, then `pnpm run
test:release-smoke` against the container — 1 passed.
- The suite's later API assertions (company, CEO agent, mission goal,
seeded issue assignment, assignment-sourced heartbeat run) all pass
unchanged against the current canary.

## Risks

- Low risk: test-only change; no product code is touched.
- The spec remains copy-coupled to the wizard. If wizard copy churn
continues, a follow-up could add stable `data-testid` hooks to the
wizard so the smoke spec stops breaking on wording changes.

## Model Used

Claude Fable 5 (Claude Code)



## Pre-submission checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
…#11582)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The release channel system re-smokes every published beta as
post-publish verification (`smoke_beta`)
> - The candidate-branch beta lane (#11209) added
`verify_beta_candidate` to `publish_beta`'s needs; that job is skipped
on every normal promote-mode beta
> - `smoke_beta`'s condition has no status-check function, so GitHub
attaches an implicit `success()` that evaluates the needs chain
transitively — a skipped ancestor makes it false
> - This pull request makes the condition explicit so promote-mode betas
smoke again, and pins the shape in the workflow wiring test
> - The benefit is that the post-publish beta gate actually runs instead
of silently skipping

## Linked Issues or Issue Description

**What happened?**

Beta `2026.818.0-beta.0` (run 32082007439) published successfully, but
its post-publish `smoke_beta` job was skipped. No configuration or input
asked for that: the run was a plain `channel: beta` dispatch with
`dry_run` at its default `false`, and the same expression
`!inputs.dry_run` evaluated true inside `publish_beta`'s own steps (the
Docker dispatch step ran).

**Expected behavior**

Every non-dry-run beta publish is followed by the release smoke suite
against the exact published version, as documented in `doc/RELEASING.md`
and `doc/RELEASE-CHECKLIST.md`.

**Steps to reproduce**

Dispatch `release.yml` with `channel: beta` promoting a nightly (promote
mode). `verify_beta_candidate` is skipped by design; `publish_beta` runs
through its explicit `!cancelled()` condition; `smoke_beta` then skips
because its implicit `success()` sees the skipped ancestor in the
transitive needs chain (actions/runner#2205 semantics). The beta
published on 2026-08-11 predated #11209, so this never surfaced before.

**Paperclip version or commit**

master at `43ab441f0` (workflow file, current head).

Related (not duplicates): #11209 introduced the candidate lane whose
skipped job triggers this; #11208 covers the adjacent tag-push failure
playbooks.

## What Changed

- `smoke_beta`'s condition becomes `!cancelled() &&
needs.publish_beta.result == 'success' && !inputs.dry_run` — an explicit
status-check function suppresses the implicit `success()`, and the
result check keeps the dependency on a successful publish.
- A comment above the job records why the explicit form is load-bearing.
- `scripts/__tests__/release-verify-workflow.test.mjs` pins the new
shape so the implicit form cannot silently return.

## Verification

- `node --test scripts/__tests__/release-verify-workflow.test.mjs` — 8
pass, including the new assertion.
- `release.yml` re-parsed as YAML.
- The exact skip is visible on run 32082007439 (`smoke_beta: skipped`
after `publish_beta: success`); the coverage gap for that beta was
closed manually by dispatching `release-smoke.yml` with
`paperclip_version: beta` (run 32084880767).
- Not exercised end-to-end: the corrected condition needs the next real
promote-mode beta to demonstrate; the expression change is minimal and
the semantics are the documented actions/runner behavior.

## Risks

- Low risk: condition-only change on one job plus a test. Dry runs still
skip the smoke (`!inputs.dry_run` retained). Candidate-mode betas, where
`verify_beta_candidate` actually runs, behave as before.

## Model Used

Claude Fable 5 (Claude Code)

## Pre-submission checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
…on the Claude login guard (#11579)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Claude local adapter supports a setup-token subscription login,
and its confidential routes pass a fail-closed transport guard
> - The guard accepts direct socket TLS, a local_trusted loopback peer,
or an allowlisted proxy peer that forwards https — and deliberately
never reads the global `TRUST_PROXY`
> - On a managed platform the edge terminates TLS, the app socket is
always plain HTTP, and the edge-proxy peer addresses are not stable or
documented, so none of the three cases can hold
> - Every login on such a deployment shows the clear-text transport
warning although the user's connection is HTTPS, and the agent-scoped
confidential routes fail closed entirely
> - This pull request adds a dedicated operator declaration that the
platform edge terminates TLS, as a fourth guard case
> - The benefit is a correct transport decision on managed platforms
with the default posture unchanged everywhere else

## Linked Issues or Issue Description

No public GitHub issue covers this. The problem is described in-PR
following the enhancement template. Related public PRs:
[#11347](#11347) added the
new-agent login flow and the non-blocking transport advisory, and
[#11286](#11286) added the
setup-token login and the guard with its `CLAUDE_LOGIN_TRUSTED_PROXIES`
allowlist.

**Subsystem affected**

server/ — the confidential transport guard for the Claude setup-token
login (`services/setup-token-session.ts`, `routes/agents.ts`, `app.ts`).

**Current behavior**

The guard allows a confidential response on direct socket TLS, on a
`local_trusted` loopback peer, or when the immediate peer is on the
dedicated `CLAUDE_LOGIN_TRUSTED_PROXIES` allowlist and forwards `https`.
Behind a managed platform's TLS-terminating edge (Railway, Render, Fly,
and similar), the app socket is plain HTTP and the edge-proxy peer
addresses are not operator-visible or stable, so the allowlist cannot
express them — IPv6 entries match by exact string only. The result: the
login panel shows "This connection is not encrypted" for a connection
that is HTTPS to the user, and the agent-scoped confidential routes
return the fixed no-secret error.

**Proposed behavior**

`CLAUDE_LOGIN_EDGE_TLS_TERMINATED=true` is an explicit, single-purpose
operator declaration that every client request reaches the server
through the platform's TLS-terminating edge. Under the declaration the
guard treats a request as confidential unless the edge itself labels the
client hop as plain `http` in `X-Forwarded-Proto`. The declaration is
never derived from the global `TRUST_PROXY` setting, which the guard
still never reads. Without the declaration, nothing changes.

**Reason and benefit**

The guard's spoofing concern does not apply to this deployment shape: a
client cannot pick its transport, because the platform admits HTTPS
only, and the header the guard consults is set by the platform edge, not
the client. A blanket warning that is always wrong teaches users to
ignore it. The declaration keeps the strict default for every deployment
that does not opt in, and it keeps the allowlist as the precise tool for
operators who do know their proxy addresses.

## What Changed

- `ConfidentialTransportConfig` gains optional `edgeTlsTerminated`
(default false), documented as the operator declaration for platform
edge TLS termination.
- `evaluateConfidentialTransport` adds the declaration as a guard case:
allowed unless the forwarded protocol's first hop is explicitly `http`
(reason `edge_labeled_plain_http` then; `operator_edge_tls_termination`
when allowed).
- `assessConfidentialStartup` reports `edge_tls_termination_declared`,
so the startup log shows why forwarded requests pass.
- `app.ts` parses `CLAUDE_LOGIN_EDGE_TLS_TERMINATED` (truthy:
`1/true/yes/on`) and passes it to the agent routes; the routes build the
guard config from it.
- The SR-7 operator-requirement comment on the setup-token routes
documents the new variable next to the allowlist.
- Tests: five new guard unit cases and a route case asserting the prompt
and code responses carry no `transportAdvisory` under the declaration.

## Verification

```sh
cd server
npx tsc --noEmit    # clean
npx vitest run src/services/setup-token-session.test.ts \
  src/routes/setup-token-route.test.ts \
  src/__tests__/openapi-routes.test.ts   # 3 files, 89 passed
```

The new "keeps failing closed when the declaration is absent" case pins
the unchanged default posture.

## Risks

The declaration is an operator statement the server cannot verify; an
operator who sets it on a deployment whose edge does not terminate TLS
re-labels plain-HTTP requests as confidential. This is the same trust
class as `CLAUDE_LOGIN_TRUSTED_PROXIES` (a wrong allowlist entry has the
same effect) and is opt-in, off by default, and scoped to the login
routes only. The guard still fails closed when the edge explicitly
labels a request `http`. No schema change, no API shape change —
`transportAdvisory` was already nullable.

## Model Used

Claude Fable 5 (`claude-fable-5`), extended thinking, with tool use and
code execution — investigation, implementation, and tests.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Stable release notes end with a Contributors section that credits
community contributors
> - The release-changelog skill excluded founders from that list, but
had no rule for non-founder core contributors
> - A release-notes draft therefore credited two core contributors as
community contributors
> - This pull request makes the exclusion list canonical in the
changelog skill and adds the core contributors
> - The benefit is that release credits consistently mean "community",
with one list to maintain

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The Contributors rules in `.agents/skills/release-changelog/SKILL.md`
and the Community-section rules in
`.agents/skills/release-changelog-discord-message/SKILL.md`.

**Current behavior**

The changelog skill says to exclude "Paperclip founders (e.g.
`cryppadotta`, `forgottendev`, `devinfoley`, `sockmonster`,
`scotttong`)". Core contributors who are not founders are not covered,
so drafts credit them in the community list. The Discord skill refers to
"founders" in two places.

**Proposed behavior**

The changelog skill holds the canonical exclusion list of specific folks
— now including `nguyenm7`, `nickyleach`, and `tonio-alucema` — and both
Discord-skill references defer to that one list.

**Reason and benefit**

Release credits consistently mean community contributions, and there is
exactly one place to update when the core team changes.

## What Changed

- `.agents/skills/release-changelog/SKILL.md`: the exclusion rule names
specific folks, is the canonical list, and adds `nguyenm7`,
`nickyleach`, and `tonio-alucema`.
- `.agents/skills/release-changelog-discord-message/SKILL.md`: both
references ("Community" template note and the final checklist) defer to
the changelog skill's canonical list.

## Verification

- Docs-only change; rendered and proofread. No workflow, script, or test
surface is touched.

## Risks

- None beyond docs accuracy. If the canonical-list wording lands after
#11567 (which touches other sections of the same files), Git merges them
cleanly — the edited regions do not overlap.

## Model Used

Claude Fable 5 (Claude Code)

## Pre-submission checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
…ding the managed home (#11578)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The codex_local adapter supports a device login that runs in a
trusted sandbox and promotes the credential into the per-company managed
Codex home
> - The managed-home re-seeding step treats every regular-file
`auth.json` as apikey-mode residue and removes it so the shared-home
symlink can be restored
> - The promotion writes the company credential as a regular file, so
the first environment Test or run after a successful login deletes it
> - On a server with no shared Codex login — any containerized
deployment — nothing replaces the file, and the UI reports that the
sandbox has no ready authentication right after it reported a successful
login
> - This pull request makes the cleanup identity-anchored: a
subscription credential whose identity the shared source does not hold
survives re-seeding
> - The benefit is that a device login stays usable after Test and runs,
on hosts with and without a shared Codex login

## Linked Issues or Issue Description

No public GitHub issue covers this. The problem is described in-PR
following the bug template. Related public PRs:
[#11237](#11237) added the
sandbox device login and the credential promotion,
[#11097](#11097) added its
building blocks, and the `ensureSymlink` heal for stale copies came from
the fix for #5028.
[#9621](#9621) touches the
adjacent sandbox auth sync-back lane but not this defect.

**Subsystem affected**

packages/adapters/codex-local — managed `CODEX_HOME` seeding
(`codex-home.ts`).

**Current behavior**

A successful device login promotes the subscription `auth.json` into the
company Codex home as a regular file, and the UI reports the login as
authenticated. The next `seedManagedCodexHome` call — the environment
Test probe and every execute both run it — removes any regular-file
`auth.json` when no API key is configured, because the cleanup assumes
such a file is apikey-mode residue left by a previous run. It then
symlinks `auth.json` from the shared source home. On a server whose
shared home has no Codex login (a container image, for example), there
is no source to symlink, so the home ends with no credential at all. The
Test probe then reports "The sandbox has no ready authentication for
this adapter" immediately after a successful login, and a fresh login
repeats the same cycle. On a server whose shared home does hold a login,
the symlink silently replaces the promoted account with the host
account.

**Expected behavior**

The credential a device login promoted stays in the company home across
Test probes and runs. The #5028 heal (a stale regular-file copy of the
shared credential becomes a symlink to the live source) and the
apikey-residue cleanup keep working.

**Steps to reproduce**

1. Run the server in an environment whose shared Codex home
(`$CODEX_HOME` or `~/.codex`) has no `auth.json`.
2. Complete a Codex device login for a company; the promotion writes the
company home `auth.json` and the UI reports authenticated.
3. Click Test on a codex_local agent (or start a run). The probe reports
no ready authentication, and the promoted `auth.json` is gone from the
company home.

**Proposed solution**

Make the cleanup identity-anchored, the same rule the promotion and the
cache vend already use. A regular-file `auth.json` survives re-seeding
when it holds a usable subscription identity that the shared source does
not also hold, and the shared symlink does not replace it. A
same-identity regular file is still the #5028 stale copy and is still
healed into the symlink, because the symlink serves the same account
with live, rotating tokens. An apikey-mode or unreadable file is still
removed.

## What Changed

- `seedManagedCodexHome` reads the target `auth.json` before the cleanup
and keeps it when `readSubscriptionAccountId` yields an identity the
shared source `auth.json` does not hold. The kept file is excluded from
the shared symlink pass, and the function logs a fixed line when it
keeps the file.
- The function doc comment states the kept-promoted-credential rule.
- Four new `seedManagedCodexHome` test cases: a promoted credential with
no shared auth, a promoted credential with a different shared identity,
the same-identity #5028 heal, and apikey-mode residue removal.

## Verification

```sh
cd packages/adapters/codex-local
npx tsc --noEmit         # clean
npx vitest run           # 28 files, 321 passed, 1 skipped
```

The four new cases fail on the previous code: the first two observed the
promoted file deleted (and, with a shared login present, replaced by the
shared symlink).

## Risks

Low risk. The change narrows one deletion path. Deployments that never
use the device login see no difference: without a promoted subscription
file, the cleanup and the symlink behave exactly as before, and the
#5028 heal is pinned by an existing test plus a new same-identity test.
The one deliberate behavioral shift: after a device login, the promoted
company credential now stays authoritative over the shared host login
for that company — which is the promotion's documented contract ("the
company credential slot").

## Model Used

Claude Fable 5 (`claude-fable-5`), extended thinking, with tool use and
code execution — investigation, implementation, and tests.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Workspaces let users and agents inspect files that belong to an
issue
> - Changed-file views use full-tree Git status scans
> - Many issue views could start those scans at the same time and make
the server unresponsive
> - Route-level limits did not protect the process or coalesce work for
one repository
> - This pull request adds one bounded scheduler for every expensive
workspace Git scan
> - It also starts browser scans only when the file panel is open and
visible
> - The benefit is bounded child-process use and responsive health
checks during request storms

## Linked Issues or Issue Description

**What happened?**

Many changed-file requests could start full `git status --porcelain=v1
-z --untracked-files=all` scans at the same time. One production
incident produced about 270 direct Git child processes. The Node process
stayed alive but stopped answering health requests in time.

**Expected behavior**

Paperclip must bound expensive Git work across all companies, actors,
issues, repositories, and browser tabs. Duplicate requests for one
worktree must share work. Excess requests must fail fast with a
retryable response. Hidden or closed file panels must not start scans.

**Steps to reproduce**

1. Open changed-file views for many issue and actor keys.
2. Send requests for two large workspace roots at the same time.
3. Observe that route-level limiter keys allow many full Git scans to
run together.
4. Observe delayed health responses and accumulated Git children.

**Paperclip version or commit**

Reproduced on master before commit `43ab441f0f`.

**Deployment mode**

Self-hosted server with local workspace repositories.

## What Changed

- Add a process-wide scheduler with configurable concurrency, queue
capacity, timeout, and cache TTL.
- Add fair admission, a bounded queue, canonical worktree keys,
single-flight joins, and bounded result caching.
- Add subprocess timeouts, TERM-to-KILL escalation, bounded output,
waiter cancellation, and slot cleanup.
- Route full-tree status work from file resources, workspace runtime,
execution workspaces, and adapter overlay sync through the scheduler.
- Return stable retryable `503` and `504` error codes for saturation and
timeout.
- Add structured logs with safe workspace hashes, durations, queue
state, cache use, joins, and terminal outcomes.
- Gate UI queries on panel and document visibility. Cancel queries on
close, hide, unmount, and workspace change.
- Disable focus and reconnect bursts. Keep one explicit refresh action
and a retryable unavailable state.
- Document the 10-second default freshness tradeoff and all
configuration variables.
- Add unit, route, UI, adapter, and deterministic 500-request load
coverage.

## Verification

- `pnpm -r typecheck`
- `pnpm build`
- `pnpm check:token-gates`
- `pnpm --filter @paperclipai/server exec vitest run
src/services/workspace-git-operation-scheduler.test.ts
src/__tests__/file-resources-git-scan-load.test.ts --reporter=dot` — 16
tests passed.
- `pnpm --filter @paperclipai/ui exec vitest run
src/components/WorkspaceFileBrowser.test.tsx
src/lib/page-visibility.test.ts --reporter=dot` — 38 tests passed.
- `pnpm --filter @paperclipai/adapter-utils exec vitest run
src/git-workspace-sync.test.ts --reporter=dot` — 16 tests passed.
- Existing file-resource, workspace-runtime, and execution-workspace
regression selections passed.
- Two cleanup safety regressions prove failed scans preserve the
worktree before archive and at the final deletion fence.
- Before: the incident produced about 270 Git children and health
requests timed out.
- After: 500 concurrent requests across 500 issue keys, 73 actors, and
two roots started two underlying scans. Peak scan concurrency was 2. All
500 requests succeeded. Health p99 was 4.94 ms. The harness found zero
unreaped children.
- The full local Vitest run passed 4,267 tests. Ten existing fixed-port
HTTPS exposure tests could not run because this host already owns
Tailnet listeners on ports 42000 and 52000. Clean GitHub CI is the final
full-suite result.
- Latest-head GitHub CI passed all required test, typecheck, build,
canary, e2e, policy, and security gates.
- Greptile completed at 5/5 with zero unresolved comments,
recommendations, or follow-ups.

## Risks

- Changed-file results can be up to 10 seconds old by default. Explicit
refresh remains available.
- A full queue returns a retryable `503` instead of waiting without a
bound.
- A scan that exceeds the default 8-second deadline returns a retryable
`504` and terminates its process group.
- Operators can tune all limits with documented environment variables.
Safe defaults protect local and shared servers.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5 family. The runtime does not expose the exact
deployment ID or context-window size. High reasoning, tool use, and code
execution were enabled.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
…#11528)

<!-- Write all pull request text in Simplified Technical English
(ASD-STE100). -->

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The PR verify workflow gates every pull request; its wall-clock time
sets the feedback loop for all contributors
> - In a recent successful PR run (actions run 32012408876), the slowest
check was "Verify serialized server suites (1/5)" at 337s, while its
four sibling shards finished in 212-238s
> - The serialized lane assigns suites to shards round-robin over an
alphabetical list, so the heavy heartbeat and issues suites cluster on
one runner
> - The general-server lane already solves this with a duration-aware
LPT partition backed by a recorded manifest
> - This pull request reuses that partitioner for the serialized lane
with a fresh per-suite duration manifest
> - The benefit is a balanced serialized matrix: the measured 968s suite
total levels to about 194s per shard, which removes about 80-100s from
the run's slowest check

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The `Verify serialized server suites` shard matrix in
`.github/workflows/pr.yml` distributes route/authz test suites across
five runners.

**Subsystem affected**

CI / test infrastructure (`scripts/run-vitest-stable.mjs`).

**Current behavior**

`selectSerializedSuites` assigns suites round-robin (`index %
shardCount`) over the alphabetically sorted file list. The heavy suites
cluster on shard 1/5. In actions run 32012408876, shard 1/5 spent 291s
in its test step while the other shards spent 170-201s, which made that
job (337s total) the slowest check of the whole PR run.

**Proposed behavior**

Partition the serialized suites with the same duration-aware LPT
algorithm the general-server lane already uses
(`scripts/general-server-shard.mjs`), backed by a new per-suite duration
manifest. All five shards then carry about 194s of measured test time.

**Reason and benefit**

The slowest check bounds PR feedback time. Balancing the serialized
matrix removes about 80-100s from that bound without adding runners.

**Breaking changes**

None. The partition remains deterministic, complete, and
non-overlapping; suites missing from the manifest get the median weight.

## What Changed

- Added `scripts/serialized-shard-durations.json`: per-suite wall-clock
durations (ms) for all 134 serialized suites, sampled from actions run
32012408876 by diffing consecutive per-suite label timestamps in the
shard logs (captures vitest spawn overhead, not just reported test time)
- `scripts/run-vitest-stable.mjs`: `selectSerializedSuites` now uses the
existing LPT partitioner (`selectGeneralServerShard`) with the new
manifest instead of round-robin
- `scripts/__tests__/run-vitest-stable-shard.test.mjs`: added a
manifest-freshness test and a shard-balance test for the serialized
lane, mirroring the general-server ones
- `.github/workflows/pr.yml`: updated the serialized matrix comment with
the new measurement and mechanism

## Verification

- `node --test ./scripts/__tests__/run-vitest-stable-shard.test.mjs`
passes (13 tests), including the existing test that the serialized
shards form a complete, non-overlapping partition
- Dry-run of all five shards shows estimated totals of
194/194/194/194/193s (round-robin was 276/175/160/172/187s):
`node scripts/run-vitest-stable.mjs --mode serialized --shard-index N
--shard-count 5 --dry-run`
- The `Verify serialized server suites` jobs on this PR run the real
partition end to end

## Risks

- Low risk. Selection logic only; the vitest invocation per suite is
unchanged
- A stale manifest degrades gracefully: unknown suites get the median
weight, and a dedicated test fails if fewer than half the current suites
have recorded durations

## Model Used

- Claude (Anthropic), model ID `claude-fable-5`, agentic coding session
with tool use (Claude Code / Claude Agent SDK); no extended-thinking
mode

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Related prior work: #10923 (split serialized tests into five shards),
#10925 (general-server duration manifest), #11156 (workspaces-a native
shards).

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Stable release notes live at `releases/vYYYY.MDD.P.md` on master as
the durable record
> - v2026.817.0 shipped from a candidate branch, so its notes file
exists on that branch and in the GitHub Release, but not yet on master
> - This pull request lands the exact shipped notes at the canonical
path
> - The benefit is a complete stable-notes record on master, and the
FULL RELEASE NOTES link in announcements resolves

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The `releases/` record on master.

**Current behavior**

`releases/` on master ends at `v2026.722.0.md`. The v2026.817.0 notes
exist only on `candidate/release-2026.817.0` and in the published GitHub
Release.

**Proposed behavior**

`releases/v2026.817.0.md` exists on master, byte-identical to what the
release published.

**Reason and benefit**

Complete history at the canonical path; the standard notes link
(`releases/v2026.817.0.md` on master) resolves for the announcement.

## What Changed

- Add `releases/v2026.817.0.md`, taken verbatim from the candidate
branch the release was published from (tag `v2026.817.0`, commit
`213dabab4`).

## Verification

- `git diff v2026.817.0 -- releases/v2026.817.0.md` against this branch
is empty (byte-identical to the shipped file).
- Docs-only; no code surface.

## Risks

- None; docs-only. The candidate branch is deleted after this merges
(the `v2026.817.0` tag keeps its commits reachable).

## Model Used

Claude Fable 5 (Claude Code)

## Pre-submission checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
…ter at promotion (#11567)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The release channel system promotes builds canary → nightly → beta →
stable, and stable releases publish a GitHub Release from
`releases/vYYYY.MDD.P.md`
> - The stable lane requires that notes file to exist inside the
promoted source commit, but the file is named for the promotion date,
which is unknown when the source commit is created
> - A promoted beta can therefore never pass the notes check: every
happy-path stable is forced through the candidate-branch fix path, with
a soak-gate justification, for a notes-only change
> - This pull request drafts the notes automatically when the beta is
published and lets the stable promotion read them from `master`
> - The benefit is a walkable stable happy path: the soak gate stays
exact, notes get a real review window during the soak, and the
justification path returns to its real purpose (cherry-picked fixes)

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The stable promotion path in the release channel system (`release.yml`,
`scripts/release.sh`).

**Current behavior**

`release.sh stable` requires `releases/vYYYY.MDD.P.md` in the
checked-out source tree, and `publish_stable` checks out the exact
promoted SHA. The soak gate requires a `beta/v*` tag to point at that
same SHA. No commit can satisfy both for a promoted beta, so a stable
promotion must cut a candidate branch with a notes-only commit and
bypass the soak gate with a written justification. Release notes are
also written at promotion time, under time pressure, with no review
window.

**Proposed behavior**

When a beta publishes, a `draft_stable_notes` job generates a grouped
notes skeleton at `releases/beta/v<beta-version>.md` and pushes it to a
machine-owned branch; a human opens the PR and edits it during the 3-day
soak. The stable preflight resolves notes before the `npm-stable`
approval gate: source-tree notes first (the candidate fix path,
unchanged), then the merged beta-keyed file on `master`; it fails early
with the missing path named when neither exists. After the stable ships,
a canonicalization job pushes a branch that moves the file to
`releases/vYYYY.MDD.P.md`.

Related (not duplicates): #11006 and #11008 introduced the nightly and
beta lanes this builds on; older changelog PRs (for example #10669)
authored notes manually at promotion time, which is the flow this
replaces.

**Reason and benefit**

The happy path becomes: promote the exact soaked SHA, no justification,
notes reviewed during the soak instead of written at the gate. The
`releases/vYYYY.MDD.P.md` invariant still holds durably via the
canonicalization PR.

## What Changed

- `scripts/release.sh`: new `--notes-file PATH` (stable only) overrides
where the pre-publish notes check looks, so notes can live outside the
source checkout without dirtying the worktree.
- `scripts/create-github-release.sh`: same `--notes-file` override for
the GitHub Release body.
- `scripts/draft-stable-notes.sh` (new): deterministic skeleton
generator — commit subjects from the newest stable tag (falling back to
the previous beta, then full history) to the beta's source commit,
grouped into Features / Fixes / Other.
- `.github/workflows/release.yml`:
- `draft_stable_notes` job after `publish_beta`: runs the generator and
force-pushes `release-notes/v<beta-version>`; the job summary links the
compare page. It recreates the beta tag locally if the tag push was
rejected (the known workflows-permission case), so drafting is not
blocked on manual tag recovery.
- `preflight_stable`: computes the target stable version (`release.sh
stable --print-version`) and resolves the notes source (`source_tree` →
`master_beta` → fail early / warn on dry run); new outputs.
- `publish_stable`: materializes `master`-side notes into `RUNNER_TEMP`
and passes `--notes-file` to both scripts; outputs the published stable
version.
- `canonicalize_stable_notes` job: pushes the `git mv` branch after a
stable that used `master`-side notes.
- `doc/RELEASING.md`, `doc/RELEASE-CHECKLIST.md`: document the
drafted-notes flow, the preflight resolution order, and the
canonicalization step; the LLM changelog flow now targets the draft
branch during the soak.
- `.agents/skills/release-changelog/SKILL.md`,
`.agents/skills/release-changelog-discord-message/SKILL.md`: the
notes-authoring skills now describe this flow — range ends at the beta
source commit (not `HEAD`), the file is beta-keyed on the
`release-notes/v<beta-version>` branch (seeded with
`scripts/draft-stable-notes.sh` for betas that predate the automation),
and the canonicalization link caveat is called out for announcements.

## Verification

- `node --test scripts/draft-stable-notes.test.mjs` — 6 tests, temp
git-repo fixtures: grouping, stable-tag range, previous-beta and
full-history fallbacks, default output path, malformed version, missing
tag.
- `node --test scripts/release-lib.test.mjs` — unchanged suite still
green.
- `bash -n` on both changed shell scripts; `release.yml` re-parsed as
YAML.
- `./scripts/release.sh stable --print-version` unchanged (prints the
next stable version); `--notes-file` on a non-stable channel fails with
a clear error.
- Not exercised end-to-end: the new workflow jobs need a real beta
publish to run. The first beta after merge is the live test; the draft
job is additive and cannot affect the publish result (it runs after
`publish_beta` completes).

## Risks

- Low risk to publishing itself: `--notes-file` defaults preserve
today's behavior everywhere; the draft and canonicalization jobs are
additive and run after the publishes succeed.
- The preflight now fails a real stable run when no notes are found.
That is the intended fail-early behavior (it previously failed later,
inside `publish_stable`, after the `npm-stable` approval).
- `draft_stable_notes` force-pushes only the machine-owned
`release-notes/v<beta-version>` branch; a beta re-cut regenerates it
cleanly.
- The stable version computed at preflight could differ from the
published one if a run crosses UTC midnight between the two jobs; the
materialized notes are passed by path, so the publish still succeeds,
and the canonicalization job uses the actually-published version.

## Model Used

Claude Fable 5 (Claude Code)



## Pre-submission checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
…to the local user actor (#11589)

<!-- Write all pull request text in Simplified Technical English
(ASD-STE100): short sentences, one instruction per sentence, simple
approved vocabulary, and the active voice. -->

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server authenticates each agent request in `actorMiddleware`
before it attributes chat comments
> - When an agent bearer token failed verification, the middleware
called `next()` with no error and the request continued without an agent
actor
> - The request then fell back to the local user actor, so the server
stored agent replies as user comments
> - The task chat UI renders user comments in blue bubbles, so agent
messages appeared as blue user bubbles
> - This pull request rejects invalid agent credentials with 401 instead
of a silent downgrade
> - The benefit is that agent messages keep agent attribution, and
broken credentials fail loudly with a clear retry message

## Linked Issues or Issue Description

**What happened?**

A user cancelled an onboarding question card. The agent posted a
follow-up reply. The reply appeared in a blue bubble, which the UI
reserves for human messages. The agent run held an expired local agent
JWT. The auth middleware could not verify the token, called `next()`
without an actor, and the request fell back to the local user identity.
The server stored the agent comment as a user comment.

**Expected behavior**

Agent messages always render as agent bubbles. A request with invalid
agent credentials must fail with 401 so the adapter can refresh
credentials and retry. It must not post content under a human identity.

**Steps to reproduce**

1. Start a local Paperclip instance.
2. Give an agent run an expired or malformed agent JWT.
3. Let the agent post an issue comment through the API bridge.
4. Before this change: the comment is stored with the local user
identity and renders as a blue bubble. After this change: the request
fails with 401 and a message that tells the caller to obtain fresh
credentials.

## What Changed

- `server/src/middleware/auth.ts`: a bearer token that fails
verification now produces a 401 `unauthorized` error instead of a silent
fall-through to the anonymous/local-user actor.
- The 401 message states the cause: expired token, unverifiable token,
empty bearer token, missing agent record, agent record in another
company, terminated agent, or agent pending approval.
- The API-key path now also rejects an agent record whose company does
not match the key.
- `packages/adapter-utils/src/execution-target.ts`: the bridge proxy now
writes a `comment id: <id>` marker to the run log for each posted issue
comment, so misattributed comments can be traced to a run.
- `ui/src/components/task-chat/task-chat-adapter.test.ts`: a regression
test asserts that a recovered `local-board` comment with a derived agent
author renders as an agent bubble, not a user bubble.
- `server/src/__tests__/agent-auth-middleware.test.ts` and
`packages/adapter-utils/src/execution-target-sandbox.test.ts`: new tests
cover each rejection path and the log marker.

## Verification

- Run `pnpm vitest run src/__tests__/agent-auth-middleware.test.ts` in
`server/` — 14 tests pass.
- Run `pnpm vitest run execution-target-sandbox` at the repo root — 44
tests pass.
- Run `pnpm vitest run
src/components/task-chat/task-chat-adapter.test.ts` in `ui/` — 4 tests
pass.
- Manual check: post an issue comment with an expired agent JWT; the API
returns 401 with a retry message and no comment is stored.

## Risks

- Behavioral shift: requests that previously continued as anonymous or
local-user actors after a failed agent-token verification now receive
401. Any caller that relied on the silent downgrade must refresh its
credentials. This is the intended fix, and the adapters already handle
401 with a credential refresh.
- No schema or migration changes. Low risk otherwise.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude (Anthropic), model ID `claude-fable-5`, via Claude Code with
extended thinking and tool use (agent harness with shell, file, and git
tools).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [ ] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents report their work in task chat, and that chat renders
markdown through `MarkdownBody`
> - Agents post shell commands, API payloads and diffs, so a fenced code
block is one of the most frequent things a person reads in the product
> - The rendered code block was pinned to two literal colors, so it
stayed dark in light mode and was the only dark surface on a light page
> - The `prose` and the `prose-invert` variables held the same two
values, so the theme could not change the block at all
> - This pull request binds the block to the theme tokens that already
exist and already carry a `.dark` override
> - The benefit is that a code block now matches the page in both modes,
and it follows any future change to the theme automatically

## Linked Issues or Issue Description

No public issue exists. The problem is described below.

**What happened?**
A fenced code block in rendered markdown is always dark. It uses the
background `#1e1e2e` and the text color `#cdd6f4` in light mode and in
dark mode. In light mode the block is the only dark surface on the page.
The rule is in `ui/src/index.css`. The variables `--tw-prose-pre-bg` and
`--tw-prose-invert-pre-bg` are set to the same literal value, so
`prose-invert` cannot change it.

**Expected behavior**
A code block uses a light surface with dark text in light mode. It uses
a dark surface with light text in dark mode. It follows the theme like
every other surface in the app.

**Steps to reproduce**
1. Start the app and open a task that contains a fenced code block in
its chat.
2. Set the theme to light.
3. Look at the code block. The block is dark. The page is light.
4. Set the theme to dark. The block does not change.

**Paperclip version or commit**
Reproduced on `master` at `4af55ba6b`.

**Agent adapter(s) involved**
Not adapter-specific (core bug). The defect is in the UI render path.

**Deployment mode**
Local development server.

## What Changed

- Bind `.paperclip-markdown pre` to `--muted`, `--foreground`,
`--border` and `--radius-lg` in `ui/src/index.css`. Remove the `#1e1e2e`
and `#cdd6f4` literals.
- Set the four `--tw-prose-*-pre-*` variables to the same tokens, so
`prose` and `prose-invert` both follow the theme.
- Apply the same four tokens to `.paperclip-mdxeditor-content pre`.
- Change the fill of the copy button and the wrap button to
`--background` in `ui/src/components/MarkdownBody.tsx`. The old fill was
`color-mix(in oklab, var(--muted) 92%, var(--background) 8%)`. That
value is almost equal to the block's new `--muted` surface, so the
buttons would nearly disappear.
- Update the stale comment above the rule. The comment said "Dark theme
code blocks".
- Add `ui/src/components/MarkdownCodeBlockStyles.test.ts`. It fails if a
literal color returns to any themed code surface.
- Export `codeBlockActionStyle` from `MarkdownBody.tsx` so the new test
can read it.

No new design token is added. Every token used here is already defined
in `ui/src/index.css`, and each one already has a `.dark` override.

## Verification

Automated:

```bash
cd ui && npx vitest run --config vitest.config.ts src/components/MarkdownCodeBlockStyles.test.ts src/components/MarkdownBody.test.tsx src/components/MarkdownBody.wrap.test.tsx src/components/MarkdownAccentStyles.test.ts
```

59 tests pass.

`MarkdownCodeBlockStyles.test.ts` is new. It guards the defect directly.
It reads `index.css` and asserts that each themed code surface rides a
token and holds no hex, `rgb()` or `hsl()` literal. I confirmed the test
fails on the original defect: restoring `#1e1e2e` and `#cdd6f4` fails 2
of the 5 tests.

Manual. Open a task that has a fenced code block in its chat. Read the
computed style of `.paperclip-markdown pre` in the browser. The measured
values are:

| Property | Light | Dark |
| --- | --- | --- |
| background | `oklch(0.97 0 0)` | `oklch(0.269 0 0)` |
| color | `oklch(0.145 0 0)` | `oklch(0.985 0 0)` |
| border | `oklch(0.922 0 0)` | `oklch(1 0 0 / 0.1)` |
| radius | `8px` | `8px` |

These values are `--muted`, `--foreground`, `--border` and `--radius-lg`
for each mode. Toggle the theme and confirm the block changes with the
page.

## Risks

Low risk. The change is CSS and one inline style value. There is no
migration and no schema change.

Two behavioral notes for the reviewer:

1. The corner radius changes from `calc(var(--radius) - 3px)` (5px) to
`var(--radius-lg)` (8px). This matches the approved design and removes
an arbitrary offset. It is a small visual change on every code block.
2. The CodeMirror theme inside the MDXEditor still uses the Catppuccin
literals (`ui/src/index.css`, the `.paperclip-mdxeditor .cm-editor`
rules). That surface is an editor, not a rendered snippet, and a change
there needs a full light CodeMirror theme. A code block therefore looks
light when it is rendered and dark while a person edits it. This is
intentional in this pull request. Tell me if you want it in scope.

Syntax highlighting, line numbers and diff rows are not in this pull
request. The repository has no highlighter dependency and no syntax or
diff tokens. Those are separate changes.

## Model Used

Claude Opus 5 (`claude-opus-5`), via Claude Code. Extended thinking was
on. Tool use was on, with file edit, shell, browser automation and the
Paper design tool. The design was produced first in Paper, then read
back through the design tool for exact token values rather than from
screenshots.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Notes on the unchecked boxes:

- Branch name. The branch is
`claude/paperclip-code-snippet-styling-202f7d`. It describes the change,
but the `202f7d` suffix comes from the local worktree name. It carries
no ticket id. I did not rename it, because the branch was already named
when this work was requested. Tell me if you want it renamed before
review.
- CI and Greptile. Not yet run at the time of opening.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ed resource ledger (#11576)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agent adapters run agent sessions through the ACPX engine
> - The ACPX engine handled one run attempt as a long implicit procedure
> - That shape made resource ownership, cleanup order, and failure
behavior hard to verify
> - This pull request gives the attempt a coordinator, a typed resource
ledger, separate run sites, and explicit turn and settlement sequences
> - The benefit is clear ownership, one cleanup path, safer session
reuse, and testable failure behavior

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The ACPX engine manages startup, turn execution, session reuse, and
cleanup inside one large run procedure.

**Current behavior**

The run procedure owns several resources through implicit control flow.
Cleanup and session reuse behavior depend on lane-specific branches and
error paths.

**Proposed behavior**

The coordinator owns the run attempt. A typed ledger records six
resources and their states. Host and sandbox run sites own lane-specific
acquisition. Turn and settlement sequences expose typed outcomes. The
engine emits allowlisted phase telemetry.

**Reason and benefit**

Explicit ownership makes cleanup and failure behavior easier to inspect.
The fault matrix and characterization tests protect the external result
while the refactor reduces hidden control flow.

**Breaking changes**

None to the public adapter contract. The host warm-save path now closes
and relaunches the runtime because a transferred runtime could retain a
run-scoped credential. A cold session-handshake failure now closes the
created runtime.

**Additional context**

This pull request contains the ACPX engine lifecycle refactor, its
tests, and the lifecycle document.

## What Changed

- Add a run coordinator for startup, turn execution, settlement, and
result reproduction.
- Add a typed resource ledger with open, sealed, and consumed states.
- Add host and sandbox run sites for lane-specific resource acquisition.
- Replace separate runtime maps with a generic session reuse store.
- Split session fingerprint identity from the outer session key.
- Add typed turn and settlement sequences with one cleanup owner.
- Add a closed allowlist for phase telemetry.
- Add characterization tests and a 17-case fault matrix.
- Add `doc/acp-run-lifecycle.md`.

## Verification

- `npx vitest run packages/adapter-utils/src/acpx-engine/` passes 18
files and 286 tests at the submitted commit.
- `pnpm --filter @paperclipai/adapter-utils typecheck` reports 0 errors
at the submitted commit.
- Run the full pull request checks after GitHub starts CI.
- Run Greptile review after the pull request opens.

## Risks

- The refactor changes internal control flow across the ACPX engine.
- Host warm-save behavior now closes and relaunches the runtime.
- Settlement changes the handling of a cold session-handshake failure
from a leak to a close.
- The characterization baselines and fault matrix reduce the risk of an
external behavior change.

> Paperclip is the open source app people use to manage AI agents for
work
> The adapter layer runs agent sessions through the ACPX engine
> The engine needs explicit lifecycle ownership for reliable cleanup
> This pull request adds coordinator-owned phases and a typed resource
ledger
> The result makes lifecycle behavior easier to test and review

## Model Used

OpenAI GPT-5 Codex. Exact model ID: GPT-5. The model used tool
execution, repository inspection, and code review support. The
implementation author supplied the submitted code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents need scoped secret bindings to use external services safely.
> - Agents could not request an existing secret under a new config name
without an internal secret identifier.
> - Existing binding proposals were only visible in Settings and did not
create an issue-thread approval path.
> - A confirmation card could record acceptance without proving that the
binding was created.
> - This pull request extends the existing secret proposal system with
safe source references and governed issue-thread confirmation cards.
> - The benefit is a one-click flow that creates the binding or shows a
clear failure without exposing secret material.

## Linked Issues or Issue Description

Related prerequisite: #11482.

**Subsystem affected**

Cross-cutting: server REST APIs, shared interaction contracts, database
proposal schema, and issue-thread UI.

**Problem or motivation**

An agent can need an existing bound secret under a second config name.
The agent cannot safely discover the internal secret identifier. The
existing proposal is also easy for the operator to miss because it only
appears in Settings. A generic confirmation can record acceptance
without executing the binding.

**Proposed solution**

Let an agent create a binding proposal from one of its existing config
paths. Mint a server-owned, human-only confirmation card on the
checked-out issue. Recheck the operator's target-agent permission under
the proposal row lock. Execute the existing proposal transaction after
card acceptance. Store an `executed` or `failed` result on the card.
Render the complete lifecycle in the issue thread and attention
resolver.

**Alternatives considered**

A new alias subsystem would duplicate proposal quotas, expiry,
authorization, and binding synchronization. A text-only issue comment
would not provide a governed action or an execution result. An
agent-supplied card payload would permit metadata smuggling. This change
uses the existing proposal transaction and a server-owned payload
instead.

**Roadmap alignment**

This change extends the completed "Secrets Manager with per-agent
access" roadmap item. It preserves scoped bindings and audited
resolution. The required GitHub search found no other open duplicate
issue or pull request.

## What Changed

- Added safe source-config-path binding proposals and preserved
user-secret ownership checks.
- Added a proposal-to-interaction link and an idempotent database
migration.
- Minted human-only `request_confirmation` cards with server-owned
`secretProposal` metadata.
- Rejected agent-supplied governed metadata and agent addressees.
- Rechecked `agent_config:update` authority under the proposal lock
before execution.
- Recorded `executed` or `failed` results and posted a failure comment
when no binding was created.
- Settled failed accepted proposals atomically and mirrored rejection,
withdrawal, and expiry in both directions.
- Emitted `secret.binding.created` for new agent binding writes.
- Added a dedicated issue-thread card for pending, executed, failed,
rejected, withdrawn, and expired states.
- Showed only the source label, target agent, config path, skeptical
justification, expiry, and safe failure code.
- Replaced resolved attention-query entries immediately with the
stitched server result.
- Added focused server, database, UI, and state-transition tests.
- Added Storybook fixtures for every review state and documented the API
and agent behavior.

## Verification

- `pnpm exec vitest run
ui/src/components/IssueThreadInteractionCard.test.tsx
ui/src/components/AttentionInteractionResolver.test.ts` — 58 passed.
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm check:token-gates`
- `pnpm build-storybook`
- `pnpm --filter @paperclipai/shared typecheck`
- `pnpm --filter @paperclipai/db typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm --filter @paperclipai/db check:migrations`
- `NODE_ENV=test pnpm exec vitest run
server/src/__tests__/issue-thread-interaction-routes.test.ts
server/src/__tests__/secret-proposals-routes.test.ts
server/src/__tests__/secrets-routes.test.ts
server/src/__tests__/agents-service-secret-bindings.test.ts` — 142
passed.
- `NODE_ENV=test pnpm --filter @paperclipai/db exec vitest run
src/company-secret-proposals-migration.test.ts --silent` — 1 passed.
- `pnpm -r typecheck`
- `pnpm test:run` — server 4,175 passed, UI 4,109 passed; the CLI
AWS-doctor case passes 8/8 with runtime-injected static AWS credential
variables unset.
- `pnpm build`
- `git diff --check origin/master...HEAD`

## Risks

- Migration `0221` adds one nullable foreign key and one index. It uses
idempotent guards.
- The accept route performs a governed write after it records card
acceptance. A failed write is visible and settles the proposal as
rejected.
- Concurrent proposal and card resolution must use
proposal-before-interaction lock order. A race test covers direct
approval against card rejection.
- The new audit event increases activity rows for newly added agent
bindings. It does not include secret values or fingerprints.
- The card includes only safe proposal metadata. It does not include
secret value, fingerprint, version, or internal secret identifiers.
- The UI uses the stitched resolution result. Focused tests cover
immediate cache replacement and every terminal state.

> This work extends an existing completed roadmap capability. The GitHub
duplicate search returned no other open related work.

## Model Used

- OpenAI Codex with model ID `gpt-5`. The runtime did not expose its
context-window size. Reasoning, repository tools, code execution,
database integration tests, UI rendering, and GitHub tools were enabled.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
…ts (#11626)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The run orchestrator prepares an execution environment before an
agent starts
> - A sandbox driver creates a remote folder before the adapter uploads
repository content
> - The orchestrator ran the host `provisionCommand` in that empty
folder
> - The command failed with exit 127 before the adapter could run its
`stage.sync` step
> - This pull request skips host provisioning for sandbox drivers and
keeps the existing local and SSH behavior
> - The benefit is that sandbox runs reach the adapter sync step without
an empty-folder setup failure

## Linked Issues or Issue Description

**What happened?**

A sandbox environment ran the host `provisionCommand` before the adapter
uploaded repository content. The command ran in an empty remote folder
and failed with exit 127.

**Expected behavior**

The orchestrator should skip host provisioning for a sandbox driver. The
adapter should upload the provisioned tree during its `stage.sync` step.

**Steps to reproduce**

1. Configure an environment with the `sandbox` driver and a host
`provisionCommand`.
2. Start a run that uses this environment.
3. Observe that the command runs in the empty sandbox folder and the run
fails with `setup_failed`.

**Paperclip version or commit**

Reproduced on the current `master` commit before this change.

**Deployment mode**

Built from source with a sandbox environment.

**Installation method**

Built from source with pnpm.

**Agent adapter(s) involved**

Not adapter-specific (core bug). The sandbox adapter syncs the tree
after environment setup.

**Database mode**

Not database-related.

**Additional context**

Related context:
[#11091](#11091) changes
provision behavior for reused workspaces. This pull request covers the
separate sandbox ordering failure.

## What Changed

- Skip the orchestrator provision step when `environment.driver` is
`sandbox`.
- Keep the existing skip for `local` and the provision step for `ssh`.
- Log one info message when a sandbox skip drops a present command.
- Keep the existing `plugin` path because it has no `stage.sync` step
and runs against the host filesystem.
- Add tests for sandbox, local, SSH, plugin, logging, and provision
failures.

## Verification

- Run `./node_modules/.bin/vitest run
server/src/__tests__/environment-run-orchestrator.test.ts`.
- Confirm that the test run passes all 10 tests.
- Confirm that CI checks pass on this pull request.

## Risks

- Low risk. The change affects only the provision gate for sandbox
drivers.
- SSH and local behavior stays unchanged.
- The plugin driver stays on its current path.
- The new log line makes a sandbox skip visible to operators.

## Model Used

OpenAI, GPT-5, exact runtime model `gpt-5`, with tool use and code
review support. The implementation author used this model to inspect
code, edit source and tests, and run the targeted test suite.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above (no exact duplicate found; related PR #11091 reviewed)
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes (no
documentation change applies to this internal gate correction)
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
…it identity (#11637)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Runs execute in transported workspaces; at finalize, the host syncs
the sandbox git history back into the local worktree
> - When both sides advanced, `integrateImportedGitHead` reconciles them
with `git merge-tree` plus `git commit-tree` on the host
> - Execution hosts are often containers with no git config and no
resolvable hostname, so `commit-tree` fails with "Author identity
unknown"
> - That one local command failure marks the whole run as failed, even
though the run's work succeeded
> - This pull request gives sync-created merge commits an explicit,
deterministic identity at the call site
> - The benefit is that workspace finalize no longer depends on ambient
host git configuration

## Linked Issues or Issue Description

No existing issue found. I searched issues and PRs for "Author identity
unknown", "unrelated histories", and "commit-tree identity".

**What happened?**

A run finished its work, but workspace finalize failed. The host-side
sync ran `git commit-tree <tree> -p <localHead> -p <importedHead> -m
"Paperclip remote git sync merge <sha>"`. Git exited with `Author
identity unknown ... fatal: unable to auto-detect email address (got
'node@<container-id>.(none)')`. The adapter recorded the whole run as
failed, and the host worktree kept the stale head. Any container
deployment without a global gitconfig reproduces this; I observed it on
a Paperclip Cloud stack.

**Expected behavior**

Commits that the sync machinery itself creates must not depend on
ambient host git configuration. The merge commit is machine-authored, so
it should carry a deterministic Paperclip identity.

**Steps to reproduce**

1. Run the Paperclip server in a container with no
`user.name`/`user.email` git config and a hostname git cannot turn into
an email.
2. Let a run's sandbox branch diverge from the host worktree, so both
sides advance.
3. Workspace finalize calls `integrateImportedGitHead`. The `git
commit-tree` step fails with "Author identity unknown" and the run
fails.

## What Changed

- `git-workspace-sync.ts`: new exported `GIT_SYNC_COMMIT_IDENTITY_ARGS`
(`-c user.name=Paperclip -c user.email=noreply@paperclip.ing`), applied
to the `commit-tree` call in `integrateImportedGitHead`.
- `ssh.ts`: the SSH-sync copy of `integrateImportedGitHead` applies the
same identity args to its `commit-tree` call.
- New regression test: builds divergent histories in a repo with no
configured identity and asserts the sync merge commit is created with
the deterministic identity, correct parents, and merged tree.

## Verification

- `pnpm vitest run
packages/adapter-utils/src/git-workspace-sync.test.ts` — 18/18 pass.
- `pnpm --filter @paperclipai/adapter-utils typecheck` — clean.
- Negative proof: with the source fix stashed, the new test fails on the
identity assertion.
- Full `pnpm vitest run packages/adapter-utils`: every file passes
except `local-process-sandbox.test.ts`, which fails identically on an
untouched `master` checkout on macOS (bubblewrap-dependent,
pre-existing, unrelated).

## Risks

Low risk. The change only adds `-c` identity flags to two
machine-generated commit invocations. `GIT_AUTHOR_*` / `GIT_COMMITTER_*`
environment variables still take precedence over `-c` when an operator
sets them, so existing deployments that configure an identity keep their
behavior.

## Model Used

- Claude Fable 5 (`claude-fable-5`), extended thinking, via Claude Code
CLI (tool use for code exploration, test runs, and verification).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes (no
doc surface describes this internal sync path)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
…ling the run (#11638)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - At run finalize, the host imports the sandbox git history and
reconciles it with the local worktree in `integrateImportedGitHead`
> - Transported workspaces are depth-1 shallow clones, so the boundary
commit reads as parentless inside the sandbox
> - A `git commit --amend` there rewrites the boundary commit into a
root commit, and the re-imported history no longer connects to the host
history
> - `git merge-tree` has no common base to merge against, so the sync
throws "Failed to merge concurrent remote git histories" and the run
fails with its work stranded in the sandbox
> - This pull request grafts the imported tree onto the current head as
a single commit instead of failing
> - The benefit is that a history rewrite inside the sandbox can no
longer lose a run's work

## Linked Issues or Issue Description

No existing issue found. I searched issues and PRs for "unrelated
histories", "Failed to merge concurrent", and "shallow". Depends on
#11637 (merged; the graft commit reuses its identity constant). This PR
is now rebased onto `master`.

**What happened?**

An agent run amended a commit inside its sandbox workspace to address
review feedback. The sandbox clone is depth-1 shallow, so git treated
the boundary commit as parentless and the amend produced a root commit.
At finalize, the host-side sync failed with `Failed to merge concurrent
remote git histories for <sha>` and the run was marked failed. A
follow-up run had to repair the branch by hand: fetch the true parent
from origin and rebuild the commit with `git commit-tree`.

**Expected behavior**

The sync must never strand completed work. When the imported history
shares no ancestor with the local one, the imported tree should still
land on the current head, with the imported message preserved and the
graft recorded.

**Steps to reproduce**

1. Start a run whose workspace transport uses the shallow clone path
(`withShallowGitWorkspaceClone`, depth 1).
2. Inside the sandbox workspace, run `git commit --amend` on the
boundary commit. The result is a parentless root commit.
3. Finish the run. The host-side `integrateImportedGitHead` finds no
merge base, `merge-tree` fails, and the run fails.

## What Changed

- `git-workspace-sync.ts`: new exported
`createUnrelatedHistoryGraftCommit` helper. It reads the imported head's
tree and message, and creates one commit on top of the current head with
the deterministic sync identity and a trailer that records the graft and
both shas.
- `integrateImportedGitHead` (both the remote-git-sync version and the
SSH copy in `ssh.ts`): when `merge-base` reports no common ancestor,
graft instead of throwing. The ref update keeps the same
compare-and-swap and concurrent-retry semantics as the merge path.
- The graft is gated on `git merge-base` exiting with status 1 — the
no-ancestor signal. Operational failures (timeout, missing object,
repository error) keep the loud merge failure instead of rewriting the
tip.
- New regression tests: one builds the exact shallow-amend shape (a root
commit rebuilt from the base tree) and asserts the graft lands on the
current head with the imported tree, subject, and graft trailer; one
integrates a well-formed sha the repository does not hold and asserts
the integration still throws with the branch tip unchanged.

## Verification

- `pnpm vitest run
packages/adapter-utils/src/git-workspace-sync.test.ts` — 19/19 pass
(includes the new graft test and the merge-base failure-discrimination
test).
- `pnpm --filter @paperclipai/adapter-utils typecheck` — clean.
- Full `pnpm vitest run packages/adapter-utils`: every file passes
except `local-process-sandbox.test.ts`, which fails identically on an
untouched `master` checkout on macOS (bubblewrap-dependent,
pre-existing, unrelated).

## Risks

- Behavioral shift: unrelated imported histories previously failed the
integration; now they land as a squash-graft. In this degenerate case
there is no base to merge against, so the imported tree is taken
wholesale and concurrent local-only tree changes are superseded at the
tip. The local commits keep their place in the graft's ancestry, and the
trailer records both shas, so nothing is unrecoverable. The old behavior
lost the imported work instead, which is the worse failure for an
autonomous run.
- The graft reuses the imported head's commit message, so branch history
still reads naturally after a sandbox rewrite.

## Model Used

- Claude Fable 5 (`claude-fable-5`), extended thinking, via Claude Code
CLI (tool use for code exploration, test runs, and verification).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes (no
doc surface describes this internal sync path)
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
…eaper (#11642)

## Thinking Path

> - Paperclip is an open source app that manages AI agents for work.
> - The server manages execution workspaces and their worktrees.
> - The terminal workspace reaper removes a workspace when its issue
tree reaches a terminal state.
> - Immediate removal prevents a person from reopening recently
completed work.
> - This pull request adds a configurable cooldown before the reaper
archives the workspace.
> - The cooldown keeps recent work available and keeps immediate cleanup
available with value `0`.

## Linked Issues or Issue Description

Refs: #7790

**Problem**

The reaper archives an execution workspace and deletes its worktree as
soon as the issue tree becomes terminal. A person cannot reopen recent
work without extra effort.

**Expected behavior**

The reaper should keep a recently completed workspace during a
configurable cooldown window. It should archive older work and support
immediate cleanup when the value is `0`.

**Proposed solution**

Read the cooldown from `PAPERCLIP_WORKSPACE_REAPER_COOLDOWN_DAYS`. Use a
seven-day default. Use the latest terminal timestamp in the source issue
tree as the cooldown anchor.

## What Changed

- Add `PAPERCLIP_WORKSPACE_REAPER_COOLDOWN_DAYS` with a seven-day
default.
- Treat `0` as no cooldown and use the default for negative or
non-numeric values.
- Use the latest `completedAt` or `cancelledAt` value in the source
issue tree.
- Use `updatedAt` when a terminal timestamp is null.
- Skip candidates inside the cooldown and report them in
`skippedCooldown`.
- Recheck the cutoff during the guarded archive operation.
- Document the environment variable and add focused tests.

## Verification

- Run `npx vitest run
server/src/__tests__/execution-workspaces-service.test.ts`.
- Confirm that the test run passes 66 tests.
- Confirm that the tests cover a recent tree, an old tree, value `0`,
and a null terminal timestamp.
- Confirm that the changed files pass `tsc --noEmit`.

## Risks

The default changes terminal workspace cleanup from immediate removal to
a seven-day delay. A value of `0` preserves immediate cleanup. The
guarded archive check limits race risk during concurrent lifecycle
changes.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap.

## Model Used

OpenAI Codex, GPT-5, tool use and code execution. This model assisted
with the implementation review and PR preparation.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
… templates (#11641)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip environments can use captured custom images for agent runs
> - A configuration fingerprint change can detach a valid custom-image
template
> - Operators need a safe way to confirm that the image still matches
the boot source
> - This pull request adds a guarded relink action with drift
classification and audit logging
> - The benefit is a deliberate relink without a new sandbox boot or
provider snapshot

## Linked Issues or Issue Description

**Subsystem affected**

Cross-cutting environment, server, and UI behavior.

**Problem or motivation**

A custom-image template detaches when the environment configuration
fingerprint changes. The runtime then uses the base image, even when the
boot source did not change. The only prior remedy required a full
re-capture.

**Proposed solution**

Add an operator-triggered relink action. Classify configuration drift
from a server-owned boot-relevant snapshot. Relink knob-only drift
without confirmation. Require explicit confirmation for boot-source or
unclassified drift. Guard the route for instance administrators and
record a safe activity event.

**Alternatives considered**

Keep requiring a full re-capture. This adds a sandbox boot and provider
snapshot for cases where the image remains correct.

**Roadmap alignment**

The roadmap has no matching custom-image relink item. This change
addresses an environment operation gap.

**Additional context**

The relink response exposes raw drift values only in the transient 409
response to the instance administrator. The service never persists or
logs fingerprints or configuration values. Reserved identity-path
segments fail closed.

## What Changed

- Add `relinkActiveTemplate` with drift classification and conditional
fingerprint update.
- Persist a server-owned boot-relevant configuration snapshot during
capture.
- Add the guarded relink route with strict request validation and
activity logging.
- Add the relink action and confirmation flow to the environment page.
- Add service, route, UI, and OpenAPI coverage.

## Verification

- Run the focused service suite: `pnpm vitest run
server/src/services/environment-custom-images-service.test.ts`.
- Run the focused route suite: `pnpm vitest run
server/src/routes/environment-custom-image-routes.test.ts`.
- Run the focused UI suite: `pnpm vitest run
ui/src/pages/CompanyEnvironments.test.tsx`.
- Run server and UI TypeScript checks.
- Confirm the OpenAPI snapshot matches the new route.
- Confirm all required GitHub checks pass on commit
`e46fdcfe94a719be854adf8849d30714e5b70b93`.
- Confirm Greptile reports 5/5 with no unresolved review threads.

## Risks

The relink action can keep an image after configuration drift. The
service requires explicit confirmation for boot-source or unclassified
drift. Reserved path segments produce a safe unresolved marker and never
enter stored values.

## Model Used

OpenAI GPT-5 Codex. The model used repository inspection, GitHub
operations, and PR preparation with tool use and code execution. The
runtime did not expose a context-window value or a separate
reasoning-mode value.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
…nts (#9237)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When an agent runs through the Hermes gateway adapter, its stdout is
parsed line-by-line into transcript entries that the issue chat renders
(the UI fetches the adapter's `./ui-parser` from
`/api/adapters/:type/ui-parser.js` and runs `parseStdoutLine`
client-side)
> - Reasoning-capable models emit a `reasoning.available` gateway event
carrying the model's reasoning text, and the chat renders `thinking`
parts as expandable chain-of-thought
> - The gateway parser mapped `reasoning.available` to a hardcoded
`"Hermes reasoning available"` string and discarded the event payload,
so the "thinking" part had no real content — the indicator looked static
and expanding it revealed nothing (#9209)
> - This pull request extracts the actual reasoning text from the event
payload and uses it as the `thinking` part's text, keeping the old
string only as a fallback for payloads that carry no text
> - The benefit is that the "Hermes reasoning available" indicator now
surfaces the model's real reasoning, which the existing
expandable-thinking UI can display

## Linked Issues or Issue Description

Fixes: #9209

## What Changed

- `packages/adapters/hermes/src/gateway/ui/parse-stdout.ts`: the
`reasoning.available` handler now extracts the reasoning text from the
event `data` via a small helper (`extractReasoningText`), checking the
plausible field names (`reasoning`, `reasoning_text`, `thinking`,
`text`, `summary`, `content`) and recursing one level into nested `data`
/ `payload` records, with ANSI stripped. The prior `"Hermes reasoning
available"` string is kept only as a fallback when no text field is
present.
- `packages/adapters/hermes/gateway-ui-parser.cjs`: applied the
identical logical change to the committed CommonJS mirror (exported as
`./gateway/ui-parser`), keeping the two files in sync.
- `packages/adapters/hermes/src/gateway/ui/parse-stdout.test.ts` (new):
unit tests for the gateway parser (there were none) covering
direct-field, `summary`, nested `data`/`payload` extraction, the no-text
fallback, and regression guards for `message.delta` and plain stdout.

## Verification

Ran from `packages/adapters/hermes`:

- `node_modules/.bin/vitest run src/gateway/ui/parse-stdout.test.ts` →
**8/8 passed**.
- Negative control: stashed the source changes and re-ran the same test
file against the current (pre-patch) parser → **4/8 failed** (exactly
the reasoning-extraction assertions), then restored — confirming the
tests are discriminating, not vacuous.
- `npx tsc --noEmit -p .` → clean.

Real-behavior proof (driving the actual shipped `gateway-ui-parser.cjs`
`parseStdoutLine`) is in the block below.

## Risks

- **Low risk.** Behavior is unchanged for events that carry no
recognizable text field — the `"Hermes reasoning available"` fallback is
preserved (verified). Only the `reasoning.available` branch changed;
`message.delta`, `run.failed`/`run.error`, and the generic/system/stdout
branches are untouched.
- The exact field name in a real `reasoning.available` payload is
defined by the external Hermes gateway and is not present anywhere in
this repo, so the extraction is intentionally defensive across several
plausible field names rather than pinned to one. If the real event nests
the text differently than `data` / `payload`, it will fall back to the
existing placeholder (i.e. no regression vs. today). Happy to tighten
the field list against real gateway traffic if a maintainer can share a
sample.

## Model Used

Claude Sonnet 5 (`claude-sonnet-5`) via Claude Code, with tool use and
local test execution (ran vitest/tsc against the change). Planning, code
review, and the real-behavior proof were done with Claude (Opus 4.8) in
the same session.

## Real behavior proof

**Behavior addressed:** A `reasoning.available` Hermes gateway event now
produces a `thinking` transcript part containing the model's real
reasoning text, instead of a static `"Hermes reasoning available"`
placeholder with no content behind it (#9209).

**Real environment tested:** Drove the actual shipped production
artifact — `packages/adapters/hermes/gateway-ui-parser.cjs`, the exact
module the UI loads via `/api/adapters/hermes-gateway/ui-parser.js` and
runs to parse gateway stdout — on Node v24.16.0, macOS. The input is a
raw stdout line in the exact format emitted by
`packages/adapters/hermes/src/gateway/server/execute.ts`
(`[hermes-gateway:event] run=… event=reasoning.available data=…`). Only
the external gateway boundary (the raw line) is synthesized; the parser
code path is the real one.

**Exact steps or command run after this patch:**
```
# BEFORE = git show HEAD:…/gateway-ui-parser.cjs ; AFTER = patched artifact
node proof.cjs   # requires each parser build and calls parseStdoutLine(line, ts)
# line = [hermes-gateway:event] run=run-abc123 event=reasoning.available \
#        data={"text":"Checking whether the cache key includes the tenant id before I refactor the lookup."}
```

**Evidence after fix:**
```
===== BEFORE (master / old code) =====
[ { "kind": "thinking", "ts": "…", "text": "Hermes reasoning available" } ]
thinking part carries real reasoning text? -> NO (static placeholder, nothing for the UI to expand)

===== AFTER (this patch) =====
[ { "kind": "thinking", "ts": "…",
    "text": "Checking whether the cache key includes the tenant id before I refactor the lookup." } ]
thinking part carries real reasoning text? -> YES
```
Additional cases through the same shipped artifact after the patch:
```
-- nested payload (data.payload.reasoning) --
{"kind":"thinking","ts":"…","text":"Weighing two migration orders."}
-- bare signal, no text field (regression guard) --
{"kind":"thinking","ts":"…","text":"Hermes reasoning available"}     # fallback preserved
-- message.delta still works (regression guard) --
{"kind":"assistant","ts":"…","text":"Hello","delta":true}
```

**Observed result after fix:** The `reasoning.available` event yields a
`thinking` part carrying the model's real reasoning text (top-level or
nested), which the existing expandable-thinking rendering in the chat
can display. Events with no text field still yield the original
placeholder, and unrelated events are unaffected.

**What was not tested:** I did not run against a live Hermes gateway —
Paperclip's Hermes gateway binary and its credentials aren't available
on this machine, and no captured real `reasoning.available` payload
exists in the repo, so the exact wire field name is inferred (hence the
defensive multi-field extraction + safe fallback). I also did not render
the full React chat component in jsdom; the change is confined to the
parser, and the chat's expandable `thinking` rendering already exists
(`ui/src/components/IssueChatThread.tsx`). CI / unit tests here are
supplemental to the runtime proof above.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above (searched `9209 in:body` and keyword variants — none found)
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change
(`fix/hermes-reasoning-available-payload`) and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] I have updated relevant documentation to reflect my changes (no
user-facing docs describe this behavior; none needed)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (will confirm once CI runs on the
PR)
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(will address on review)
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Bumps [@playwright/test](https://github.com/microsoft/playwright) from
1.61.1 to 1.62.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/microsoft/playwright/releases">@​playwright/test's
releases</a>.</em></p>
<blockquote>
<h2>v1.62.1</h2>
<h3>Bug Fixes</h3>
<ul>
<li><a
href="https://redirect.github.com/microsoft/playwright/issues/41989">#41989</a>
[Regression]: tsconfig &quot;extends&quot; bare specifier isn't resolved
via node_modules walk-up like tsc (fatal since 1.62)</li>
<li><a
href="https://redirect.github.com/microsoft/playwright/issues/41998">#41998</a>
[Regression]: directory-form tsconfig project references
(&quot;path&quot;: &quot;../pkg&quot;) fail to resolve (fatal since
1.62)</li>
<li><a
href="https://redirect.github.com/microsoft/playwright/issues/41985">#41985</a>
Accessibility snapshot drops button name when text is nested inside
spans with aria-hidden SVG</li>
<li><a
href="https://redirect.github.com/microsoft/playwright/issues/42000">#42000</a>
[Regression]: page.evaluate() arg of a branded primitive type (string
&amp; { brand }) no longer type-checks since 1.62</li>
<li><a
href="https://redirect.github.com/microsoft/playwright/issues/42013">#42013</a>
[BUG]Image-type actionable elements are not presented in the
snapshot.</li>
</ul>
<h2>v1.62.0</h2>
<h2>🧱 New component testing model</h2>
<p><a href="https://playwright.dev/docs/test-components">Component
testing</a> moves to a <strong>stories and galleries</strong> model.
A <strong>story</strong> wraps your component in one specific scenario —
hard-coded props, mock data, providers — and a <strong>gallery</strong>
page that you serve renders stories on demand.
The new <a
href="https://playwright.dev/docs/api/class-fixtures#fixtures-mount">fixtures.mount()</a>
fixture navigates to the gallery, mounts a story by id, and returns a <a
href="https://playwright.dev/docs/api/class-locator">Locator</a> scoped
to the story's root element:</p>
<pre lang="js"><code>test('click should expand', async ({ mount }) =&gt;
{
  const component = await mount('components/Expandable/Stateful');
  await component.getByRole('button').click();
  await expect(component.getByTestId('expanded')).toHaveValue('true');
});
</code></pre>
<p>Pass a story type as a template argument to type-check its props, and
use <code>update(props)</code> / <code>unmount()</code> on the returned
locator to re-render or tear down within a test.</p>
<h2>🛑 Cancel operations with AbortSignal</h2>
<p>Most operations and web-first assertions now accept a
<code>signal</code> option that takes an <a
href="https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal"><code>AbortSignal</code></a>,
letting you cancel long-running actions, navigations, waits, and
assertions:</p>
<pre lang="js"><code>const controller = new AbortController();
setTimeout(() =&gt; controller.abort(), 1000);
<p>await page.getByRole('button', { name: 'Submit' }).click({ signal:
controller.signal });
await expect(page.getByText('Done')).toBeVisible({ signal:
controller.signal });
</code></pre></p>
<p>Providing a signal does not disable the default timeout; pass
<code>timeout: 0</code> to disable it.</p>
<h2>🖼️ WebP screenshots</h2>
<p><a
href="https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1">expect(page).toHaveScreenshot()</a>
and <a
href="https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-screenshot-1">expect(locator).toHaveScreenshot()</a>
can now store snapshots in the WebP format — just give the snapshot a
<code>.webp</code> name:</p>
<pre lang="js"><code>// Visual comparisons store the golden snapshot as
lossless WebP.
await expect(page).toHaveScreenshot('homepage.webp');
<p>// Standalone screenshots can trade quality for size with lossy WebP.
await page.screenshot({ path: 'homepage.webp', quality: 50 });
&lt;/tr&gt;&lt;/table&gt;
</code></pre></p>
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/microsoft/playwright/commit/26a9e470a7b3c7822084b09fb7f13902c5f37b51"><code>26a9e47</code></a>
cherry-pick(<a
href="https://redirect.github.com/microsoft/playwright/issues/42043">#42043</a>):
docs: release notes for v1.62 Python, Java, and .NET (<a
href="https://redirect.github.com/microsoft/playwright/issues/4">#4</a>...</li>
<li><a
href="https://github.com/microsoft/playwright/commit/0a81d5d09b10eeefe228fe745c3f80c7368a239b"><code>0a81d5d</code></a>
cherry-pick(<a
href="https://redirect.github.com/microsoft/playwright/issues/42040">#42040</a>):
docs(release-notes): mention the isolated headless clipb...</li>
<li><a
href="https://github.com/microsoft/playwright/commit/83768264e64a821bcef9e634b8e5c33897f2b032"><code>8376826</code></a>
cherry-pick(<a
href="https://redirect.github.com/microsoft/playwright/issues/42034">#42034</a>):
fix(aria): keep icon-only clickable elements in ai snaps...</li>
<li><a
href="https://github.com/microsoft/playwright/commit/66c5cc92a60ce20ab3abe779339e1f90d2e2e888"><code>66c5cc9</code></a>
chore: mark v1.62.1 (<a
href="https://redirect.github.com/microsoft/playwright/issues/42020">#42020</a>)</li>
<li><a
href="https://github.com/microsoft/playwright/commit/9672bc3f2a7098cb6a9791ca97222187363a3037"><code>9672bc3</code></a>
cherry-pick(<a
href="https://redirect.github.com/microsoft/playwright/issues/42009">#42009</a>):
fix(types): support branded primitives in evaluate argum...</li>
<li><a
href="https://github.com/microsoft/playwright/commit/4325804427a214aa0c8c39bb1352f4ac4f712fd1"><code>4325804</code></a>
cherry-pick(<a
href="https://redirect.github.com/microsoft/playwright/issues/41988">#41988</a>):
fix(aria): preserve names from collapsed text contributors</li>
<li><a
href="https://github.com/microsoft/playwright/commit/9632f8ecbc2accba140ea342f1070ccfdd5f5d41"><code>9632f8e</code></a>
cherry-pick(<a
href="https://redirect.github.com/microsoft/playwright/issues/42005">#42005</a>):
fix(tsconfig): do not throw when
&quot;extends&quot;/&quot;references&quot; ...</li>
<li><a
href="https://github.com/microsoft/playwright/commit/e3950d9c140d007bd52853b45813c6274b24e36f"><code>e3950d9</code></a>
chore: mark v1.62.0 (<a
href="https://redirect.github.com/microsoft/playwright/issues/41981">#41981</a>)</li>
<li><a
href="https://github.com/microsoft/playwright/commit/f07e0f720fbe6691cc3d3d66ff9f3e58139e804c"><code>f07e0f7</code></a>
cherry-pick(<a
href="https://redirect.github.com/microsoft/playwright/issues/41940">#41940</a>):
docs: release notes for v1.62 (<a
href="https://redirect.github.com/microsoft/playwright/issues/41967">#41967</a>)</li>
<li><a
href="https://github.com/microsoft/playwright/commit/05a306c78f11767535fd986eebab5d4c4dad4614"><code>05a306c</code></a>
cherry-pick(<a
href="https://redirect.github.com/microsoft/playwright/issues/41964">#41964</a>):
Revert &quot;feat(routeFromHar): add interceptAPIRequests opt...</li>
<li>Additional commits viewable in <a
href="https://github.com/microsoft/playwright/compare/v1.61.1...v1.62.1">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…1513)

Bumps
[@storybook/addon-docs](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/docs)
from 10.5.0 to 10.5.8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/storybookjs/storybook/releases">@​storybook/addon-docs's
releases</a>.</em></p>
<blockquote>
<h2>v10.5.8</h2>
<h2>10.5.8</h2>
<ul>
<li>React: Fix RDT tsconfig selection for Vite project references - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35743">#35743</a>,
thanks <a
href="https://github.com/ndelangen"><code>@​ndelangen</code></a>!</li>
<li>Tanstack React: Remove <code>@​cloudflare/vite-plugin</code> from
the inherited Vite config - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35706">#35706</a>,
thanks <a
href="https://github.com/FrancoKaddour"><code>@​FrancoKaddour</code></a>!</li>
<li>Tanstack: Wait for router to load before rendering - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35784">#35784</a>,
thanks <a
href="https://github.com/huang-julien"><code>@​huang-julien</code></a>!</li>
<li>Test: Fix Illegal invocation when reading prototype.focus - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35528">#35528</a>,
thanks <a
href="https://github.com/FrancoKaddour"><code>@​FrancoKaddour</code></a>!</li>
</ul>
<h2>v10.5.7</h2>
<h2>10.5.7</h2>
<ul>
<li>Angular: Serve ancestor node_modules for addon-vitest in browser
mode - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35600">#35600</a>,
thanks <a
href="https://github.com/brandonroberts"><code>@​brandonroberts</code></a>!</li>
<li>Refactor: Update getVersionedPackages method to handle non-Storybook
packages correctly - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35769">#35769</a>,
thanks <a
href="https://github.com/valentinpalkovic"><code>@​valentinpalkovic</code></a>!</li>
</ul>
<h2>v10.5.6</h2>
<h2>10.5.6</h2>
<ul>
<li>Dependencies: Pin `@testing-library/jest-dom` to `6.9.1` - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35614">#35614</a>,
thanks <a
href="https://github.com/ndelangen"><code>@​ndelangen</code></a>!</li>
<li>ESLint Plugin: Add plugin meta and document oxlint usage - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35655">#35655</a>,
thanks <a
href="https://github.com/yannbf"><code>@​yannbf</code></a>!</li>
<li>Vue: Skip docgen for module ids carrying a query - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35598">#35598</a>,
thanks <a
href="https://github.com/seanogdev"><code>@​seanogdev</code></a>!</li>
</ul>
<h2>v10.5.5</h2>
<h2>10.5.5</h2>
<ul>
<li>CLI: Update AI setup instructions to msw-storybook-addon v3 - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35512">#35512</a>,
thanks <a
href="https://github.com/yannbf"><code>@​yannbf</code></a>!</li>
<li>Core: Upgrade `ws` to fix security advisories - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35584">#35584</a>,
thanks <a
href="https://github.com/ndelangen"><code>@​ndelangen</code></a>!</li>
<li>ReactNative: Telemetry framework detection fix - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35560">#35560</a>,
thanks <a
href="https://github.com/ndelangen"><code>@​ndelangen</code></a>!</li>
<li>SyntaxHighlighter: Fix PrismJS dark mode mismatch - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35541">#35541</a>,
thanks <a
href="https://github.com/hxy-asdw"><code>@​hxy-asdw</code></a>!</li>
<li>TanStack: Preserve explicit route ids on pathful clones - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35499">#35499</a>,
thanks <a
href="https://github.com/unpunnyfuns"><code>@​unpunnyfuns</code></a>!</li>
<li>TanStack: Resolve mock redirects through Vite's resolver - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35501">#35501</a>,
thanks <a
href="https://github.com/unpunnyfuns"><code>@​unpunnyfuns</code></a>!</li>
<li>TanStack: Respect routeOverrides component overrides in stories - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35497">#35497</a>,
thanks <a
href="https://github.com/unpunnyfuns"><code>@​unpunnyfuns</code></a>!</li>
</ul>
<h2>v10.5.4</h2>
<h2>10.5.4</h2>
<ul>
<li>ReactNative: Telemetry framework detection fix - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35560">#35560</a>,
thanks <a
href="https://github.com/ndelangen"><code>@​ndelangen</code></a>!</li>
<li>SyntaxHighlighter: Fix PrismJS dark mode mismatch - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35541">#35541</a>,
thanks <a
href="https://github.com/hxy-asdw"><code>@​hxy-asdw</code></a>!</li>
</ul>
<h2>v10.5.3</h2>
<h2>10.5.3</h2>
<ul>
<li>Dependencies: Upgrade TypeScript to 6.0.3 - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34971">#34971</a>,
thanks <a
href="https://github.com/valentinpalkovic"><code>@​valentinpalkovic</code></a>!</li>
</ul>
<h2>v10.5.2</h2>
<h2>10.5.2</h2>
<ul>
<li>Angular-Vite: Drop <code>@​angular/platform-browser-dynamic</code>
peer dependency - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35457">#35457</a>,
thanks <a
href="https://github.com/valentinpalkovic"><code>@​valentinpalkovic</code></a>!</li>
<li>Angular-Vite: Widen TypeScript peer dependency range to support
TypeScript 6 - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35455">#35455</a>,
thanks <a
href="https://github.com/valentinpalkovic"><code>@​valentinpalkovic</code></a>!</li>
<li>Core: Include chromatic packages in ecosystem identifier - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35170">#35170</a>,
thanks <a
href="https://github.com/yannbf"><code>@​yannbf</code></a>!</li>
<li>TanStack: Fix createServerFn validator mock - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35185">#35185</a>,
thanks <a
href="https://github.com/sjh9714"><code>@​sjh9714</code></a>!</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md">@​storybook/addon-docs's
changelog</a>.</em></p>
<blockquote>
<h2>10.5.8</h2>
<ul>
<li>React: Fix RDT tsconfig selection for Vite project references - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35743">#35743</a>,
thanks <a
href="https://github.com/ndelangen"><code>@​ndelangen</code></a>!</li>
<li>Tanstack React: Remove <code>@​cloudflare/vite-plugin</code> from
the inherited Vite config - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35706">#35706</a>,
thanks <a
href="https://github.com/FrancoKaddour"><code>@​FrancoKaddour</code></a>!</li>
<li>Tanstack: Wait for router to load before rendering - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35784">#35784</a>,
thanks <a
href="https://github.com/huang-julien"><code>@​huang-julien</code></a>!</li>
<li>Test: Fix Illegal invocation when reading prototype.focus - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35528">#35528</a>,
thanks <a
href="https://github.com/FrancoKaddour"><code>@​FrancoKaddour</code></a>!</li>
</ul>
<h2>10.5.7</h2>
<ul>
<li>Angular: Serve ancestor node_modules for addon-vitest in browser
mode - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35600">#35600</a>,
thanks <a
href="https://github.com/brandonroberts"><code>@​brandonroberts</code></a>!</li>
<li>Refactor: Update getVersionedPackages method to handle non-Storybook
packages correctly - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35769">#35769</a>,
thanks <a
href="https://github.com/valentinpalkovic"><code>@​valentinpalkovic</code></a>!</li>
</ul>
<h2>10.5.6</h2>
<ul>
<li>Dependencies: Pin <code>@testing-library/jest-dom</code> to
<code>6.9.1</code> - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35614">#35614</a>,
thanks <a
href="https://github.com/ndelangen"><code>@​ndelangen</code></a>!</li>
<li>ESLint Plugin: Add plugin meta and document oxlint usage - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35655">#35655</a>,
thanks <a
href="https://github.com/yannbf"><code>@​yannbf</code></a>!</li>
<li>Vue: Skip docgen for module ids carrying a query - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35598">#35598</a>,
thanks <a
href="https://github.com/seanogdev"><code>@​seanogdev</code></a>!</li>
</ul>
<h2>10.5.5</h2>
<ul>
<li>CLI: Update AI setup instructions to msw-storybook-addon v3 - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35512">#35512</a>,
thanks <a
href="https://github.com/yannbf"><code>@​yannbf</code></a>!</li>
<li>Core: Upgrade <code>ws</code> to fix security advisories - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35584">#35584</a>,
thanks <a
href="https://github.com/ndelangen"><code>@​ndelangen</code></a>!</li>
<li>ReactNative: Telemetry framework detection fix - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35560">#35560</a>,
thanks <a
href="https://github.com/ndelangen"><code>@​ndelangen</code></a>!</li>
<li>SyntaxHighlighter: Fix PrismJS dark mode mismatch - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35541">#35541</a>,
thanks <a
href="https://github.com/hxy-asdw"><code>@​hxy-asdw</code></a>!</li>
<li>TanStack: Preserve explicit route ids on pathful clones - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35499">#35499</a>,
thanks <a
href="https://github.com/unpunnyfuns"><code>@​unpunnyfuns</code></a>!</li>
<li>TanStack: Resolve mock redirects through Vite's resolver - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35501">#35501</a>,
thanks <a
href="https://github.com/unpunnyfuns"><code>@​unpunnyfuns</code></a>!</li>
<li>TanStack: Respect routeOverrides component overrides in stories - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35497">#35497</a>,
thanks <a
href="https://github.com/unpunnyfuns"><code>@​unpunnyfuns</code></a>!</li>
</ul>
<h2>10.5.4</h2>
<ul>
<li>ReactNative: Telemetry framework detection fix - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35560">#35560</a>,
thanks <a
href="https://github.com/ndelangen"><code>@​ndelangen</code></a>!</li>
<li>SyntaxHighlighter: Fix PrismJS dark mode mismatch - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35541">#35541</a>,
thanks <a
href="https://github.com/hxy-asdw"><code>@​hxy-asdw</code></a>!</li>
</ul>
<h2>10.5.3</h2>
<ul>
<li>Dependencies: Upgrade TypeScript to 6.0.3 - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34971">#34971</a>,
thanks <a
href="https://github.com/valentinpalkovic"><code>@​valentinpalkovic</code></a>!</li>
</ul>
<h2>10.5.2</h2>
<ul>
<li>TanStack: Fix createServerFn validator mock - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35185">#35185</a>,
thanks <a
href="https://github.com/sjh9714"><code>@​sjh9714</code></a>!</li>
<li>TanStack: Support pathless layout routes (id-only) in story routing
- <a
href="https://redirect.github.com/storybookjs/storybook/pull/35465">#35465</a>,
thanks <a
href="https://github.com/unpunnyfuns"><code>@​unpunnyfuns</code></a>!</li>
<li>Tanstack-react: Add missing Hydrate export - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35111">#35111</a>,
thanks <a
href="https://github.com/arun-357"><code>@​arun-357</code></a>!</li>
<li>Tanstack-react: Keep JSX-only component references during dead-code
elimination - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35206">#35206</a>,
thanks <a
href="https://github.com/yatishgoel"><code>@​yatishgoel</code></a>!</li>
<li>Vitest: Fix coverage toggle crash on Vite 6 by clearing closed
Vitest instance on restart - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35461">#35461</a>,
thanks <a
href="https://github.com/yannbf"><code>@​yannbf</code></a>!</li>
</ul>
<h2>10.5.1</h2>
<ul>
<li>Angular-Vite: Drop <code>@​angular/platform-browser-dynamic</code>
peer dependency - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35457">#35457</a>,
thanks <a
href="https://github.com/valentinpalkovic"><code>@​valentinpalkovic</code></a>!</li>
<li>Angular-Vite: Widen TypeScript peer dependency range to support
TypeScript 6 - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35455">#35455</a>,
thanks <a
href="https://github.com/valentinpalkovic"><code>@​valentinpalkovic</code></a>!</li>
<li>Core: Include chromatic packages in ecosystem identifier - <a
href="https://redirect.github.com/storybookjs/storybook/pull/35170">#35170</a>,
thanks <a
href="https://github.com/yannbf"><code>@​yannbf</code></a>!</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/storybookjs/storybook/commit/6ef7d1ae816ebd5fb8bf84b8dec7d4a92410d73c"><code>6ef7d1a</code></a>
Bump version from &quot;10.5.7&quot; to &quot;10.5.8&quot; [skip
ci]</li>
<li><a
href="https://github.com/storybookjs/storybook/commit/7c6fb3a5ecf4495d73de6d70f802251934e079bd"><code>7c6fb3a</code></a>
Bump version from &quot;10.5.6&quot; to &quot;10.5.7&quot; [skip
ci]</li>
<li><a
href="https://github.com/storybookjs/storybook/commit/3126f0a14a351e971679f61cd0bd5609d086ed57"><code>3126f0a</code></a>
Bump version from &quot;10.5.5&quot; to &quot;10.5.6&quot; [skip
ci]</li>
<li><a
href="https://github.com/storybookjs/storybook/commit/05a52b7a888c6b85c3f8aa6765ed9a0a69a79e4c"><code>05a52b7</code></a>
Bump version from &quot;10.5.4&quot; to &quot;10.5.5&quot; [skip
ci]</li>
<li><a
href="https://github.com/storybookjs/storybook/commit/3327dc44697304275e28ceaa2cd34d9bede4e333"><code>3327dc4</code></a>
Bump version from &quot;10.5.3&quot; to &quot;10.5.4&quot; [skip
ci]</li>
<li><a
href="https://github.com/storybookjs/storybook/commit/9ac273930a49ad33b6a331f1f36dc472f5c36054"><code>9ac2739</code></a>
Bump version from &quot;10.5.2&quot; to &quot;10.5.3&quot; [skip
ci]</li>
<li><a
href="https://github.com/storybookjs/storybook/commit/b4b00f27662caf7328330b5ab47e9b903218f43a"><code>b4b00f2</code></a>
Merge pull request <a
href="https://github.com/storybookjs/storybook/tree/HEAD/code/addons/docs/issues/34971">#34971</a>
from storybookjs/valentin/upgrade-typescript-6</li>
<li><a
href="https://github.com/storybookjs/storybook/commit/518f711cb367d8df184be22f1fab9a218b2743df"><code>518f711</code></a>
Bump version from &quot;10.5.1&quot; to &quot;10.5.2&quot; [skip
ci]</li>
<li><a
href="https://github.com/storybookjs/storybook/commit/c253a0667d39899a0f7a09fa90f262f69ad4ae90"><code>c253a06</code></a>
Bump version from &quot;10.5.0&quot; to &quot;10.5.1&quot; [skip
ci]</li>
<li>See full diff in <a
href="https://github.com/storybookjs/storybook/commits/v10.5.8/code/addons/docs">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@storybook/addon-docs&package-manager=npm_and_yarn&previous-version=10.5.0&new-version=10.5.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
nickyleach and others added 30 commits August 26, 2026 22:20
…12280)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The server manages duplex channels that carry data between workers
and hosts.
> - The aggregate byte-ledger ceiling test can race the channel bind.
> - The race can make channel open fail before the test checks the
ceiling rejection.
> - This pull request writes one byte after the open call binds the
channel.
> - The test now checks the post-bind rejection and the retained-byte
count.
> - The benefit is a stable test that checks the intended byte-ledger
behavior.

## Linked Issues or Issue Description

**What happened?**

The duplex aggregate byte-ledger ceiling test scripted data during
channel open. Under load, the host could process the data notification
before the open continuation bound the route. The test then saw
`DUPLEX_CHANNEL_OPEN_FAILED` instead of the intended post-bind
rejection.

**Expected behavior**

The test must open the channel first. It must then write one byte and
confirm that the serialized host-to-worker frame exceeds the four-byte
ceiling. The route must reject the write and retain no bytes.

**Steps to reproduce**

1. Run the focused server test file.
2. Repeat the test several times under load.
3. Observe that the old test can fail during channel open.
4. Run the updated test and confirm the post-bind rejection.

**Paperclip version or commit**

b64fbcd

**Deployment mode**

Built from source with the server test runner.

**Installation method**

Built from source.

**Agent adapter(s) involved**

Not adapter-specific (core test).

**Database mode**

Not database-related.

**Additional context**

The change keeps the test-only scope to one file. A previous dependency
change used a separate pull request. This pull request covers the duplex
byte-ledger test fix only.

## What Changed

- Open the duplex channel without scripted data.
- Write one byte after the open call resolves.
- Update the test name and comments to describe the two reservations.
- Keep the change limited to
`server/src/__tests__/plugin-worker-manager-duplex-byte-ledger.test.ts`.

## Verification

- The focused test file passed five consecutive runs before this pull
request opened.
- The test passed with the four-byte ceiling.
- A control run with a 4096-byte ceiling failed only in this test case.
- GitHub Actions must pass the server test suite and all required gates.
- Greptile must return 5/5 with no open findings.

## Risks

Low risk. The change updates one test file and adds no production code.
The test now depends on the open call completing before the write, which
matches the route bind contract.

## Model Used

OpenAI Codex, GPT-5, tool use and code execution. The execution platform
manages the exact context window details.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with Fixes: # / Closes: #
/ Refs: # OR (b) described the issue in-PR following the relevant issue
template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
…Sentry server peer on the exact version (#12270)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Paperclip uses separate server and browser packages for runtime
services and the board.
> - Sentry integrations need an exact SDK version and safe optional
loading.
> - A version range can select an SDK that the privacy tests did not
audit.
> - Missing peer metadata does not describe the optional server SDK
contract.
> - This pull request pins the browser SDK and gates the optional server
SDK on its exact version.
> - The benefit is a clear SDK contract with fail-open startup behavior.

## Linked Issues or Issue Description

**What happened?**

The browser package used the range ^10.71.0, so a lockfile refresh could
select a newer SDK. The server loaded @sentry/node dynamically but did
not declare its optional peer contract.

**Expected behavior**

The browser package must use the audited 10.71.0 version. The server
must load @sentry/node only when the installed peer matches 10.71.0. The
server must start when the optional peer is absent.

**Steps to reproduce**

1. Install the project dependencies.
2. Inspect the browser Sentry version and the server package metadata.
3. Start the server without installing @sentry/node.
4. Confirm that the server starts and that the dynamic Sentry bootstrap
does not load an unsupported peer version.

**Paperclip version or commit**

9c57c0f

**Deployment mode**

Built from source with pnpm dev or pnpm build.

**Installation method**

Built from source.

**Agent adapter(s) involved**

Not adapter-specific (core change).

**Database mode**

Not database-related.

## What Changed

- Pin @sentry/browser to exactly 10.71.0 as a UI development dependency.
- Declare @sentry/node as an optional server peer dependency at 10.71.0.
- Gate the dynamic server bootstrap on the exact peer version.
- Add tests for the browser pin, peer metadata, version gate, and
fail-open loading.
- Document the supported server SDK version.
- Keep the lockfile unchanged because the pull request workflow
regenerates it for manifest changes.

## Verification

- Server tests pass with six expected skips when @sentry/node is absent.
- UI tests pass.
- The UI build emits the lazy Sentry browser chunk.
- git diff --check passes.
- GitHub pull request checks must pass after this pull request opens.
- Greptile must return a 5/5 score with no open findings.

## Risks

The exact version gate prevents Sentry startup when an unsupported SDK
version exists. The integration remains optional and fail-open. The
lockfile workflow must regenerate the lockfile before frozen downstream
jobs run. The label-gated Storybook visual job must not run until it can
restore the generated lockfile artifact.

## Model Used

OpenAI Codex, GPT-5, tool use and code review support, exact context
window details are managed by the execution platform.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with Fixes: # / Closes: #
/ Refs: # OR (b) described the issue in-PR following the relevant issue
template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Auto-generated lockfile refresh after dependencies changed on master.
This PR only updates pnpm-lock.yaml.

Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Sandbox providers run agent work in isolated environments
> - The Daytona documents described a command wrapper that the provider
no longer uses
> - Those documents therefore described a control that the code does not
have
> - This pull request states the real sandbox boundary and the controls
for paths that cross it
> - The benefit is accurate security guidance for sandbox provider
authors and operators

## Linked Issues or Issue Description

**Issue type**

Outdated (no longer matches behavior).

**Where is the issue?**

`packages/plugins/sandbox-providers/SANDBOX-REQUIREMENTS.md` and
`packages/plugins/sandbox-providers/daytona/README.md`.

**What's wrong?**

The Daytona provider no longer uses the documented command wrapper,
package installation commands, or sudoers rule. The requirements
document also lacked a clear statement of the sandbox security boundary.

**Suggested fix**

State that the sandbox provides the boundary. Name outbound workspace
synchronization and the application programming interface bridge as the
paths that cross the boundary. State that a provider must not map a host
path into a sandbox synchronization path.

## What Changed

- Replace stale wrapper requirements with the actual sandbox security
boundary.
- State the controls that apply to outbound workspace synchronization
and the application programming interface bridge.
- State that this repository does not enforce the provider path-mapping
duty today.
- Remove obsolete Daytona package-install commands and the sudoers rule.

## Verification

- Confirm the difference contains the two documentation files and the
test file changed by the follow-up fix.
- Confirm that no unrelated source, configuration, or fixture file
appears in the difference.
- Run the repository continuous integration checks and confirm that
every required check passes.
- Run the repository review bot and confirm its final verdict.

## Risks

Low risk. This pull request changes two documents and closes a database
client in one integration test. It does not change product runtime
behavior or configuration.

## Model Used

OpenAI Codex, GPT-5, with tool use and code execution. The model
produced the documentation change and the pull request text.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run the affected test locally; continuous integration
provides complete test verification.
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People write almost every issue, goal, document, and chat message
through the rich markdown editor, which wraps `@mdxeditor/editor` on top
of Lexical
> - On `master` that editor renders its raw-source textarea instead. It
shows "Rich editor unavailable for this markdown" on every field, for
all content, including empty content
> - Lexical throws at render time because the project holds two copies
of `LexicalBuilder`. The root `pnpm.overrides` block forced the Lexical
family past the version range the editor supports, and it missed the
packages that are reached transitively
> - Every markdown surface in the product degrades to plain text
editing, so this is a full loss of a core authoring feature and not a
cosmetic problem
> - This pull request removes the Lexical entries from `pnpm.overrides`,
pins the app to Lexical 0.48.0, and adds a test that guards the
resolution graph
> - The benefit is that the rich editor renders again, and a future
override or bump that splits Lexical fails in CI instead of in the
browser

## Linked Issues or Issue Description

No public issue exists. The problem is described below with the bug
report template.

**What happened?**

The rich markdown editor falls back to its raw-source textarea on every
markdown field. The header reads "Rich editor unavailable for this
markdown. Showing raw source instead." The fallback appears for all
content, including empty content. React catches an error during the
first render of the editor.

The cause is dependency resolution. The root `package.json`
`pnpm.overrides` block pinned the Lexical family to `0.49.0`.
`@mdxeditor/editor@4.2.1` declares `lexical: "^0.48.0"`. A caret range
on a `0.x` version pins the minor, so `^0.48.0` means `>=0.48.0
<0.49.0`. The override therefore pushed the editor past its only
supported line. Lexical 0.49.0 also carries breaking `$config()` node
changes.

The override list was also incomplete. `@lexical/extension`,
`@lexical/history`, and `@lexical/internal` are reached transitively and
were never listed. Those packages stayed on 0.48 while the listed
packages moved to 0.49. The graph mixed the two lines, and the built
browser bundle carried two
`Symbol.for("@lexical/extension/LexicalBuilder")` registrations.

Commit 04432f8 (#10728) introduced this. It was a Dependabot bump from
0.46.0 to 0.49.0.

**Expected behavior**

The rich editor renders a live WYSIWYG surface on every markdown field.
The raw-source fallback stays reserved for real markdown parse failures.

**Steps to reproduce**

1. Check out `master` and run `pnpm install`.
2. Start the app and open any issue, goal, or document.
3. Look at any markdown field, for example the issue description.
4. The field shows the fallback banner and a plain textarea. The rich
toolbar is absent.

You can also see the split without the browser:

```
pnpm --filter @paperclipai/ui build
grep -ohE 'Symbol\.for\("@lexical/extension/LexicalBuilder"\)' ui/dist/assets/index-*.js | wc -l
```

On `master` this prints `2`. A correct graph prints `1`.

**Paperclip version or commit**

`master` at 1ba7b2c. The regression entered at 04432f8 (#10728).

**Relevant logs or output**

```
LexicalBuilder.fromEditor: The given editor was created with LexicalBuilder
0.48.0+dev.esm but this version is 0.49.0+dev.esm. A project should have
exactly one copy of LexicalBuilder
```

React reports the error in `<LexicalExtensionEditorComposer>`.
`MarkdownEditorRichErrorBoundary` catches it and shows the raw-source
fallback.

## What Changed

- Removed the Lexical entries from `pnpm.overrides` in the root
`package.json`. This drops `lexical`, `@lexical/clipboard`,
`@lexical/link`, `@lexical/list`, `@lexical/markdown`,
`@lexical/plain-text`, `@lexical/react`, `@lexical/rich-text`,
`@lexical/selection`, and `@lexical/utils`. The `rollup`, `react`, and
`react-dom` overrides stay as they are.
- Pinned `lexical` and `@lexical/link` to `0.48.0` in `ui/package.json`.
The app subclasses `LinkNode` for mention-aware links and registers it
into the Lexical instance that MDXEditor owns, so node identity needs
one copy. `@lexical/link@0.48.0` pins `lexical` at `0.48.0` exactly.
- Left `pnpm-lock.yaml` out of this commit on purpose. The `policy` job
blocks manual lockfile edits, regenerates the lockfile from the
manifests, and uploads it for the downstream `pnpm install
--frozen-lockfile` jobs. I did regenerate the lockfile locally to verify
the result. That local lockfile touched only Lexical and MDXEditor
entries and held no Lexical 0.49 reference.
- Added `ui/src/lib/lexical-single-copy.test.ts`. It asserts the app and
the editor resolve the same `lexical` file, that `@lexical/link` and the
transitively reached `@lexical/extension` sit on that version, and that
the version satisfies the range the editor declares.

`ui` is the only workspace package that declares a Lexical dependency,
so nothing else changes.

### Why the overrides can go

#9180 added the Lexical overrides for a good reason. At that time the
app used Lexical 0.46.0 and `@mdxeditor/editor` declared
`lexical@^0.35.0`. The two ranges could not meet, so an override was the
only way to force one copy.

That condition is gone. `@mdxeditor/editor@4.2.1` now declares `lexical:
"^0.48.0"`. When the app also declares `0.48.0`, pnpm resolves one copy
on its own and the override has nothing left to do.

The override is now a liability. It can only pin the packages it lists
by name. `@lexical/extension` did not exist when the list was written,
so the list never covered it, and the list silently stopped covering the
whole family. Natural resolution has no such blind spot, because every
Lexical package carries its own exact pin on the core.

The existing `lexical` alias in `ui/vite.config.ts` and
`ui/vitest.config.ts` stays. It still resolves at 0.48.0. Note that the
alias covers only the `lexical` core, which is why it did not prevent
this bug and why the new test reads the resolution graph with
`createRequire` instead of importing Lexical.

### Related pull requests

- #10728 — the Dependabot bump that introduced the regression.
Superseded by this change.
- #10724 — the companion core bump merged the same day.
- #9180 — added the overrides block that this change removes. See the
explanation above.
- #9179 — an older, still-open 0.46.0 bump. It is stale and this change
does not depend on it.

I found no open pull request that already fixes this problem.

## Verification

All commands ran from the repository root.

- `npx vitest run ui/src/lib/lexical-single-copy.test.ts` — 3 passed.
- Guard proven against the broken tree. I restored the pre-fix
`package.json`, `ui/package.json`, and `pnpm-lock.yaml`, ran `pnpm
install`, and re-ran the guard. It failed 2 of 3: `expected '0.48.0' to
be '0.49.0'` for `@lexical/extension`, and `expected '^0.48.0' to be
'^0.49.0'` for the declared range. I then restored the fix and `pnpm
install` reproduced the committed lockfile byte for byte.
- `pnpm --filter @paperclipai/ui typecheck` — clean. The Lexical API
calls in the app still compile on 0.48.
- `npx vitest run ui/src/lib/mention-deletion.test.ts
ui/src/lib/mention-aware-link-node.test.ts
ui/src/components/MarkdownEditor.test.tsx` — 42 passed.
- `pnpm --filter @paperclipai/ui build`, then the `grep` above — the
bundle now holds 1 `LexicalBuilder` registration. It held 2 before this
change.
- Full `ui` project: `npx vitest run --project '@paperclipai/ui'` — 4426
of 4431 passed. I re-ran the 5 failures in isolation. Four passed, so
they were load-induced 5 second timeouts in
`CompanyEnvironments.test.tsx` and `IssuesList.test.tsx`. The last one,
`OnboardingWizard.test.tsx > renders instead of throwing when the
browser denies storage access`, also fails on clean `master` and is
unrelated to this change.
- Real render probe. I wrote a throwaway test that renders
`MarkdownEditor` against the real `@mdxeditor/editor` with no mock.
Before this change it produced the fallback banner and the
`LexicalBuilder` error. After this change the editor mounts a live
`contenteditable` surface and logs no error. The probe was temporary and
is not part of this diff.

## Risks

- This pins the app's Lexical to the line the editor supports rather
than to the newest release. A future Lexical upgrade must wait for an
`@mdxeditor/editor` release that supports it. I checked published
`@mdxeditor/editor` through 4.2.3 and none supports Lexical 0.49, so
fixing forward is not possible today. The new test enforces that
ordering, so an upgrade attempt fails in CI with a clear reason.
- Dependabot will offer the Lexical 0.49 bump again. The new test will
fail that pull request. That is the intended behavior. A reviewer should
keep the bump closed until MDXEditor moves first.
- 04432f8 was a routine version bump and not a security fix, so this
change reintroduces no known advisory.
- The change is limited to dependency resolution and one new test. It
alters no application code.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

Claude (Anthropic), Claude Opus, agentic tool use via Claude Code.

- Provider and model: Claude (Anthropic), Claude Opus
- Capability: agentic tool use through Claude Code, with repository
search, file edits, dependency installation, test and build runs

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes (no
document in the repository states a Lexical version or pinning policy,
so the reasoning is recorded in the new test's header comment)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
## Thinking Path

> - Paperclip helps people manage AI agents and their work
> - People use the rich markdown editor to write task descriptions,
comments, and agent instructions
> - The editor disables HTML processing, but the markdown parser still
treats some angle-bracket text as HTML
> - A value such as `<name>` could therefore stop the rich editor and
show the raw-source fallback
> - The retry action used the same input, so it could not recover
> - This pull request escapes unsupported angle brackets only while the
editor processes the value
> - The benefit is that the rich editor works and stored markdown stays
unchanged

## Linked Issues or Issue Description

Fixes: #12197

Related prior attempt: #2696

**What happened?**

The rich markdown editor did not start when prose contained a bare angle
bracket, such as `<name>`. It showed the raw-source fallback. The retry
action repeated the same failure.

**Expected behavior**

The rich editor starts for normal prose. It stores the text in its clean
form because agent prompts can use this text.

**Steps to reproduce**

1. Open a task description, comment, or other field that uses the rich
markdown editor.
2. Enter `Rename <name> to the real name`.
3. Reload the field.
4. Observe the raw-source fallback.

**Paperclip version or commit**

`master` at d785b19.

**Deployment mode**

All modes. The defect is in the UI package.

## What Changed

- Escape only angle brackets that the parser treats as unsupported HTML
constructs.
- Restore the clean markdown before the editor sends a value to its
parent.
- Keep code, autolinks, link destinations, and existing escapes
unchanged.
- Apply the same conversion to pasted and inserted markdown.
- Make the empty-editor fallback wait for editor initialization and
confirm the empty state.
- Re-arm the initial empty-change guard when the user retries the
editor.
- Add `MDE-PARSE`, `MDE-RENDER`, and `MDE-EMPTY` fallback codes.
- Correct the editor mock and add regression tests for parsing, round
trips, paste, insert, retry, and fallback behavior.

## Verification

- `npx vitest run ui/src/lib/angle-bracket-markdown.test.ts
ui/src/lib/blockquote-markdown.test.ts
ui/src/components/MarkdownEditor.test.tsx` — 106 tests passed.
- `pnpm --filter @paperclipai/ui typecheck` — passed.
- `pnpm -r typecheck` — passed.
- `pnpm build` — passed.
- `pnpm check:token-gates` — reports nine existing color findings in
`ui/src/components/onboarding/PillGuy.tsx`. This pull request does not
change that file.
- `pnpm test:run` — the local run found four existing port-allocation
failures in unrelated workspace-runtime tests. The focused editor tests
passed. The pull request CI runs these suites on clean workers.

## Risks

- Low data risk. The conversion applies only while MDXEditor processes a
value. Stored markdown keeps the clean form.
- The scanner skips code, autolinks, link destinations, and existing
escapes to prevent content changes.
- The raw-source fallback can appear about 300 ms later because it now
confirms that the editor stayed empty.
- There is no database or API change.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Original implementation: Anthropic Claude Opus, 1M context, with
agentic tool use in Claude Code.
- PR preparation and verification: OpenAI Codex with GPT-5, reasoning,
code execution, and tool use. The runtime did not expose the
context-window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Auto-generated lockfile refresh after dependencies changed on master.
This PR only updates pnpm-lock.yaml.

Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents can pause a task and ask the user structured questions.
> - The answer is durable in the issue interaction, but delivery to the
next run is not durable.
> - A process restart can therefore leave an answered interaction
without a continuation attempt.
> - Native runners also need a provider-neutral question contract before
the task page can consume native events safely.
> - This pull request adds a content-free delivery outbox and an
optional native steering seam.
> - Direct adapters keep their existing heartbeat continuation path.
> - The benefit is reliable answer delivery without changing runtime
selection or task-page behavior.

## Linked Issues or Issue Description

Refs #12202. This pull request replaces the question-delivery foundation
from that stale task-thread pull request. The task-thread projection
will follow in a smaller pull request.

**What happened?**

Question answers were stored in the issue interaction. The server then
made one in-memory continuation wake. A server stop between those
operations could leave the answer stored but not delivered. The combined
native task-thread pull request also made this behavior hard to review
separately from UI changes.

**Expected behavior**

The answer and its delivery receipt must commit in one transaction. The
server must retry pending receipts after a restart. Existing direct
adapters must keep the current wake path. A native runtime may use the
optional steering seam, but this pull request does not enable native
steering in production.

**Steps to reproduce**

1. Create an `ask_user_questions` interaction.
2. Answer the interaction.
3. Stop the server before the continuation wake completes.
4. Start the server again.
5. On current master, no durable record tells the server to retry the
answer delivery.

**Paperclip version or commit**

Current `master` at `4d82f5eae`.

## What Changed

- Add the `issue_question_response_deliveries` table and migration.
- Store only routing state, a correlation ID, and a payload digest in
the delivery row. The answer remains in the existing interaction result.
- Commit an answered interaction and its pending delivery row in one
transaction.
- Add bounded claims, retry recovery, cumulative terminal state, and
content-free activity records.
- Keep every built-in direct adapter and external adapter on the
existing heartbeat wake path.
- Add an optional native steering seam. No production caller supplies
that seam in this pull request.
- Retain the provider-neutral `paperclip.question_set.v1` presentation
on recovered interactions.
- Run delivery immediately after an answer and sweep pending rows at
startup and on the existing server interval.
- Add focused database, service, route, startup, adapter-matrix, digest,
and duplicate-delivery tests.

## Compatibility Boundary

- This pull request does not change adapter selection.
- This pull request does not start runnerd.
- This pull request does not create native run records.
- Direct adapters never call the native steering seam.
- The existing interaction result stays authoritative for answer
content.
- The migration is additive and does not rewrite existing rows.
- This pull request has no UI, dependency, workflow, package-manager, or
lockfile changes.
- The diff has 19 files.

## Verification

- `pnpm exec vitest run
server/src/__tests__/question-response-delivery.test.ts
server/src/services/issue-thread-interactions.test.ts
server/src/__tests__/issue-thread-interaction-routes.test.ts
server/src/__tests__/server-startup-feedback-export.test.ts` — 4 files
and 120 tests passed.
- `pnpm -r typecheck` — passed for all applicable workspaces. This
includes Cargo format and check, protocol drift checks, and migration
safety.
- `pnpm build` — passed. This includes the Rust release binary, server
build, and UI production build.
- `git diff --check` — passed.
- Secret patterns were not present in the changed text files.
- The repository token gates currently report violations from unchanged
files on `master`. This pull request does not change those files.

## Risks

The main risk is routing a direct-adapter answer into a native session.
The service checks the persisted runtime mode, and the adapter matrix
proves that all direct adapters use only the existing wake path. The new
table is additive. It has foreign keys, unique correlation constraints,
bounded attempts, and status checks. Activity records omit question and
answer content.

## Model Used

OpenAI Codex, GPT-5 family. The client does not expose the exact
deployment ID or context window. Agentic reasoning, tool use, and code
execution were enabled.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes:` / `Closes:`
/ `Refs:` OR (b) described the issue in-PR following the relevant issue
template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal
Paperclip ticket ID or instance-derived details
- [x] I have run the affected tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have documented the new contracts and compatibility boundary
- [x] I have considered and documented compatibility and security risks
above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Adapter utilities transfer files between the host and an agent
environment
> - A sandbox target already provides the security boundary for inbound
files
> - The generic fallback adds a temporary file and a rename that do not
add protection inside that boundary
> - This pull request writes a mode-constrained inbound file directly to
its target and applies the mode after the write
> - The benefit is a simpler transfer path while host targets keep the
strict pre-write mode rule

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The inbound file-sync fallback for a mode-constrained file stages the
file under a temporary name, applies the mode, and renames the file into
place.

**Subsystem affected**
`packages/adapter-utils` and `packages/plugins`.

**Current behavior**
A sandbox target uses a temporary path before it receives the file. The
host then changes the mode and renames the file to the target path.

**Proposed behavior**
A sandbox target receives the file at its target path. The host applies
the mode after the write. A host target still applies the mode before
the first byte.

**Reason and benefit**
The sandbox boundary already protects the target. The direct write
removes an unnecessary staging path and rename.

**Breaking changes**
None. The directory path and outbound transfer path keep their existing
behavior.

## What Changed

- Write a mode-constrained single-file inbound transfer directly to the
sandbox target.
- Apply the mode after the direct write and keep the confinement check
before post-upload commands.
- Scope the protocol comment by transfer direction and preserve the
strict host-target rule.
- Keep directory inbound transfers and outbound transfers unchanged.

## Verification

- Run the targeted unit suite for the changed package.
- Verify the suite covers direct target writes, post-write mode
application, and confinement rejection.
- Run `tsc --noEmit` for both changed packages.
- Review the full GitHub Actions check set after the PR opens.

## Risks

- A sandbox provider that assumes a temporary inbound path could expose
a behavior mismatch.
- The confinement check remains before post-upload commands, which
limits escape risk.
- Host targets keep the pre-write mode rule, so host permission behavior
does not change.

## Model Used

OpenAI GPT-5. This model assisted with Git operations, PR preparation,
review coordination, and tool use. Context window size and reasoning
mode are not exposed by the runtime.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People write almost every issue, goal, document, and chat message
through the rich markdown editor, which wraps `@mdxeditor/editor` on top
of Lexical
> - #12317 fixed a dependency split that broke that editor everywhere,
and it added `ui/src/lib/lexical-single-copy.test.ts` to keep the
Lexical graph honest
> - Greptile raised a P2 on the last assertion in that test. It compared
the range `@mdxeditor/editor` declares to a literal string, so it
rejected equivalent spellings such as `>=0.48.0 <0.49.0` even when the
resolved version satisfies them
> - A guard that fails on a healthy tree teaches people to delete it,
which would give back the protection #12317 just added
> - #12317 merged before the fix landed, so this pull request carries it
> - This pull request stops the test from reading semver ranges at all,
and asserts the mechanism that can actually break the graph
> - The benefit is a guard that fails only on a real problem, and a
smaller test file than before

## Linked Issues or Issue Description

Refs #12317 — this addresses the Greptile P2 left on that pull request.
No public issue exists, so the problem is described below with the bug
report template.

**What happened?**

`ui/src/lib/lexical-single-copy.test.ts` asserted the declared range
with an exact string comparison:

```ts
const [major, minor] = versionOf("lexical", requireFromUi).split(".");
expect(declared).toBe(`^${major}.${minor}.0`);
```

That accepts one spelling only. If `@mdxeditor/editor` published
`>=0.48.0 <0.49.0`, or `^0.48.2`, or `^0.47.0 || ^0.48.0`, the test
would fail even though the resolved Lexical version satisfies the
declared range. The test would report a dependency split that does not
exist.

**Expected behavior**

The test fails when the Lexical graph is actually split or forced. It
passes on any healthy tree, whatever range syntax the editor happens to
publish.

**Steps to reproduce**

1. Check out `master` at c8a136f.
2. Edit `ui/node_modules/@mdxeditor/editor/package.json` and change
`"lexical": "^0.48.0"` to the equivalent `"lexical": ">=0.48.0
<0.49.0"`.
3. Run `npx vitest run ui/src/lib/lexical-single-copy.test.ts`.
4. The range case fails, although 0.48.0 satisfies the range.

**Paperclip version or commit**

`master` at c8a136f. The assertion arrived with #12317.

**Relevant logs or output**

```
AssertionError: expected '>=0.48.0 <0.49.0' to be '^0.48.0' // Object.is equality
```

## What Changed

The test no longer reads semver ranges. It asserts the mechanism
instead.

- Replaced the range assertion with a check that no `lexical` or
`@lexical/*` key appears in the root `pnpm.overrides`. The failure
message names the fix.
- Removed the range assertion and the version-line comparison that stood
in for it. Both needed range syntax to mean something specific.
- Documented on the copy check why it now carries the whole guarantee.

Why this is sufficient, and stronger:

- An override is the only thing that can push a resolved version outside
the range a package declares. Remove the override and pnpm honours every
declared range by construction, so `@mdxeditor/editor` gets a Lexical it
supports. No parser needed.
- `@mdxeditor/editor` resolved its own Lexical from its own declared
range. The copy check asserts the app resolved the same version. So an
equal version proves the app sits on a line the editor supports,
transitively.
- It also closes the second half of the original bug. The old override
list could only pin the packages it named, and `@lexical/extension` was
reached transitively and never listed. A guard on "no Lexical override
at all" has no such blind spot, while a guard on "the override pins the
right version" would have.

The file is 65 lines shorter than the first attempt and no longer
carries a hand-rolled semver evaluator.

I considered two alternatives and rejected both. A `semver` dependency
in `ui` would work, but `ui` cannot resolve `semver` today and a new
direct dependency is a heavy way to buy one assertion. A larger
hand-rolled evaluator would have to cover partial versions, wildcards,
hyphen ranges, and spaced comparators before it stopped producing false
failures, which is a semver library with fewer tests.

Nothing outside this test file changes. The Lexical pins from #12317
stay as they are, and `pnpm-lock.yaml` is untouched because no manifest
changes.

## Verification

- `npx vitest run ui/src/lib/lexical-single-copy.test.ts` — 3 passed.
- `pnpm --filter @paperclipai/ui typecheck` — clean.
- Regression still caught. I restored the pre-#12317 pins, which are the
0.49.0 `pnpm.overrides` block and the 0.49.0 pins in `ui/package.json`,
ran `pnpm install`, and re-ran the guard. Two of the three cases failed,
and the new one reports the cause and the fix together:

```
AssertionError: Pin Lexical through ui/package.json instead. An override cannot
cover the packages it does not name, and it hides the range @mdxeditor/editor
declares.: expected [ 'lexical', …(9) ] to deeply equal []

AssertionError: expected '0.48.0' to be '0.49.0'
```

I then restored the current pins and re-ran the guard, which passed 3 of
3.
- `npx vitest run ui/src/lib/mention-deletion.test.ts
ui/src/lib/mention-aware-link-node.test.ts
ui/src/components/MarkdownEditor.test.tsx` — 42 passed, unchanged.

## Risks

- Low risk. The change touches one test file and no application code.
- The guard now forbids a legitimate tool. If someone later has a real
need for a Lexical `pnpm.overrides` entry, this test blocks it. That is
deliberate: an override is what broke the editor, and the block is one
line to remove with a reviewer looking at it. The failure message states
the supported alternative, which is pinning in `ui/package.json`.
- The guard no longer checks the declared range directly. It relies on
pnpm honouring declared ranges when no override is present, which is the
package manager's contract and the same property `pnpm install
--frozen-lockfile` depends on in CI.
- The ordering #12317 set out still holds: a Lexical upgrade must wait
for an `@mdxeditor/editor` release that supports it. Bumping `ui` past
the editor's range now produces two copies, which the copy checks fail
on.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

Claude (Anthropic), Claude Opus, agentic tool use via Claude Code.

- Provider and model: Claude (Anthropic), Claude Opus
- Capability: agentic tool use through Claude Code, with repository
search, file edits, dependency installation, test and build runs

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes (the
reasoning lives in comments in the test file; no document states a
Lexical version or pinning policy)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
…spaces (#12288)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Humans oversee those agents in teams, so each person has a login and
a profile with an avatar
> - Avatars, logos and pasted images all go to one asset upload API,
which files each object under a namespace
> - The avatar namespace embeds the user id, and a deployment can take
user ids from an external identity layer, where a subject often holds
":", "|", "." or "@"
> - But the namespace validator accepted only letters, numbers, "/", "_"
and "-", so those users got a 400 "Invalid image metadata" error and
could not set a profile photo
> - This pull request widens the accepted characters, rejects "." and
".." path segments with a clear message, and cleans the namespace in the
upload client
> - The benefit is that profile photo upload works for every user, and a
namespace the API refuses now returns a message that says what is wrong

## Linked Issues or Issue Description

No existing issue or open pull request covers this. I searched the issue
and pull request lists for "avatar upload", "profile photo", "Invalid
image metadata" and "asset namespace" and found no duplicate. The bug
report follows.

**What happened?**

Profile photo upload fails. `ui/src/pages/ProfileSettings.tsx` sends the
namespace `profiles/${user.id}` to `POST
/api/companies/:companyId/assets/images`. When the user id comes from an
external identity layer it can contain ":", "|", "." or "@" — for
example `oidc:example|jane.example@example.com`.
`createAssetImageMetadataSchema` in
`packages/shared/src/validators/asset.ts` accepted only
`/^[a-zA-Z0-9\/_-]+$/`, so the route returned 400 "Invalid image
metadata" (`server/src/routes/assets.ts`). The image bytes were never
the problem, but the message pointed at the image, so the toast gave the
user nothing to act on.

A second case has the same cause. The agent instructions editor in
`ui/src/pages/AgentDetail.tsx` builds a namespace that ends with a
filename, such as `agents/<id>/instructions/SKILL.md`. The "." in the
filename also failed the check.

**Expected behavior**

A profile photo uploads for any user id the app itself issues, and an
image pasted into the agent instructions editor uploads for any
instruction filename. A namespace the API does refuse returns a message
that names the field and states the rule.

**Steps to reproduce**

1. Run Paperclip with an external identity provider, so `user.id` holds
an OIDC subject such as `oidc:example|jane.example@example.com`.
2. Open Settings, then Profile.
3. Choose an avatar image.
4. The upload fails and the page shows "Invalid image metadata".

Or, with no identity provider:

1. Open an agent, then the instructions editor, and select a file whose
name contains a "." such as `SKILL.md`.
2. Paste an image into the editor.
3. The upload fails with the same error.

**Paperclip version or commit**

`master` at eb86fcd.

**Deployment mode**

Any deployment whose user ids come from an external identity layer. The
instructions-editor case reproduces on a plain self-hosted install too.

**Agent adapter(s) involved**

Not adapter-specific (core bug).

## What Changed

- `packages/shared/src/validators/asset.ts`: widen the namespace pattern
to `/^[a-zA-Z0-9\/_.:@|-]+$/`, and reject any "/"-separated segment
equal to "." or "..". A traversal attempt now gets a clean 400 from the
validator instead of an error from the storage provider.
- `packages/shared/src/validators/asset.ts`: add
`sanitizeAssetNamespace()`, which maps any string to a namespace the
schema accepts. It works per segment: it keeps the accepted characters,
turns the others into "-", collapses repeated dashes, drops empty and
dot-only segments, and caps the result at 120 characters. It returns
`undefined` when no segment survives, and the caller then sends no
namespace.
- `packages/shared/src/validators/asset.ts`: export
`ASSET_NAMESPACE_MAX_LENGTH` and `ASSET_NAMESPACE_RULE`, so the rule
text and the API error cannot drift apart.
- `ui/src/api/assets.ts`: run the namespace through
`sanitizeAssetNamespace()` in `uploadImage`. This is one choke point for
all callers, so no caller has to know the rule.
- `server/src/routes/assets.ts`: name the field in the 400 message —
`Invalid image metadata: "namespace" must be 1-120 characters of
letters, numbers, or / _ - . : @ |, and cannot contain "." or ".." path
segments`. The zod issue details stay in the response. The UI shows
`body.error`, so the toast is now actionable.
- Tests: a new `packages/shared/src/validators/asset.test.ts`
accept/reject matrix for the schema and the sanitizer; three cases in
`server/src/__tests__/assets.test.ts`; one case in
`ui/src/pages/ProfileSettings.test.tsx`.

## Verification

Targeted runs:

```
npx vitest run packages/shared/src/validators/asset.test.ts        # 22 passed
npx vitest run server/src/__tests__/assets.test.ts                 # 11 passed
npx vitest run ui/src/pages/ProfileSettings.test.tsx               # 2 passed
```

New cases:

- Schema: accepts identity-provider ids that hold ":", "|", "." and "@";
accepts `agents/<id>/instructions/SKILL.md`; rejects `profiles/bad
name!`, over-length input, and `.` or `..` segments.
- Sanitizer: passes identity-provider ids through unchanged, replaces
and collapses the other characters, drops the `.` and `..` segments
while keeping a segment of three or more dots, caps at 120 characters
without leaving a dot segment behind at the cut, and returns `undefined`
when nothing survives. One case asserts the sanitizer output always
parses.
- Route: 201 for `profiles/oidc:example|jane.example@example.com`, and
the storage service receives that namespace; 400 naming `namespace` for
`profiles/bad name!`; 400 for `profiles/../secrets`.
- UI: a session user id holding ":" and "|" uploads, and the namespace
reaches the API unchanged.

Typecheck:

```
pnpm --filter @paperclipai/shared typecheck   # clean
pnpm --filter @paperclipai/ui typecheck       # clean
cd server && npx tsc --noEmit -p tsconfig.json # clean
```

Package suites:

```
npx vitest run --project @paperclipai/shared --exclude "**/dist/**"   # 586 passed, 8 pre-existing failures in src/worktree-seed-source.test.ts
npx vitest run --project @paperclipai/ui --exclude "**/dist/**"       # 4402 passed
```

CI runs the server suite as ten shards (five general, five serialized),
which is the authoritative full run for this package. All shards pass on
this branch.

The `worktree-seed-source` failures reproduce on an unmodified checkout
of the same base commit and are unrelated to this change. The UI
failures seen in that run were 5-second test timeouts caused by running
two suites at once on one machine; each file passes when it runs alone.

No document states the namespace character rule — I checked `docs/` and
`doc/`, where the asset upload endpoint appears only in an OpenAPI
registry entry and a smoke-lab note, neither of which describes the
metadata fields. The rule now lives in one exported constant that the
API error reuses.

## Risks

Low risk.

- The wider character set does not widen what a caller can write to
disk. `server/src/storage/service.ts` already replaces every character
outside `[a-zA-Z0-9._-]` in each path segment, and
`server/src/storage/local-disk-provider.ts` already rejects "." and ".."
segments and any key that resolves outside the base directory. This
change moves the "." and ".." refusal earlier, to the validator, so the
caller gets a clear 400.
- The API is more permissive than before, so no request that used to
succeed can start failing.
- Namespaces stored before this change keep working. The namespace is
not a key that is looked up; it is a prefix under which new objects are
filed.
- One behavior change worth noting: the UI now cleans a namespace
instead of sending it as typed, so a caller that passes an unusable
namespace gets a cleaned prefix rather than a failed upload.

## Model Used

- Claude (Anthropic), Claude Opus, 1M context window, extended thinking,
agentic tool use through Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Hosting operators (a managed cloud, an internal shared server) tune
the settings surface with `PAPERCLIP_HIDDEN_SETTINGS`, which today hides
whole pages
> - The Secrets page bundles four tabs, and two of them — Provider
vaults and Proposals — do not apply to deployments where the operator
provisions provider credentials itself
> - Hiding the whole Secrets page is too coarse: the Secrets and My
secrets tabs stay essential everywhere
> - This pull request adds per-tab visibility keys
(`company.secrets.vaults`, `company.secrets.proposals`) as a new
company-section registry group
> - The benefit is that any hosting operator can trim the Secrets page
to what fits their deployment, with self-hosted behavior unchanged by
default

## Linked Issues or Issue Description

No public issue exists; following the enhancement template:

**What existing behavior does this improve?**
`PAPERCLIP_HIDDEN_SETTINGS` can hide the whole Secrets page
(`company.secrets`) but not individual tabs. Operators of managed
deployments need to hide the Provider vaults and Proposals tabs while
keeping the rest of the page.

**Subsystem affected**
Settings visibility (`packages/shared/src/settings-visibility.ts`) and
the Secrets page UI (`ui/src/pages/Secrets.tsx`).

**Current behavior**
The Secrets page always renders all four tabs (Secrets, My secrets,
Provider vaults, Proposals), polls pending proposals for the badge, and
offers "manage vaults" affordances that jump to the vaults tab.

**Proposed behavior**
Two new registry keys, `company.secrets.vaults` and
`company.secrets.proposals`, hide the corresponding tab: the tab-bar
entry disappears, an active hidden tab snaps back to Secrets, the
manage-vaults affordances are suppressed, and the pending-proposals poll
stops. UI visibility only — the provider-config and proposal APIs stay
live for agents and integrations, matching the existing `company.*`
precedent. Nothing changes when the variable is unset.

**Reason and benefit**
Any hosting operator (a managed cloud, an internal shared server) can
trim the Secrets page to what fits their deployment — for example when
the operator provisions provider credentials itself, so the vault and
proposal flows do not apply — without losing the Secrets and My secrets
tabs, which stay essential everywhere.

**Breaking changes**
None. With `PAPERCLIP_HIDDEN_SETTINGS` unset (or set to existing keys
only) nothing changes; older app versions receiving the new keys ignore
them with a warning by design.

## What Changed

- `packages/shared/src/settings-visibility.ts`: new
`HIDEABLE_COMPANY_SECTIONS` group (`company.secrets.vaults`,
`company.secrets.proposals`), `HideableCompanySection` type,
`hidesCompanySection()` helper, wired into `HideableSettingKey` /
`HIDEABLE_SETTING_KEYS`, re-exported from the package index.
- `ui/src/pages/Secrets.tsx`: tab-bar filtering, hidden-tab snap-back
effect, gated pending-proposals query, conditional `onManageVaults` on
both the import button and dialog (the button's "AWS vault disabled —
manage" affordance renders nothing when vaults are hidden), hidden
`TabsContent` blocks.
- Docs: new bullet in `docs/deploy/environment-variables.md` under
"Hiding settings surfaces".
- Tests: registry membership/parse cases in
`settings-visibility.test.ts`; new render cases in
`Secrets.render.test.tsx` (hidden tabs absent + proposals poll skipped;
default render keeps both tabs and the poll).

## Verification

- `npx vitest run packages/shared/src/settings-visibility.test.ts
ui/src/pages/Secrets.render.test.tsx` — 43 tests passing.
- `pnpm --filter @paperclipai/shared typecheck` and `pnpm --filter
@paperclipai/ui typecheck` — clean.

## Risks

- Low. Nothing changes with `PAPERCLIP_HIDDEN_SETTINGS` unset (covered
by the default-render test). The keys are UI-visibility only, so
agent/integration API access is unaffected. Older app versions receiving
the new keys ignore them with a warning by design.

## Model Used

Claude (Anthropic), model id `claude-fable-5`, extended thinking,
agentic tool use via Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Hosting operators (a managed cloud, an internal shared server) tune
the settings surface with `PAPERCLIP_HIDDEN_SETTINGS`, but hiding a
control never changes its value
> - An instance whose stored feedback-sharing preference is still the
schema default ("prompt") keeps prompting users even when the operator
hid the control, leaving them no way to answer
> - More generally, operators have no supported way to change what a
setting defaults to without patching code
> - This pull request adds `PAPERCLIP_SETTING_DEFAULTS`, a generic
operator-supplied read-time default overlay for registry-listed general
settings
> - The benefit is that any hosting operator can pair "hide the control"
with "default the value", while explicit user choices and self-hosted
stock behavior stay untouched

## Linked Issues or Issue Description

No public issue exists; following the enhancement template:

**What existing behavior does this improve?**
Hosting operators need to supply the default value of selected instance
settings (first: `feedbackDataSharingPreference`) via configuration,
without patching code and without a hard-coded, opinionated constant in
the product.

**Subsystem affected**
Server (instance-settings service, feedback service, boot) and
`packages/shared` (settings schemas).

**Current behavior**
Setting defaults are fixed in the shared zod schemas.
`PAPERCLIP_HIDDEN_SETTINGS` can hide the feedback-sharing control and
floor writes, but the stored value stays "prompt", so issue-chat
surfaces keep prompting with no way to answer.

**Proposed behavior**
`PAPERCLIP_SETTING_DEFAULTS` takes a JSON object validated against a
shared registry of defaultable fields. The operator value substitutes
for the schema default at read time: a field whose effective value is
still the schema default resolves to the operator value; an explicit
non-default user choice always wins. Never persisted; unsetting the
variable restores stock behavior. Malformed JSON or an invalid value for
a known field refuses startup (fail closed); unknown field names warn
and are ignored (mixed-version fleet safe).

**Reason and benefit**
Any hosting operator can pair "hide the control" with "default the
value" without forking the product. Explicit user choices and
self-hosted stock behavior stay untouched.

**Breaking changes**
None. With the variable unset, every read path is byte-identical to
before.

## What Changed

- New `packages/shared/src/setting-defaults.ts`:
`SETTING_DEFAULTS_ENV_KEY`, `DEFAULTABLE_GENERAL_SETTINGS` registry
(currently `feedbackDataSharingPreference`), `parseSettingDefaults`
(fail-closed for policy content, warn-ignore unknown fields),
`applyOperatorGeneralDefaults` (pure read-time overlay),
`stripOperatorGeneralEchoes` (persist-time echo strip, see below),
re-exported from the package index.
- New `server/src/services/setting-defaults.ts`: parse-once accessor
mirroring `settings-visibility.ts`.
- `server/src/services/instance-settings.ts`: `toGeneralView` applies
the overlay in `get`/`getGeneral`/update responses; persisted writes
never carry operator values. Because general-settings writes materialize
every field, a stored schema-default value is treated as unchosen —
deliberate, documented, and covered by tests.
- `server/src/services/feedback.ts`: the preference-persistence branch
now checks the effective (overlaid) preference, so a stray prompt answer
cannot overwrite an operator default; its local normalize fallback now
returns full schema defaults.
- `server/src/index.ts`: boot-time fail-fast parse with a log line
naming the defaulted settings, mirroring the managed-config posture.
- The hidden-settings write floor (`assertNoHiddenSettingChanges`) keeps
comparing against effective values, so clients echoing a full GET
response keep working. To keep the overlay strictly read-time,
`updateGeneral` strips such echoes at persist time: a write of the
operator value over a field whose stored value is still the schema
default (unchosen) maps back to the schema default, so an echo cannot
promote the operator value into an explicit stored choice and later
changes to (or removal of) `PAPERCLIP_SETTING_DEFAULTS` still take
effect. A write of any other value, or over an explicit stored choice,
persists as given.
- Docs: `PAPERCLIP_SETTING_DEFAULTS` row + "Operator setting defaults"
section in `docs/deploy/environment-variables.md`.
- Tests: `packages/shared/src/setting-defaults.test.ts` (parse matrix,
overlay precedence, echo-strip matrix, immutability) and
`server/src/__tests__/instance-settings-operator-defaults.test.ts`
(accessor, substitution, explicit-choice wins, unset identity,
never-persisted, full-GET echo stays unchosen, explicit non-default
write persists).

## Verification

- `npx vitest run packages/shared/src/setting-defaults.test.ts
server/src/__tests__/instance-settings-operator-defaults.test.ts
server/src/__tests__/instance-settings-managed-overlay.test.ts` — 33
tests passing.
- `npx vitest run server/src/__tests__/instance-settings-routes.test.ts
server/src/__tests__/instance-settings-service.test.ts` — 57 passing;
`npx vitest run server/src/__tests__/feedback-service.test.ts
server/src/__tests__/issue-feedback-routes.test.ts` — 18 passing.
- `pnpm --filter @paperclipai/shared typecheck` and `pnpm --filter
@paperclipai/server typecheck` — clean.

## Risks

- Low. With the variable unset every read path is byte-identical to
before (identity overlay, covered by tests). The overlay is read-time
only and never persisted, so no migration and no data risk. Fail-closed
parsing means a bad policy value is a loud boot failure rather than
silent drift — consistent with the existing managed-config contract.

## Model Used

Claude (Anthropic), model id `claude-fable-5`, extended thinking,
agentic tool use via Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Instance settings collect deployment-wide controls; one of them was
the Heartbeats page, an instance-wide list of scheduler heartbeat agents
with enable/disable toggles
> - The same controls live on each agent's own configuration surface, so
the standalone list duplicates them, and its framing no longer matches
how heartbeat agents are managed
> - Keeping a settings view that no longer makes sense costs every
deployment navigation noise and maintenance
> - This pull request removes the page, its route, its navigation
entries, and its hidden-settings key for all deployments
> - The benefit is a smaller, coherent settings surface, with operator
hidden-settings lists that still mention the retired key continuing to
work unchanged

## Linked Issues or Issue Description

No public issue exists; describing the issue inline per the enhancement
template:

**What existing behavior does this improve?**
The instance settings surface — specifically the Settings → Heartbeats
page, which listed scheduler heartbeat agents instance-wide with
enable/disable toggles. The view no longer makes sense as a standalone
settings page: the same controls are available on each agent's
configuration surface, and the instance-wide list framing does not match
how heartbeat agents are managed.

**Subsystem affected**
Cross-cutting: `ui/` (page, route, navigation), `packages/shared`
(settings-visibility registry), docs.

**Current behavior**
The page renders at `/company/settings/instance/heartbeats`, appears in
the settings sidebar and tab bar, and is hideable by hosting operators
via the `instance.heartbeats` key of `PAPERCLIP_HIDDEN_SETTINGS`.

**Proposed behavior**
The page, route, and navigation entries are removed for every
deployment. The `instance.heartbeats` registry key is retired; operator
lists that still send it are logged and ignored, so mixed-version fleets
keep working. Remembered settings paths pointing at the old page remap
to the settings root. Heartbeat APIs are unchanged.

**Reason and benefit**
A smaller, coherent settings surface with no duplicated controls; less
navigation noise and maintenance for every deployment.

**Breaking changes**
None functional. Bookmarks and remembered paths to the removed page land
on the settings root; `PAPERCLIP_HIDDEN_SETTINGS` lists that still
include `instance.heartbeats` log a warning and are otherwise honored
unchanged.

## What Changed

- Deleted `ui/src/pages/InstanceSettings.tsx` (the Heartbeats view) and
its route in `ui/src/App.tsx`.
- Removed the sidebar entry (`CompanySettingsSidebar`) and tab-bar item
(`CompanySettingsNav`).
- Removed `"/heartbeats"` from the remembered-settings-path allowlist;
remembered heartbeats paths now remap to the settings root.
- Retired the `instance.heartbeats` key from the shared
settings-visibility registry and the environment-variables doc;
documented that retired keys are ignored with a warning.
- Dropped the now-unused UI client wrapper for the instance
scheduler-agent list (`heartbeatsApi.listInstanceSchedulerAgents`); the
server endpoint stays.
- Removed the unused `schedulerHeartbeats` query key.

## Verification

- `npx vitest run packages/shared/src/settings-visibility.test.ts
ui/src/lib/instance-settings.test.ts
ui/src/components/CompanySettingsSidebar.test.tsx
ui/src/components/access/CompanySettingsNav.test.tsx` — 24 tests
passing.
- Full `ui` vitest suite: 4426 tests, 4 failures — all in files this PR
does not touch; 3 were load-induced timeouts that pass on rerun, and
`OnboardingWizard.test.tsx` "renders instead of throwing when the
browser denies storage access" fails identically on a clean master
checkout (pre-existing).
- `pnpm --filter @paperclipai/ui typecheck` and `pnpm --filter
@paperclipai/shared typecheck` — clean.
- Merged `master` to clear a conflict (see below) and re-ran the four
focused suites (24 passing), `ui/src/App.test.tsx` and
`ui/src/plugins/bridge.test.ts` (22 passing), and both typechecks — all
clean. Full CI is green on the merge commit.

## Merge With master

`master` gained the `company` → `organization` copy pass (#12243), which
reworded strings inside `ui/src/pages/InstanceSettings.tsx` — the page
this branch deletes — producing a modify/delete conflict. Resolved by
keeping the deletion: the page is going away, so the rewording of its
copy has nothing to apply to. Every other file merged cleanly, and
`master`'s rewording in `App.tsx`, `App.test.tsx`, and
`CompanySettingsSidebar.tsx` sits away from this branch's structural
removals, so both changes survive. The net diff against `master` is
unchanged from the pre-merge review: the same 13 files, 23 insertions,
330 deletions.

## Risks

- Low. Pure removal of a UI surface; heartbeat data and APIs are
untouched. Operators still listing `instance.heartbeats` in
`PAPERCLIP_HIDDEN_SETTINGS` get a warning log and otherwise unchanged
behavior (covered by the registry's unknown-key handling). Bookmarks and
remembered paths to the old page land on the settings root.

## Model Used

Claude (Anthropic), model id `claude-fable-5`, extended thinking,
agentic tool use via Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Company settings include member management and invites, which are
one workflow: invite someone, watch the join request, manage the
membership
> - Today invites sit on a standalone settings page, costing a sidebar
entry and forcing people to bounce between two pages for one task
> - Merging invites into the Members page keeps the workflow in one
place without losing any capability
> - This pull request turns the Members page into a Members/Invites tab
bar, redirects the old URL, and keeps both operator-visibility keys
meaningful
> - The benefit is a tighter settings surface for every deployment, with
`company.invites` now hiding just the tab while `company.members` hides
the whole page

## Linked Issues or Issue Description

No public issue exists; describing the issue inline per the enhancement
template:

**What existing behavior does this improve?**
Company invites live on a standalone settings page separate from the
Members page, even though inviting someone and managing the resulting
membership are one workflow.

**Subsystem affected**
Web UI (company settings)

**Current behavior**
`/company/settings/invites` is its own page with its own sidebar entry
(gated by `company.invites`); the Members page (`company.members`) is
separate. The sidebar company menu's "Invite people" shortcut links to
the invites page and is not gated by the hidden-settings mechanism at
all.

**Proposed behavior**
The Members page carries a Members/Invites tab bar, addressable via
`?tab=invites`. The invite creation flow, latest-link panel, and invite
history move unchanged into an `InvitesSection` component. The old URL
redirects to the tab (still behind its `HiddenSettingsPageGate`).
`company.members` hides the whole page; `company.invites` hides just the
Invites tab, and the tab bar collapses when only Members remains. The
"Invite people" shortcut points at the tab and hides when either surface
is operator-hidden.

**Reason and benefit**
One settings surface for one workflow: fewer sidebar entries and no
bouncing between two pages to invite someone and then manage the
membership. Operators keep the same visibility controls, with a sharper
meaning for each key.

**Breaking changes**
None. Bookmarks to the old invites URL redirect to the tab, invite and
membership APIs are unchanged, and both hidden-settings keys keep
working.

## What Changed

- `ui/src/pages/CompanyInvites.tsx` →
`ui/src/components/access/InvitesSection.tsx` (page chrome and
breadcrumbs dropped; content unchanged), with its tests moved alongside.
- `ui/src/pages/CompanyAccess.tsx`: Members/Invites tabs via the shared
`PageTabBar`, `?tab=invites` search param, `company.invites` gating with
tab snap-back; legacy "Open Invites" button retargeted.
- `ui/src/App.tsx`: the invites route becomes a gated redirect to
`/company/settings/members?tab=invites`.
- `CompanySettingsSidebar` / `CompanySettingsNav`: standalone Invites
entry/tab removed; the old path maps to the members tab.
- `ui/src/components/SidebarCompanyMenu.tsx`: "Invite people" now links
to the tab and hides when `company.members` or `company.invites` is
hidden (closes an existing gating gap).
- Tests: moved invites tests, new tab coverage (default tab, deep link,
operator-hidden tab skips the invites fetch), sidebar/nav suites
updated.

## Verification

- `npx vitest run ui/src/components/CompanySettingsSidebar.test.tsx
ui/src/components/access/CompanySettingsNav.test.tsx
ui/src/pages/CompanyAccess.test.tsx
ui/src/components/access/InvitesSection.test.tsx` — 24 tests passing.
- `pnpm --filter @paperclipai/ui typecheck` — clean.

## Risks

- Low. Pure UI restructure: invite APIs, membership APIs, and the
hidden-settings registry keys are unchanged. Bookmarks to the old
invites URL redirect (and stay gated). The `company.invites` key's
meaning narrows from "hide the page" to "hide the tab", which is the
same effective surface.

## Model Used

Claude (Anthropic), model id `claude-fable-5`, extended thinking,
agentic tool use via Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
…me and follow renames (#12292)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Every company has an issue prefix. It is the visible half of each
task and case identifier, and a self-hosted company derives it from the
name it was created with
> - A hosted or managed instance does not use the create-company flow.
The trusted-header auth path claims the tenant company instead
> - That path minted the prefix from a hash of the stack id, and it
wrote a placeholder description that nobody chose
> - So a hosted company showed opaque task IDs such as `PC7F2A-14`, and
a rename never changed them
> - This pull request derives the prefix from the company name on that
path too. It re-derives the prefix when the name changes on a managed
instance, and it rewrites the stored issue and case identifiers so
existing tasks follow the rename
> - It also repairs each company that an earlier build claimed. The
repair runs once, on the next authenticated request
> - The benefit is that task IDs on a hosted instance read like the ones
on a self-hosted instance, and they stay correct after a rename

## Linked Issues or Issue Description

No public issue exists. The description below follows
`.github/ISSUE_TEMPLATE/enhancement.yml`.

**What existing behavior does this improve?**

The tenant company claim in `resolveCloudTenantActor`
(`server/src/middleware/auth.ts`) and the company update in
`companyService.update` (`server/src/services/companies.ts`). Both
decide the
`issue_prefix` and the `description` of a company on a hosted or managed
instance.

**Subsystem affected**

`server/` — REST API and orchestration services. One small hint was also
added
in `ui/`.

**Current behavior**

A self-hosted company gets its issue prefix from its name. "Acme
Robotics"
becomes `ACM`, and its tasks read `ACM-14`.

A hosted or managed instance claims the company through the
trusted-header auth
path. That path wrote a different prefix: `"PC"` plus the first four hex
characters of the SHA-256 of the stack id. The same path also wrote a
placeholder description, `"Provisioned by ... for stack <stack id>."`.

The result is a task ID such as `PC7F2A-14`. It says nothing about the
company.
A later rename of the company does not change it, because nothing
re-derives
the prefix after creation.

**Proposed behavior**

The claim path derives the prefix from the company name, exactly as the
create-company flow does. It writes no description.

On a managed instance, a rename re-derives the prefix. The stored issue
and
case identifiers move with it, so `ACM-14` becomes `NOR-14` when "Acme
Robotics" becomes "Northwind Traders". A rename that keeps the same
three-letter
base keeps the current prefix, including any disambiguating suffix.

A self-hosted instance is unchanged. A rename there still keeps the
prefix the
company was created with.

Companies that an earlier build already claimed get a one-time repair on
their
next authenticated request. The repair re-derives the prefix from the
current
name, re-keys the identifiers, and clears the placeholder description.

**Reason and benefit**

A task ID is the primary handle for a task. People type it, paste it
into chat,
and read it in a URL. On a hosted instance that handle was an opaque
hash, and
it disagreed with the company name that the same user chose during
signup. The
name is the only prefix source a hosted user ever supplies, so the
prefix now
follows it.

**Breaking changes**

Yes, on hosted and managed instances only. A company rename now rewrites
the
stored issue and case identifiers. Links that carry an old identifier
stop
resolving after the rename. The company settings page states this before
the
user saves. The one-time repair applies the same rewrite once to
companies that
carry the old hash prefix. Self-hosted behavior does not change.

## What Changed

- Added `server/src/services/issue-prefix.ts`. It holds the prefix
helpers that
used to live inside the `companyService` closure:
`ISSUE_PREFIX_FALLBACK`,
  `deriveIssuePrefixBase`, `issuePrefixSuffixForAttempt`, and
  `isIssuePrefixConflict`. The companies service now imports them.
- Added `pickAvailableIssuePrefix` to that module. It reads the prefixes
in one
base family and returns the first free candidate. A standalone `INSERT`
can
retry on a unique violation, because each failed statement is its own
implicit
transaction. A caller that already holds a transaction cannot, because
the
violation aborts the whole transaction. Such a caller picks first, then
writes.
- Added `rekeyCompanyIssueIdentifiers` to that module. It rewrites the
prefix of
the stored `issues.identifier` and `cases.identifier` values of one
company in
  the caller's transaction, and it returns the two row counts.
- `companyService.update` re-derives the prefix when the name changes on
a
managed instance, re-keys both tables in the same transaction, and
writes a
  `company.updated` activity entry after the commit.
- `resolveCloudTenantActor` claims the company with a name-derived
prefix and a
null description. The claim retries with the next suffix when the prefix
is
  taken.
- `resolveCloudTenantActor` also runs a one-time repair for companies
that carry
the old hash prefix. An exact-match fence on the update lets a
concurrent
rename win. The repair is idempotent, because its guards stop matching
after
  it lands.
- The rename takes a row lock on the company before it compares anything
against
it, and it re-keys from the prefix it reads under that lock. Only patch
and
environment facts gate the lock, so no stale read can steer the
decision. Two
overlapping updates would otherwise leave a company whose prefix
disagrees with
its own identifiers, in either direction: two renames, where the second
re-keys
from a prefix the first already moved; or a rename plus a stale form
that
resubmits the original name, where the second sees an unchanged name,
skips
re-derivation, and restores the old name on top of the first rename's
prefix.
Only a managed instance takes the lock, and only for an update that
carries a
  name.
- Both helpers compare an exact identifier head instead of a LIKE
pattern. A
  stored prefix is data, so it must never be read as a pattern.
- The company settings page shows a hint under the name field on a
managed
  instance: renaming can change the task ID prefix.

## Verification

Automated tests:

```
pnpm --filter @paperclipai/server exec vitest run \
  src/services/issue-prefix.test.ts \
  src/__tests__/companies-service.test.ts \
  src/__tests__/cloud-tenant-company-provisioning.test.ts \
  src/middleware/cloud-tenant-actor.test.ts \
  src/__tests__/auth-session-route.test.ts \
  src/__tests__/cloud-routes.test.ts \
  src/__tests__/cloud-instance.test.ts \
  src/__tests__/company-branding-route.test.ts \
  src/__tests__/company-cloud-floor.test.ts \
  src/__tests__/companies-route-cross-company-authz.test.ts \
  src/__tests__/companies-route-path-guard.test.ts \
  src/__tests__/company-portability.test.ts
pnpm --filter @paperclipai/ui exec vitest run
pnpm --filter @paperclipai/ui typecheck
```

New coverage:

- `server/src/services/issue-prefix.test.ts` covers the derivation, the
suffix
  ladder, the cause-chain walk of the unique-violation detector, and
  `pickAvailableIssuePrefix` against a stubbed select.
- `server/src/__tests__/companies-service.test.ts` covers a managed
rename
against a real Postgres database: the prefix moves, both identifier
tables are
re-keyed, and the activity entry is written. It also covers a same-base
rename,
  a collision that takes the suffixed candidate, a non-name patch, and a
self-hosted rename that leaves the prefix alone. Two more tests drive
the
overlap cases: two concurrent renames of the same company, and a rename
racing
a stale form that resubmits the original name. Both assert that the
surviving
name's base matches the company prefix and that the stored identifiers
sit on
  that prefix.
- `server/src/__tests__/cloud-tenant-company-provisioning.test.ts`
covers the
claim path and the repair against a real Postgres database: a
name-derived
prefix, a null description, a suffixed prefix on collision, the full
repair,
  a second pass that changes nothing, a description-only repair, and an
  operator-written description that the repair leaves alone.
- `ui/src/pages/CompanySettingsRenameHint.test.tsx` covers the hint on a
managed
  instance and its absence on a self-hosted instance.

The `substring` cast in `rekeyCompanyIssueIdentifiers` is load-bearing
and the
database tests prove it. The driver binds the offset as text. Without
the
`::int` cast Postgres resolves the SQL-regex overload of `substring`,
and every
identifier becomes NULL.

## Risks

- **Re-keying changes existing identifiers and URLs.** This is
deliberate, and
it happens on hosted and managed instances only. After a rename, a link
that
carries an old task identifier stops resolving. The settings page warns
about
  this before the user saves.
- **Identifiers inside comment text are not rewritten.** Only the
`identifier`
columns of `issues` and `cases` move. A task ID that someone typed into
a
  comment, a description, or a document keeps the old prefix.
- **A lost prefix race inside the rename transaction surfaces as a
conflict.**
The rename picks a free prefix and then writes, because a unique
violation
inside a transaction aborts the whole transaction. Two *different*
companies
renamed onto the same base at the same moment can still collide. The
loser
sees its PATCH fail with the unique violation. The write is retryable by
the
client, and the window is a single statement wide. Two renames of the
*same*
company no longer race: the row lock serializes them, and the second one
  re-keys from what the first committed.
- **The rename holds a row lock.** A managed rename takes `SELECT ...
FOR UPDATE`
on its own company row for the rest of the transaction. It is one row,
and no
other path in the transaction locks a company row, so there is no
lock-order
cycle. A self-hosted instance and every non-rename company update never
reach
  the lock.
- **The one-time repair is best effort.** It runs inside a try/catch and
logs a
warning on failure, so it never blocks authentication. A failed pass is
retried
  on the next request, because its guards still match.
- No schema change and no migration.

## Model Used

- Provider: Anthropic (Claude)
- Model: Claude Opus, model id `claude-opus-5[1m]`
- Context window: 1M
- Reasoning mode: extended thinking
- Capabilities used: agentic tool use through Claude Code (file edits,
shell,
  test runs against an embedded Postgres database)

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
…mode (#12293)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - An instance can turn on the `enableManagedSandboxOnly` feature,
which hides the local environment and runs every agent in the
platform-managed environment
> - That feature already gated the environment pickers, the onboarding
wizard, and the server-side run selection, but many other screens still
showed absolute paths on the execution host and still let the user pick
an execution engine
> - On such an instance those controls name a filesystem the user cannot
reach; a path written there is stored and then ignored, which reads as a
broken control
> - This pull request hides the remaining host-path and execution-engine
surfaces behind the same feature, adds a server rule that refuses a
project-workspace path write while the feature is on, and closes a
related route gap in the isolated-workspace pages
> - The benefit is that a managed instance shows no host path and no
folder picker anywhere, and a write that carries a path now fails with a
clear message instead of being silently discarded

## Linked Issues or Issue Description

No public issue exists. The description below follows
`.github/ISSUE_TEMPLATE/enhancement.yml`.

**What existing behavior does this improve?**

The `enableManagedSandboxOnly` instance feature, and the UI surfaces
that show
a host filesystem path: project properties, the new-project dialog, the
project
workspace and execution workspace detail pages, the workspace and task
cards,
plugin local folders, and the agent configuration form with its
per-adapter
fields. It also improves route gating for `enableIsolatedWorkspaces`.

**Subsystem affected**

Cross-cutting (`ui/` and `server/`).

**Current behavior**

When `enableManagedSandboxOnly` is on, the local environment disappears
from the
environment pickers and the server refuses to run an agent on the local
host.
Everything else stays visible. A user still sees:

- the project "Local folder" row, its absolute path, and the
Set/Change/Clear buttons
- the "Local folder" field and its "Choose" folder picker in the
new-project dialog
- the "Local path" field and fact row on a project workspace
- the "Paths" and "Lifecycle commands" groups on an execution workspace
- the working directory on workspace cards, task properties, and runtime
service rows
- the plugin "Local folders" section
- "Working directory (deprecated)", "Command", "Execution engine",
"ACP server command", "ACP state directory", and "Agent instructions
file" in
  the agent configuration form

A path typed into any of these names a filesystem no agent on the
instance uses.
The project workspace API also accepts a `cwd` write and stores it.

Separately, `/workspaces`, `/execution-workspaces/*`, and
`/projects/:projectId/workspaces/:workspaceId` render for anyone who
types or
bookmarks the URL, even with `enableIsolatedWorkspaces` off. Only the
sidebar
entry reads that flag.

**Proposed behavior**

With `enableManagedSandboxOnly` on, none of those surfaces render. A
project
whose codebase came from a managed checkout keeps its one-line
"Paperclip-managed folder." label and shows no path. The non-path
controls stay:
repo URL, branch, service URL, port, command output, ACP session mode,
ACP
non-interactive permissions, Codex fast mode, and the sandbox toggles.

The project-workspace create and patch routes, and the nested workspace
on
project create, answer `422` with
"This instance runs agents only in the platform-managed environment;
local
folders are not configurable." when the payload carries a non-null
`cwd`.
A `cwd: null` write still passes, so an instance that just turned the
feature on
can clear a stale path.

With `enableIsolatedWorkspaces` off, the three workspace route groups
redirect to
the dashboard.

**Reason and benefit**

A control that cannot do anything is worse than a missing control: the
user fills
it in, saves, and gets no error and no effect. The server rule turns
that silent
no-op into a clear refusal. The route gate stops a feature that an
instance has
turned off from staying reachable by URL, which is the same standard the
Cases,
Pipelines, and hidden-settings pages already meet.

**Breaking changes**

None for a default instance: both flags are off by default for
self-hosted and
managed instances, so nothing changes unless an operator turns them on.
Stored
`adapterConfig` values are never cleared, so turning the feature off
restores
every previous value.

## What Changed

- Add `ui/src/hooks/useManagedSandboxOnly.ts`, modelled on
`useAppsEnabled`, for
components that do not already read the experimental settings. It
exposes
`hideHostPaths`, which fails closed while the settings query is in
flight, so
a cold cache never flashes a host path before the policy resolves.
Components
that keep their own settings read compute the same gate from
`isFetched`.
- Add `managedSandboxOnly` to `AdapterConfigFieldsProps` and populate it
where
`AgentConfigForm` builds the adapter field props. Resolve the effective
instructions-file gate once as `hideInstructionsFile || hideHostPaths`,
so
  every adapter hides that path field with no per-adapter edit.
- Hide under the flag: the project "Local folder" block and its
absolute-path
  edit panel (a managed checkout keeps its label, without the path); the
new-project "Local folder" field; the project-workspace "Local path"
field and
fact row; the execution-workspace "Paths" and "Lifecycle commands"
groups; the
working directory on the workspace summary card, the task workspace
card, the
task properties "Folder" row, and the runtime service rows; the plugin
"Local
folders" section; "Working directory (deprecated)" and "Command" in the
agent
form; and the per-adapter "Execution engine", "ACP server command", and
"ACP state directory" for `claude_local`, `codex_local`, and
`gemini_local`.
- Drop two working-directory fallbacks that had no gate to read: the
close-workspace
dialog now falls back to "No additional details", and the reuse-existing
  workspace label and picker subtitle fall back to a neutral phrase.
- Refuse a non-null `cwd` with `422` on `POST /projects/:id/workspaces`,
`PATCH /projects/:id/workspaces/:workspaceId`, and the nested workspace
on
  `POST /companies/:companyId/projects`, following the
  `assertNoAgentHostWorkspaceCommandMutation` precedent on those routes.
- Add `IsolatedWorkspacesRouteGate` and wrap the `/workspaces`,
`/execution-workspaces/*`, and
`/projects/:projectId/workspaces/:workspaceId`
  routes with it.
- Leave the SSH "Remote workspace path" and the workspace file browser
alone,
  with a comment explaining why.

## Verification

Automated:

- `pnpm --filter @paperclipai/ui exec vitest run` — 479 of 480 files
pass
(4455 of 4456 tests). The one failure is `OnboardingWizard.test.tsx >
renders
instead of throwing when the browser denies storage access`, which also
fails
  on `origin/master` and is unrelated to this change.
- `pnpm --filter @paperclipai/server exec vitest run project workspace
instance-settings`
— 45 of 50 files pass. Four files fail on macOS for reasons unrelated to
this
  change: `workspace-instance-cleanup`, `workspace-runtime`,
  `execution-workspace-runtime-control-conflict`, and
  `workspace-runtime-exposure` compare `/var/...` against the resolved
`/private/var/...` or bind real ports. The same files fail on a clean
`master`
  checkout on the same machine.
- `pnpm --filter @paperclipai/ui typecheck`
- `tsc --noEmit` in `server/` (after
`pnpm --filter @paperclipai/plugin-sdk ensure-build-deps`). The package
`typecheck` script also builds the Rust runner, which needs `cargo`; it
is not
  installed on the machine that ran this.

New and extended tests:

- `ui/src/adapters/managed-sandbox-only-config-fields.test.tsx` — the
three
adapters drop the execution engine, the ACP paths, the instructions-file
path,
and every "Choose" button when the flag is on, and keep the non-path
controls.
- `ui/src/components/AgentConfigForm.render.test.tsx` — flag-on and
flag-off
renders for the working directory, the command, the engine, the ACP
paths, and
  the resolved adapter field props.
- `ui/src/components/ProjectProperties.managed-sandbox.test.tsx`,
  `ui/src/components/NewProjectDialog.managed-sandbox.test.tsx`,
  `ui/src/pages/ProjectWorkspaceDetail.test.tsx`,
  `ui/src/components/ProjectWorkspaceSummaryCard.test.tsx`,
  `ui/src/components/WorkspaceRuntimeControls.test.tsx`.
- `ui/src/components/IsolatedWorkspacesRouteGate.test.tsx` — redirect
when off,
  render when on, and render nothing while the flag query is in flight.
- "Still loading" cases for the project properties, the new-project
dialog, the
workspace summary card, the runtime service rows, and the agent
configuration
form, each asserting that no host path renders before the policy
resolves.
-
`server/src/__tests__/project-workspace-managed-sandbox-routes.test.ts`
— the
  `422` on all three write paths, the `cwd: null` pass-through, and the
  flag-off pass-through.

Manual check to reproduce: turn on Managed Environment Only in instance
experimental settings, then open a project, the new-project dialog, an
agent's
configuration, and a workspace page. No path, folder icon, or "Choose"
button
appears. Turn the setting off and each control returns with its stored
value.

No documentation change was needed. The operator-facing text for both
settings
lives in the feature catalog entry, which already states the contract
this pull
request now enforces across the UI.

## Risks

- Low. Both flags default to off, so a default instance is unchanged.
- The hidden fields are presentation only. No stored `adapterConfig`
value is
cleared, because an import carries adapter configuration written on
another
instance and clearing it would break that flow. Turning the setting off
shows
  every previous value again.
- The `422` is the one behavior change for an API caller, and only while
the
setting is on. `cwd: null` still passes so a stale path can be cleared.
- The route gate renders nothing until the flag query settles, so an
instance
with isolated workspaces on never flashes a redirect. An instance with
the
  feature off now redirects a bookmarked workspace URL to the dashboard.
- Every host-path guard fails closed while the settings query is in
flight, so a
default instance shows those controls a moment later than before on a
cold
load. That is the safe direction: the alternative flashes a path a
managed
  instance must never show.
- Two path surfaces stay on purpose, each with a comment: the SSH
"Remote
workspace path" is a path on the user's own remote host, and the
workspace file
browser shows workspace-relative paths. The instance Adapters page also
keeps
its "Local path" install option, since that page is an instance-admin
surface
the hosting operator can already hide through the hidden-settings
mechanism.

## Model Used

Claude (Anthropic), Claude Opus, 1M context window, extended thinking,
agentic
tool use through Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A company is the top-level container, and the company General page
holds its settings
> - Two of those settings did almost nothing: the brand color only
tinted the generated company icon, and the attachment size limit sat
under the deployment-level `PAPERCLIP_ATTACHMENT_MAX_BYTES` cap that
already bounded every upload
> - A setting that changes one icon hue, and a setting that can only
lower a limit the operator already set, are not worth the page space or
the code that carries them
> - This pull request deletes both settings from the UI, the validators,
the API contract, the server, and the database
> - With the deployment cap as the only limit left, the message a person
sees when an upload is rejected has to name that limit in terms they can
act on, so the raw byte count becomes a human-readable size
> - The benefit is a shorter company General page for every deployment,
one attachment limit instead of two, and less code between an upload and
its ceiling

## Linked Issues or Issue Description

No existing issue. The description below follows
`.github/ISSUE_TEMPLATE/enhancement.yml`.

**What existing behavior does this improve?**

The company General page (`/company/settings`), the `PATCH
/api/companies/{companyId}` and `PATCH
/api/companies/{companyId}/branding` request contracts, and the
attachment upload limit on task, case, and company-import uploads.

**Subsystem affected**

Cross-cutting: `ui/`, `server/`, `packages/shared`, `packages/db`.

**Current behavior**

The company General page shows an "Appearance" section with three
controls: Logo, Brand color, and Attachment size limit. The brand color
is a hex value that feeds one thing — the hue of the generated company
pattern icon. Companies that never set one already get a hue derived
from the company name. The attachment size limit is a per-company byte
count stored on `companies.attachment_max_bytes`. Every upload path
clamps it against the deployment-level `PAPERCLIP_ATTACHMENT_MAX_BYTES`
cap, so the per-company value can only lower a limit the operator
already chose.

**Proposed behavior**

The Appearance section keeps the Logo control only. The company pattern
icon always derives its hue from the company name. Every attachment path
reads the deployment cap directly, so `PAPERCLIP_ATTACHMENT_MAX_BYTES`
is the single limit. An upload rejected by that limit says so in human
units — "File is larger than the 10 MB limit" rather than a raw byte
count. The `companies.brand_color` and `companies.attachment_max_bytes`
columns are dropped, and both fields leave the company API contract.

**Reason and benefit**

Both settings ask an operator to make a decision that changes almost
nothing. The brand color moves one icon hue on a page that also lets you
upload a real logo, which overrides the icon entirely. The attachment
limit reads as a real control but cannot raise anything, so it is a
second place to look when an upload is rejected. Removing both shortens
the page every deployment sees, removes a company-scoped read from the
task attachment upload path, and leaves one attachment limit to reason
about instead of two.

**Breaking changes**

The company API responses no longer include `brandColor` or
`attachmentMaxBytes`, and `GET /api/invites/{token}` no longer includes
`companyBrandColor`. `PATCH /api/companies/{companyId}/branding` is
strict, so a request that sends `brandColor` now returns 400; the
non-strict `PATCH /api/companies/{companyId}` schema strips it. Company
packages exported by older versions still import: the portability
company manifest schema is non-strict, so the retired keys are stripped
and ignored rather than rejected. Companies that stored a brand color
lose it — their icon reverts to the name-derived hue that every company
without a color already used.

## What Changed

- Removed the "Brand color" and "Attachment size limit" fields from the
company General page, along with their state, dirty checks, save
payload, and Save-button gating.
- Removed `brandColor` and `attachmentMaxBytes` from
`createCompanySchema`, `updateCompanySchema`, and
`updateCompanyBrandingSchema`, and deleted the now-orphaned
`DEFAULT_COMPANY_ATTACHMENT_MAX_BYTES` and
`MAX_COMPANY_ATTACHMENT_MAX_BYTES` constants.
- Removed both fields from the `Company` type, the portability manifest
type and schema, and the `companiesApi.update` payload allowlist.
- Dropped `brandColor` from `CompanyPatternIcon` and its callers, so the
icon hue always comes from the company name. Deleted the now-unused
`hexToHue` helper and the now-unused `pickTextColorForSolidBg` export.
- Stopped emitting `brandColor` from the company service selection and
from the invite-summary and invite-branding payloads in
`server/src/routes/access.ts`.
- Replaced `normalizeIssueAttachmentMaxBytes` with the deployment cap:
task attachments, case attachments, and company import now use
`MAX_ATTACHMENT_BYTES` directly. The helper is deleted.
- Added `formatAttachmentSize()` next to `MAX_ATTACHMENT_BYTES` and
routed every over-limit message through it, so a rejected upload names
the limit in human units instead of raw bytes: `Image exceeds 10485760
bytes` becomes `Image is larger than the 10 MB limit`. Enforcement is
unchanged — the same single cap, the same multer limits, the same status
codes and response shapes.
- Added migration
`0229_drop_company_brand_color_and_attachment_max_bytes.sql` and removed
both columns from the Drizzle `companies` schema.
- Kept legacy imports working: the portability company manifest schema
is non-strict, so older packages carrying the retired keys still import
with the keys ignored.
- Updated the skill API reference and the implementation spec, and
pruned the token-extraction allowlist entries that the removed code made
stale.

## Verification

Commands run from the repository root:

- `pnpm --filter @paperclipai/shared typecheck` — pass
- `pnpm --filter @paperclipai/db typecheck` — pass (includes
`check:migrations`, which validates the new migration number and journal
entry)
- `pnpm --filter @paperclipai/ui typecheck` — pass
- server typecheck via `node_modules/.bin/tsc --noEmit` in `server/` —
pass. `pnpm --filter @paperclipai/server typecheck` could not run
locally because it builds the Rust runner first and `cargo` is not
installed on this machine; the TypeScript step it wraps is the command
above.
- `npx vitest run packages/shared/src/validators/company.test.ts` — 6
passed
- `npx vitest run server/src/__tests__/company-portability.test.ts` — 90
passed
- `npx vitest run server/src/__tests__/attachment-types.test.ts
server/src/__tests__/assets.test.ts
server/src/__tests__/issue-attachment-routes.test.ts
server/src/__tests__/company-portability.test.ts
server/src/__tests__/cases-routes.test.ts` — 165 passed (the
human-readable limit messages)
- `npx vitest run server/src/__tests__/company-branding-route.test.ts
server/src/__tests__/issue-attachment-routes.test.ts
server/src/__tests__/invite-summary-route.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/companies-route-cross-company-authz.test.ts` — all
passed
- `npx vitest run cli/src/__tests__/company.test.ts
cli/src/__tests__/company-delete.test.ts` — 27 passed
- `npx vitest run` in `ui/` — 4425 passed, 1 pre-existing failure
unrelated to this change (`OnboardingWizard.test.tsx` "renders instead
of throwing when the browser denies storage access", which also fails on
`master`)
- `npx vitest run` in `server/` — see the note below
- `node scripts/check-token-gates.mjs` — no new violations; the only
reported violations are the pre-existing `PillGuy.tsx` ones present on
`master`

New tests added:

- `packages/shared/src/validators/company.test.ts` — the create and
update schemas strip the retired keys, the strict branding schema
rejects `brandColor`, and the portability manifest schema accepts a
legacy entry carrying both keys and drops them.
- `server/src/__tests__/company-branding-route.test.ts` — `PATCH
/api/companies/{companyId}/branding` returns 400 for `brandColor` and
does not call the company service.
- `server/src/__tests__/company-portability.test.ts` — a legacy package
that declares `brandColor` and `attachmentMaxBytes` imports
successfully, and neither key reaches `companies.create`.
- `server/src/__tests__/issue-attachment-routes.test.ts` — the effective
task attachment limit is the deployment cap, and the route no longer
loads the company to size an upload.
- `server/src/__tests__/attachment-types.test.ts` —
`formatAttachmentSize()` renders the default cap as `10 MB`, keeps one
decimal place for fractional sizes and drops a trailing `.0`, falls back
to KB and bytes for small caps, steps up to GB, and never emits `NaN`
for a degenerate input.
- `server/src/__tests__/assets.test.ts` — the asset-image and
company-logo routes both return the human-readable limit message on an
over-cap upload.

## Merge with master

`master` moved while this was open, and the merge needed two
resolutions:

- **`ui/src/pages/CompanySettings.tsx`.** #12243 reworded the
user-facing
copy from "company" to "organization", and that rewording landed inside
  the "Brand color" and "Attachment size limit" hints — the two fields
this change deletes. Both fields are removed, so the conflicted block is
dropped whole. The Logo field and every other copy change from #12243
are
  kept.
- **Migration renumbered 0228 -> 0229.** #12307 landed
  `0228_nasty_grim_reaper`, so this migration is now
`0229_drop_company_brand_color_and_attachment_max_bytes`. Its snapshot
is
rebuilt from master's `0228_snapshot.json` with only the two `companies`
  columns removed, and `meta/_journal.json` is master's journal plus a
single `idx: 229` entry. `pnpm --filter @paperclipai/db
check:migrations`
  passes.

The snapshot was rebuilt by hand rather than taken from `drizzle-kit
generate`, because master's `0228_snapshot.json` has drifted from
master's
own schema: `issue_question_response_deliveries.error_count` is created
by
master's 0228 SQL but missing from its snapshot, and the snapshot still
carries `decision_archive_notification_outbox.error_count`. Regenerating
folds both into this migration, and the resulting `ADD COLUMN
error_count`
would fail on a fresh database where master's 0228 already created that
column. Rebuilding from master's snapshot leaves that drift exactly
where
it is and keeps this migration to the two column drops. The drift is
pre-existing on master and is not addressed here.

## Risks

- **The migration is a destructive column drop.**
`0229_drop_company_brand_color_and_attachment_max_bytes.sql` removes
`companies.brand_color` and `companies.attachment_max_bytes`. It is safe
because both features are removed in the same change and nothing reads
either column after it. The statements use `DROP COLUMN IF EXISTS`,
matching the convention of the recent drop migrations in this
repository. The drop is not reversible: a downgrade after this migration
loses any stored values.
- **Stored brand colors are lost.** A company that had set a color now
renders the name-derived icon hue that every company without a color
already used. No other surface changes, and an uploaded logo still
overrides the icon.
- **API response shape narrows.** `brandColor` and `attachmentMaxBytes`
leave the company payloads, and `companyBrandColor` leaves the invite
summary payload. A client reading those fields now sees `undefined`. The
bundled UI and CLI are updated in this change.
- **Legacy imports are covered.** Packages exported by older versions
still carry both keys. The manifest schema is non-strict, so the keys
are stripped rather than rejected, and a test locks that in.
- **The over-limit message strings changed.** Anything matching on the
old `... exceeds N bytes` text — a test, a script, or a client that
string-matches `body.error` — needs updating. The status codes (422) and
response shapes are unchanged, so structured clients are unaffected.
- **Attachment limits can only widen.** A deployment that had lowered a
company below the deployment cap now allows uploads up to the cap for
that company. Lower `PAPERCLIP_ATTACHMENT_MAX_BYTES` if a smaller
ceiling is needed.
- **Storybook visual baselines shift** for the `CompanyPatternIcon`
matrix story, because those fixtures had brand colors. That workflow
runs only on a PR labeled `storybook-visual`, so it does not gate this
PR; regenerate the baselines if the label is added.

## Model Used

Claude (Anthropic), Claude Opus, agentic tool use via Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
…12333)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip keeps its state in PostgreSQL, and `packages/db` owns that
schema through Drizzle
> - `drizzle-kit generate` writes a new migration by diffing
`packages/db/src/schema/` against the newest snapshot in
`packages/db/src/migrations/meta/`, so the snapshot must describe the
schema that the migrations produce
> - Snapshot `0228` recorded the new `error_count` column on the wrong
table, and snapshot `0229` inherited the error, so the newest snapshot
no longer matched the schema
> - Because of that, `generate` on `master` folded the drift into any
new migration: it emitted an `ADD COLUMN` for a column that migration
`0228` already creates, which fails on a fresh database, plus an
out-of-scope `DROP COLUMN`
> - This pull request moves the column entry to the correct table in
both snapshots and adds a test that repeats the diff `generate` performs
> - The benefit is that the next person who generates a migration gets
only their own change, and CI fails if the snapshot drifts again

## Linked Issues or Issue Description

No existing issue. The description below follows
`.github/ISSUE_TEMPLATE/bug_report.yml`.

**What happened?**

`drizzle-kit generate` on `master` emits a wrong migration. The newest
snapshot, `packages/db/src/migrations/meta/0229_snapshot.json`,
disagrees with the schema in two places. It omits
`issue_question_response_deliveries.error_count`, which
`0228_nasty_grim_reaper.sql` creates. It also carries
`decision_archive_notification_outbox.error_count`, which no migration
ever creates and the Drizzle schema never declared. Snapshot `0228`
introduced both halves of the error: it added the new `error_count`
column to `decision_archive_notification_outbox` instead of the table
that the same migration creates. Snapshot `0229` copied it forward. Any
new migration therefore starts with two statements that do not belong to
it:

```sql
ALTER TABLE "issue_question_response_deliveries" ADD COLUMN "error_count" integer DEFAULT 0 NOT NULL;
ALTER TABLE "decision_archive_notification_outbox" DROP COLUMN "error_count";
```

The `ADD COLUMN` fails on a fresh database, because migration `0228`
already creates that column. The `DROP COLUMN` targets a column that
does not exist on any deployment.

**Expected behavior**

`drizzle-kit generate` reports "No schema changes, nothing to migrate"
on a clean checkout of `master`, and a new migration contains only the
author's own schema change.

**Steps to reproduce**

1. Check out `master` at commit `bc1a21564`.
2. Run `pnpm install`.
3. Run `pnpm --filter @paperclipai/db generate`.
4. Read the emitted `packages/db/src/migrations/0230_*.sql`. It contains
the two statements above, and no schema file was changed.

**Paperclip version or commit**

`master` at `bc1a21564`. The drift entered in #12307 (snapshot `0228`)
and was carried forward by #12291 (snapshot `0229`), which worked around
it by building its snapshot by hand.

**Deployment mode**

Not deployment specific. It affects anyone who generates a migration,
and it affects any fresh database that would later run the bad
migration.

**Database mode**

All PostgreSQL modes: embedded, local Docker, and hosted.

## What Changed

- Moved the `error_count` column entry from
`decision_archive_notification_outbox` to
`issue_question_response_deliveries` in
`packages/db/src/migrations/meta/0228_snapshot.json` and
`packages/db/src/migrations/meta/0229_snapshot.json`. Both files keep
their `id` and `prevId`, so the snapshot chain is unchanged.
- Added `packages/db/src/migration-snapshot-drift.test.ts`. It reads the
newest snapshot named by `_journal.json`, serializes the schema modules
with `generateDrizzleJson`, and asserts that `generateMigration` returns
no statements. This is the same diff that `generate` performs.
- Documented the snapshot rule in `doc/DATABASE.md` under a new
"Migration snapshots" section.

No migration SQL was added, renumbered, or edited. No schema file
changed. The database is correct as it is; only the snapshot was wrong.

Why both snapshots and not only the newest one: `0229` is the file that
`generate` reads, so repairing it is what fixes the bug. `0228` holds
the same error, and `drizzle-kit drop` removes the last migration and
its snapshot, which would promote `0228` back to newest and bring the
drift back. Repairing both removes that trap. Snapshots are never
applied to a database, so neither edit changes any deployment.

## Verification

Commands run from the repository root.

- `pnpm --filter @paperclipai/db generate` — "No schema changes, nothing
to migrate 😴". It writes no SQL file, no snapshot, and no journal entry.
`git status` stays clean. Before the fix, the same command wrote
`0230_fast_caretaker.sql` with the two spurious statements.
- The repaired `0229_snapshot.json` is byte-identical to the snapshot
that a real `generate` run produced, except for the `id` and `prevId`
that keep the chain intact.
- Chain check: the repaired `0228` and `0229` snapshots now differ by
exactly the two columns that
`0229_drop_company_brand_color_and_attachment_max_bytes.sql` drops,
`companies.brand_color` and `companies.attachment_max_bytes`, and by
nothing else.
- Database check: applied all 229 migrations in order to an embedded
PostgreSQL, then compared the live schema with the repaired snapshot.
179 tables and 2687 columns match, with no missing column, no extra
column, and no nullability difference. The same comparison against the
pre-fix snapshot reports exactly two problems: `column only in snapshot:
decision_archive_notification_outbox.error_count` and `column only in
database: issue_question_response_deliveries.error_count`. This harness
was a scratch script and is not part of the pull request.
- `pnpm --filter @paperclipai/db typecheck` — pass. It runs
`check:migrations`, which is `check-migration-numbering` and
`check-migration-safety`.
- `npx vitest run --root packages/db` — 28 files, 102 tests, all pass.
This includes the new test.
- New test, negative case: with the pre-fix `0229_snapshot.json`
restored, `migration-snapshot-drift.test.ts` fails and prints exactly
the two spurious statements, plus the instruction to run `generate`. It
passes on the repaired snapshot. It takes about 1.2 seconds and needs no
database.
- `node scripts/check-forbidden-tokens.mjs` and `node
scripts/check-no-git-push.mjs` — pass.

## Risks

Low risk. A Drizzle snapshot is a build-time record for `drizzle-kit
generate`. It is never applied to a database, so this change cannot
alter any deployment, and no operator action is needed. Databases that
already ran migrations `0228` and `0229` are correct today and stay
correct.

The proof is a clean `generate`: the command that produced the wrong
migration now reports "No schema changes, nothing to migrate" and writes
nothing.

Two smaller notes:

- The new test depends on `drizzle-kit/api`, which is already a dev
dependency of `packages/db`. If a future `drizzle-kit` upgrade changes
that surface, the test fails loudly at import rather than passing
silently.
- The test imports every module in `packages/db/src/schema/`, which is
the same set that `drizzle.config.ts` points the CLI at. It deduplicates
by object identity, because the barrel re-exports the same table objects
and `drizzle-kit` rejects a table it sees twice.

## Model Used

Claude (Anthropic), Claude Opus, agentic tool use via Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server test suite checks question response delivery and wake
claims
> - One test used wall-clock time and could start a second delivery
under load
> - The second delivery reused one promise resolver and could hang for
15 seconds
> - This pull request uses the injected clock and one resolver for each
wakeup
> - The benefit is a deterministic test that fails at once if a second
wakeup occurs

## Linked Issues or Issue Description

**What happened?**

The wake-claim lease test slept for 70 milliseconds before it ran the
pending sweep. Under load, the lease could look stale during that
interval. The sweep then started a second delivery. The second delivery
reused one promise resolver, so the test hung until the 15-second suite
timeout.

**Expected behavior**

The test must control the time used by the service. One wakeup must use
one resolver. An unexpected second wakeup must fail at once.

**Steps to reproduce**

1. Run the question response delivery test under CPU load.
2. Let the test sleep before the pending sweep.
3. Observe that a second delivery can start and the test can reach the
15-second timeout.

**Paperclip version or commit**

Commit `0dd735e53a5cc9f6d3395834b826dfb0b1da2ea9`.

**Deployment mode**

Built from source with the server test suite.

**Installation method**

Built from source.

**Agent adapter(s) involved**

Not adapter-specific (core test issue).

**Database mode**

Not database-related.

**Additional context**

The change affects one test file. It does not change production source
code.

## What Changed

- Drive the test with the service's injected clock.
- Give each wakeup call its own promise resolver.
- Assert that lease renewal advances the last attempt time.
- Assert that the wakeup runs one time and the attempt count stays at 1.

## Verification

- Run `pnpm exec vitest run
server/src/services/__tests__/question-response-delivery.test.ts`.
- The changed file reports 29 passing tests.
- Run the changed file 25 times, including 5 runs under CPU load.

## Risks

Low risk. The change affects one test file and test setup only. It does
not change production behavior.

## Model Used

OpenAI GPT-5, exact runtime model ID supplied by the Paperclip agent
environment, tool use and code review assistance.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
…ster stops (#12335)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip uses database clients and embedded PostgreSQL test
fixtures
> - A fixture stopped its embedded PostgreSQL cluster while clients
still held connections
> - The postgres.js driver then scheduled a write on a stopped
connection
> - That write escaped the timer callback and caused a test process to
exit with an error
> - This pull request closes registered clients before the fixture stops
its cluster
> - The benefit is stable test teardown and clear failure reporting in
continuous integration

## Linked Issues or Issue Description

Refs: #10869

**What happened?**

An embedded PostgreSQL test fixture stopped its cluster while database
clients still held open connections. The postgres.js driver then
scheduled a deferred write on a dead connection. The write caused an
unhandled error after the test shard reported success.

**Expected behavior**

The fixture closes all live clients for its cluster before it stops the
embedded PostgreSQL cluster. Tests then finish without a deferred write
on a dead connection.

**Steps to reproduce**

1. Run the database regression test with the embedded PostgreSQL
fixture.
2. Stop the fixture while its database client still has an open
connection.
3. Observe the deferred write and the process exit status.

**Paperclip version or commit**

Branch base: bdd8f1b. Change head:
93e85d2.

**Deployment mode**

Local dev with the embedded PostgreSQL test fixture.

**Installation method**

Built from source with pnpm.

**Agent adapter(s) involved**

Not adapter-specific. This change covers database test infrastructure.

**Database mode**

Embedded PGlite.

**Additional context**

The change keeps client references weak and keys them by host and port.
It does not retain credentials. It also handles connection URLs that the
driver accepts when the URL parser rejects them.

## What Changed

- Add a registry for live database clients in the database package.
- Close registered clients before the embedded PostgreSQL fixture stops
its cluster.
- Add a regression test for the teardown race.
- Handle driver-compatible URLs that the standard URL parser rejects.
- Add cleanup for the shared route test harness.

## Verification

- Run the full `packages/db` suite.
- Run `tsc --noEmit` in `packages/db`.
- Run the server suite that uses `route-test-harness.ts`.
- Run the teardown regression test five times.
- Confirm that the negative control fails three times.
- Confirm that no shard reports green tests and exits with an error.

## Risks

The registry changes client cleanup for embedded test fixtures. Weak
references limit retained memory in long-lived processes. The registry
uses host and port only, so it does not retain credentials. No
migration, schema, API, telemetry, authentication, or cryptography
change exists.

## Model Used

OpenAI Codex, GPT-5, tool use and code review support, standard
reasoning mode. The implementing engineer supplied the code and
verification results.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Sandbox providers run agent work in isolated environments
> - The Daytona inbound file-sync path writes selected sandbox data to
host paths
> - Its in-sandbox lexical and realpath guards do not protect host-write
or Paperclip API authority
> - This pull request removes those guards and simplifies the inbound
file-sync commands
> - The benefit is a smaller path with the same outbound controls and
host extraction validation

## Linked Issues or Issue Description

No public issue exists for this change. This pull request describes the
enhancement.

**What existing behavior does this improve?**

It improves inbound file and directory synchronization for the Daytona
sandbox provider.

**Subsystem affected**

`packages/plugins` — the Daytona sandbox provider.

**Current behavior**

The inbound path checks lexical and realpath confinement inside the
sandbox. It uses file-descriptor-pinned commands for file promotion,
archive extraction, and decompression. These checks do not protect
host-write or Paperclip API authority.

**Proposed behavior**

Remove the inbound lexical and realpath checks. Use `mv -f` for file
mappings, `tar -xf` for directory mappings, and `zstd -d -o` for
decompression. Keep outbound source checks, atomic snapshot downloads,
and tarball member validation.

**Reason and benefit**

The sandbox boundary protects the relevant authorities. The removed
guards run inside that boundary and only produce early errors. The
simpler commands reduce code and preserve the controls that protect the
host boundary.

**Breaking changes**

The inbound path no longer rejects mappings because of sandbox-side
lexical or realpath confinement. Outbound source validation and tarball
member validation remain unchanged.

## What Changed

- Remove lexical and realpath confinement guards from inbound file
mappings, inbound directory mappings, and post-upload command working
directories.
- Replace file-descriptor-pinned promotion with one `mv -f` command per
file mapping.
- Replace file-descriptor-pinned extraction with one `tar -xf` command
per directory mapping.
- Replace retained-descriptor decompression with `zstd -d -o`.
- Keep outbound source guards, atomic snapshot-and-download, and tarball
member validation.
- Update Daytona tests for the new command shapes and remove tests for
the removed rejections.

## Verification

- The author ran the Daytona package test suite: 232 tests passed and 6
tests skipped.
- The author ran the Daytona package typecheck successfully.
- Review the diff and confirm it changes only the three Daytona files
named in this description.
- Confirm the Storybook check may report `SKIPPED` as an expected
repository state.

## Risks

The inbound path now trusts the sandbox boundary for host-write
protection. A later change that gives sandbox code host-write or
Paperclip API authority could require new guards. Outbound source checks
and archive member validation remain in place. No Kubernetes or core
runtime file changes exist.

## Model Used

OpenAI GPT-5; exact model version and context window were not provided;
tool use and code review assistance.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Sandbox providers let agents run in remote environments
> - The Daytona login flow creates a home directory for each login
session
> - The create path ran owner, mode, and link-type checks inside the
sandbox
> - These checks cannot protect the host because sandbox code can change
the checked state
> - This pull request uses one `mkdir -p` command and removes the unused
helper scripts
> - The benefit is a simpler login path with the host-side credential
checks unchanged

## Linked Issues or Issue Description

**What happened?**

The Daytona login flow used helper scripts and inside-sandbox checks for
the session-home directory. The standalone package build also copied a
scripts directory that no longer existed after the helper scripts were
removed.

**Expected behavior**

The login flow must create the session home with one `mkdir -p` command.
The package build must complete without copying a removed directory.

**Steps to reproduce**

1. Build the Daytona plugin package.
2. Start a Daytona device login.
3. Inspect the session-home create command and the package output.

**Paperclip version or commit**

Commit `dfdf5914ba37caa1e3bc380236844a7d76237e12`.

**Deployment mode**

Built from source.

## What Changed

- Replace the session-home helper checks with one `mkdir -p` command.
- Remove the two unused session-home helper scripts.
- Remove the dead build copy steps for the deleted scripts directory.
- Add coverage for a failed session-home create command.
- Keep the host-side credential reader unchanged.
- Keep the Kubernetes provider package unchanged.

## Verification

- Package unit tests pass: 220 passed, 6 skipped.
- Package typecheck passes.
- The package build passes and emits 56 files in `dist`.
- The roadmap check confirms that this change stays within the planned
sandbox-provider work.
- GitHub search found no open duplicate or related pull request.

## Risks

The login flow no longer reports owner, mode, or link-type errors from
inside the sandbox. Those checks did not protect the host. The host-side
credential reader still uses no-follow path opens and accepts only a
regular file with owner and exact mode `0600`. Risk is low because this
change removes checks that cannot enforce the host security boundary.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. This pull request updates an
existing sandbox-provider path, not a new core feature.

## Model Used

OpenAI Codex, GPT-5. The runtime provides tool use and code execution.
The runtime does not expose the context window size or a more specific
model identifier.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - New tenants meet it through an onboarding wizard: create your agent,
connect a model, review
> - Those three screens only render for a signed-in account that owns a
provisioned stack, so the only way to look at one was to walk a real
signup
> - Round 4 redesigned all three and shipped them without anyone seeing
them render; when the connect step then failed on a live stack, the
review step behind it could not be reached at all
> - Storybook already mounts the app's provider stack and stubs `/api`,
and already has an `Onboarding/Agent arc` story — but that story
previews `AgentCapsule`, which the wizard stopped using in round 4
> - This pull request mounts the real wizard in Storybook at each step,
and replaces the stale capsule stories with the component the arc
actually renders
> - The benefit is that these screens can be reviewed, and regressions
seen, without provisioning anything

## Linked Issues or Issue Description

No existing issue. Describing it inline, following
`.github/ISSUE_TEMPLATE/enhancement.yml`.

**What existing behavior does this improve?**

Reviewing the tenant onboarding wizard. Today it can only be seen by
signing up for a real account and provisioning a real stack.

**Subsystem affected**

`ui` — the onboarding wizard and its Storybook coverage.

**Current behavior**

`OnboardingWizard` renders only for a signed-in account that owns a
company. There is no route, harness, or story that mounts it, so no
screen in the agent arc can be looked at in isolation. The existing
`Onboarding/Agent arc` story previews `AgentCapsule` in
`slot`/`configured`/`online` and describes it as what "the wizard holds
in one tree slot" — round 4 replaced that with `PillGuy` and
`dormant`/`alive`, so the story documents a component the arc no longer
renders.

**Proposed behavior**

Stories that mount the real `OnboardingWizard` at each of the three
steps against the existing Storybook API fixtures, plus stories for
`PillGuy` and its transition.

**Reason and benefit**

Round 4 shipped three redesigned screens that nobody could see. The
connect step then failed on staging, which made the review step
unreachable even with an account — reviewing it required hand-editing
`localStorage`. Stories remove that whole class of problem.

**Breaking changes**

None. Storybook-only; no product code is touched.

## What Changed

- `CreateYourAgent`, `ConnectAModel`, `Review` — the real wizard, per
step.
- `PillStates` and `PillMorph` — the two states, and the transition on a
loop with a toggle. The morph is the arc's payoff and the hardest thing
to judge from a still.
- Replaces the `AgentCapsule` stories in this file with `PillGuy`.
`AgentCapsule` is still used by `DesignGuide` and keeps its coverage
there.
- Four routes added to the Storybook fetch fixtures:
`/api/instance/settings`, `…/environments`, `…/adapters/:type/models`.
The empty environment list is the cloud-tenant shape, and also the state
that produces the "no managed sandbox environment is available" notice —
worth being able to look at rather than only meeting it on a live stack.

## Three properties of the wizard the stories had to respect

Each of these cost a debugging cycle, so they are documented at the call
site:

1. **The draft is seeded during render, not in an effect.** Roughly
twenty `useState(saved?.x ?? default)` initializers read the restored
blob exactly once, so a draft written after mount arrives too late.
2. **Nothing mounts until the companies list settles.** The wizard's own
mount gate waits on `isFetching`, but that query is *disabled* until the
account settles, and a disabled query is not fetching. Mounting straight
away gets an inner wizard that reads a null draft, falls back to
`initialStep`, and then persists that back over the seed. A real session
never hits this because the dashboard has already loaded the list.
3. **The review step opens with no `initialStep`.** An explicit option
takes precedence over saved state by design, so passing one clamps 5 to
4 and lands on Connect.

## Verification

- `npx vitest run src/components/OnboardingWizard.test.tsx
src/components/OnboardingWizard.step.test.tsx` — 44 tests, all passing.
- `npx tsc -p tsconfig.json --noEmit` — no new errors.
(`src/lib/sentry.*` reports pre-existing missing-type errors for
`@sentry/browser` on master.)
- Each of the three step stories loaded in a running Storybook and read
back:
- `create-your-agent` → "STEP 1 OF 3 / Create your first agent / Name /
Next"
- `connect-a-model` → "STEP 2 OF 3 / Connect a model / Paperclip works
with your existing subscription or API keys. / Claude Code / Codex /
Advanced settings / Connect"
- `review` → "STEP 3 OF 3 / Let's get started... / Darnold is ready to
work! / Get started"

One difference from a real walk, worth knowing before treating a story
as ground truth: the review story has no Back button, because
`entryStep` is 5 there and back-navigation is bounded by where the run
entered.

## Risks

Low. Storybook-only — no product code, routes, or bundles change. The
four added fetch fixtures are inside the Storybook mock and cannot
affect the app.

The one thing to watch: these stories mount the real wizard, so a future
change to how it restores drafts or decides its initial step can break
them. That is arguably the point — the stories would be the first place
it shows — but it does mean they are coupled to internals rather than to
a prop surface, and the three notes above are what a maintainer needs.

## Model Used

Claude Fable 5 (`claude-fable-5`), extended thinking, with tool use:
file editing, shell, and browser automation for loading and reading back
each story.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [ ] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…12375)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server test suite checks budget and cost routes
> - The costs-service test rebuilt the full route module graph before
every test
> - Synchronous graph rebuilds caused long stalls under CPU load
> - This pull request loads the mocked graph once for each describe
block and keeps per-test mock setup
> - The benefit is a stable 17-test file without production code changes

## Linked Issues or Issue Description

**What happened?**

The costs-service route test rebuilt its full mocked module graph before
every test. Under CPU load, a rebuild sometimes stalled a test past the
15-second timeout.

**Expected behavior**

The test file should load its mocked route graph once for each describe
block while each test keeps isolated mock behavior.

**Steps to reproduce**

1. Run the costs-service route test under synthetic CPU load.
2. Repeat the file test 30 times.
3. Observe intermittent test timeouts before this change.

**Paperclip version or commit**

The test used the current master branch at the time of this change.

**Deployment mode**

Built from source.

## What Changed

- Add `hoistModuleGraph` to load the mocked route graph once for each
describe block.
- Keep per-test mock setup in `beforeEach` so test isolation stays
unchanged.
- Keep all 17 tests and their assertions.
- Remove the module graph rebuild from the per-test path.

## Verification

- Run `npx vitest run src/__tests__/costs-service.test.ts` from
`server/`.
- Confirm that the file reports 17 tests and zero skipped tests.
- Confirm that 30 runs under the same synthetic CPU load report 0.0%
failure after the change, compared with 10.0% before the change.
- Confirm that mutation checks still fail when each authorization guard
is broken.

## Risks

This change affects test setup only. The main risk is weaker test
isolation if a mock keeps state between tests. Each test still re-arms
its mock behavior in `beforeEach`, and the full assertion set remains.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. This bug fix does not add a
core feature.

## Model Used

OpenAI GPT-5. The model used tool calls and code execution. The exact
context window and reasoning configuration were not exposed by the
runtime.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass (the submitting engineer
ran the file before handoff)
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Paperclip supports opt-in Sentry error monitoring for server and
browser errors.
> - The hosted image must include the server package when an operator
sets SENTRY_DSN.
> - The server package is an optional peer in the source tree, so the
image did not include it.
> - This pull request installs the declared server package in the hosted
image and checks the result.
> - The benefit is a hosted tenant can send server errors without a
manual package install.

## Linked Issues or Issue Description

No public issue exists for this change.

**What happened?**

The hosted image did not include the declared @sentry/node server
package. A hosted tenant could set SENTRY_DSN, but the server could not
load the package from the image.

**Expected behavior**

The hosted image must include the exact @sentry/node version from
server/package.json. The self-hosted image must remain without this
optional package.

**Steps to reproduce**

1. Build or pull the hosted image.
2. Resolve @sentry/node from the server package path.
3. Compare its version with server/package.json.
4. Confirm that the tsx loader path still resolves.

**Paperclip version or commit**

Commit b6ff556.

**Deployment mode**

Docker hosted image.

## What Changed

- Add a cloud-server-deps Docker stage that installs the declared
@sentry/node version in isolation.
- Copy the isolated package into the cloud image without changing the
production image.
- Add a probe that checks the tsx loader and the resolved Sentry
version.
- Run the probe after the hosted image push in the Docker workflow.
- Add server tests and update the observability documentation.

## Verification

- Run `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/cloud-image-sentry.test.ts`.
- Confirm that the changed test passes in CI.
- Confirm that all pull request checks pass.
- Note that the Docker workflow does not run for pull requests. It runs
after a push to master, for configured tags, or after manual dispatch.

## Risks

- Low risk. The production image body stays unchanged.
- The cloud image adds the declared Sentry package and a small
dependency tree.
- The workflow probe fails if the image loses the tsx loader or resolves
a different Sentry version.

## Model Used

OpenAI GPT-5; exact model version supplied by the execution service;
tool use and code execution; context window not specified.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
…nnel is already lost (#12394)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The adapter runtime settles each run through a duplex control
channel
> - A lost channel can leave the remote session-close call without a
usable peer
> - The call has no deadline, so run teardown can wait for the full
adapter timeout
> - This pull request skips that remote call after the runtime latches
channel loss
> - The benefit is faster run finalization while the local cleanup
effects remain

## Linked Issues or Issue Description

**What happened?**

The run teardown placed a remote session-close call over a duplex
control channel that the runtime had already latched as lost. The call
blocked until the adapter execution timeout released it.

**Expected behavior**

Run teardown should release the local warm handle and continue when the
duplex control channel has already failed.

**Steps to reproduce**

1. Start an adapter run with the duplex control channel.
2. Latch a channel-loss state before settlement.
3. Use a runtime whose close call never resolves.
4. Confirm that teardown returns without a remote close call.

**Paperclip version or commit**

Commit d966069.

**Deployment mode**

Built from source.

**Agent adapter(s) involved**

Not adapter-specific. The change applies to the shared adapter runtime.

**Database mode**

Not database-related.

**Additional context**

Pull request #12373 used a larger approach for the same failure. The
board closed that pull request. This pull request contains the smaller
change.

## What Changed

- Add the required readonly `skipRemoteClose` field to the runtime
settlement plan.
- Set the field from the latched channel-loss state on the turn-finalize
plan.
- Set the field to `false` on every other settlement plan.
- Release the warm handle locally before the `end_session` step returns
without the remote call.
- Add a test that drives the lost-channel path through the settlement
sequence.

## Verification

- `./node_modules/.bin/tsc --noEmit -p packages/adapter-utils`
- `./node_modules/.bin/vitest run
packages/adapter-utils/src/acpx-engine/execute.test.ts` passes the new
test. Three existing tests fail on the merge base: two
session-fingerprint tests and one workspace-hints test.
- `./node_modules/.bin/vitest run
packages/adapter-utils/src/acpx-engine/run-fault-matrix.test.ts` reports
20 passed.
- Full CI will run on this pull request.

## Risks

The skipped remote close also skips the vendored runtime caller for
`closeBackendSession`. The run can keep a retained client after duplex
loss. A separate follow-up owns that residual. The environment lease
still releases in the teardown `finally` block.

## Model Used

OpenAI Codex, GPT-5, with tool use and code review assistance.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A self-hosted install in `authenticated` mode signs users in with
Better Auth, mounted at `/api/auth` over a hand-written Drizzle
`account` table in `packages/db`
> - Better Auth 1.7.0 added a required `issuer` field to that `account`
model, plus a unique index on `(issuer, accountId)`
> - The dependency bump in #11886 changed only `server/package.json` and
the lockfile, so the Drizzle table never grew the column
> - The Drizzle adapter checks the model against the schema on every
write, so `linkAccount` throws and sign-up answers 500 with an empty
body; a fresh install cannot create its first user, and an upgraded
install locks out every existing user
> - This pull request adds the `issuer` column and its unique index, and
migrates the column in with a backfill that covers every existing row
> - The benefit is that sign-up and sign-in work again, on a new install
and after an upgrade

## Linked Issues or Issue Description

No existing issue. Describing it inline, following
`.github/ISSUE_TEMPLATE/bug_report.yml`.

Refs #11886 (the dependency bump that introduced the required field).
Refs #12269 (an earlier attempt at this fix; its backfill covers only
`provider_id = 'credential'`).

**What happened?**

Sign-up fails on a self-hosted install. `POST /api/auth/sign-up/email`
answers HTTP 500 with a zero-byte body. The server log carries:

```
[Better Auth]: The field "issuer" does not exist in the "account" Drizzle schema.
# SERVER_ERROR: [BetterAuthError: The field "issuer" does not exist in the "account" Drizzle schema.]
```

The request writes the `user` row and then fails on the `account` row.
The address is stuck after that: a second sign-up answers 422
`USER_ALREADY_EXISTS`, sign-in answers 401, and password reset answers
400 `RESET_PASSWORD_DISABLED` because the account that would hold the
password does not exist.

An upgraded install is worse. `sign-in/email` matches the credential
account on `account.issuer === 'local:credential'`. Rows written before
the upgrade have no issuer, so every existing user is locked out.

**Expected behavior**

`POST /api/auth/sign-up/email` answers 2xx and writes both the `user`
row and its credential `account` row. `POST /api/auth/sign-in/email`
then answers 2xx and sets a session cookie. An install that upgrades
keeps its existing users.

**Steps to reproduce**

1. Start a server from `master` with
`PAPERCLIP_DEPLOYMENT_MODE=authenticated` against an empty database.
2. `curl -X POST http://127.0.0.1:<port>/api/auth/sign-up/email -H
'Content-Type: application/json' -H 'Origin: http://127.0.0.1:<port>'
--data
'{"name":"A","email":"a@example.com","password":"a-long-password"}'`
3. The response is HTTP 500 with an empty body.

**Paperclip version or commit**

`master` at 4436cf0. The defect starts at 69e8585 (#11886), which moved
Better Auth from 1.6.28 to 1.7.0.

**Deployment mode**

`authenticated`. `local_trusted` does not sign users in, so it is not
affected. Hosted tenants are not affected either: that path resolves the
actor from a trusted header and never reads `account`.

**Database mode**

Both. Embedded PostgreSQL and external PostgreSQL use the same Drizzle
schema.

**Relevant logs or output**

Reproduced in a test by reverting the schema change:

```
stderr | better-auth-credential-signup.integration.test.ts
[Better Auth]: The field "issuer" does not exist in the "account" Drizzle schema.
AssertionError: expected 500 to be 200
```

## What Changed

- `packages/db/src/schema/auth.ts`: adds `issuer` (text, NOT NULL) to
`authAccounts`, and the `(issuer, account_id)` unique index that mirrors
the index Better Auth declares on the model. The field name, type,
requiredness, and index all come from
`@better-auth/core/dist/db/get-tables.mjs` in 1.7.0.
- `packages/db/src/migrations/0230_better_auth_account_issuer.sql`: adds
the column, backfills every existing row, sets NOT NULL, and creates the
unique index.
- `packages/db/src/migrations/meta/0230_snapshot.json` and
`_journal.json`: regenerated with `pnpm --filter @paperclipai/db
generate`.
- `packages/db/src/better-auth-account-issuer-migration.test.ts`: new.
Asserts the schema shape, then rewinds the migration on a real database,
seeds pre-upgrade rows, and re-applies it.
-
`server/src/__tests__/better-auth-credential-signup.integration.test.ts`:
new. Real sign-up and sign-in through the Better Auth mount, against the
real Drizzle schema and a migrated PostgreSQL.
- `cli/src/__tests__/worktree.test.ts`: the worktree seed fixture writes
a credential `account` row, so it now writes `issuer` too.

`server/package.json` and `pnpm-lock.yaml` are untouched. The dependency
is correct; the schema was what was missing.

### The issuer values, and where they come from

Better Auth builds these itself, in
`@better-auth/core/src/db/schema/account.ts`:

```ts
export function createLocalAccountIssuer(providerId: string): string {
  return `local:${encodeURIComponent(providerId)}`;
}
export function createOAuthAccountIssuer(providerId: string): string {
  return `local:oauth:${encodeURIComponent(providerId)}`;
}
```

Sign-up and sign-in both call `createLocalAccountIssuer("credential")`,
so a credential account is `local:credential`. An OAuth account whose
provider declares no `accountIssuer` of its own is
`local:oauth:<providerId>` — no built-in social provider declares one.
The migration writes exactly those two forms:

```sql
ALTER TABLE "account" ADD COLUMN IF NOT EXISTS "issuer" text;
UPDATE "account"
SET "issuer" = CASE
  WHEN "provider_id" = 'credential' THEN 'local:credential'
  ELSE 'local:oauth:' || "provider_id"
END
WHERE "issuer" IS NULL;
ALTER TABLE "account" ALTER COLUMN "issuer" SET NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS "account_issuer_account_id_uq" ON "account" USING btree ("issuer","account_id");
```

Two limits are worth stating plainly. The OAuth branch reproduces
`createOAuthAccountIssuer` for provider ids that need no
percent-encoding, which covers every built-in provider id; a provider id
with a character `encodeURIComponent` would escape would get a slightly
different string. And a generic-OAuth provider that sets `accountIssuer`
explicitly (Okta, Auth0, Keycloak, Slack, Line) uses the real issuer
URL, which this migration cannot know. Neither case can arise on
Paperclip today: `createBetterAuthInstance` configures
`emailAndPassword` only and registers no social or generic-OAuth
provider, so every existing row is a credential row. The OAuth branch is
there so the backfill stays total rather than leaving a NULL that aborts
`SET NOT NULL`.

## Verification

- `pnpm --filter @paperclipai/db check:migrations` — passes.
- `pnpm --filter @paperclipai/db typecheck` — passes.
- `packages/db` suite: 30 files, 107 tests, all pass.
- `npx tsc --noEmit` in `server/` — no error in any changed file. (The
wrapped `pnpm typecheck` builds the runner vendor first, which needs
cargo; that toolchain was not available here, so the pre-existing
"cannot find module" errors from the unbuilt workspace packages remain
in the bare run.)
- `node --test scripts/__tests__/run-vitest-stable-shard.test.mjs` —
passes with the new server suite in the file list.
- The two new tests were confirmed to fail without the fix:
- Reverting `packages/db/src/schema/auth.ts` to its `master` content
makes the server test fail with the reported error and `expected 500 to
be 200`.
- Narrowing the backfill to `WHERE "issuer" IS NULL AND "provider_id" =
'credential'` makes the migration test fail with `column "issuer" of
relation "account" contains null values` — the failure mode of #12269.
- End to end against a server built from this branch, started with
`PAPERCLIP_DEPLOYMENT_MODE=authenticated` on embedded PostgreSQL:
  - `POST /api/auth/sign-up/email` → 200 with a user and token.
  - `POST /api/auth/sign-in/email` → 200 with a session cookie.
  - `GET /api/auth/get-session` → 200 with the session.
- The stored row is `issuer = 'local:credential'`, `provider_id =
'credential'`, `account_id = user_id`, and `pg_indexes` lists
`account_issuer_account_id_uq`.
- `scripts/docker-onboard-smoke.sh` was not used as proof: it installs
`paperclipai` from npm inside the container, so it exercises a published
release rather than this branch.

## Risks

- **Migration.** The migration backfills every existing row before `SET
NOT NULL`, so an install that upgrades keeps working and its users keep
signing in. `account` is one row per user per provider, so the
full-table `UPDATE` and the index build are cheap;
`packages/db/src/table-size-estimates.ts` already classes `account` as
small, and `check:migrations` passes with no new safety finding.
- **New unique index.** `(issuer, account_id)` is the key Better Auth
resolves accounts by, so a duplicate would already be a defect. Better
Auth writes one credential account per user keyed on the user id, so the
pair is unique by construction. An install that somehow holds a
duplicate would fail the index build rather than corrupt anything, and
the migration is a single transaction.
- **Orphaned users are not repaired.** An address that hit the broken
window has a `user` row and no `account` row. This migration does not
delete or repair those rows, so that address stays unusable after the
upgrade: sign-up says the user exists, and there is no credential
account to sign in as or reset. Only installs that ran a build
containing #11886 are affected, and the repair — deleting the orphaned
`user` rows — is a judgment call about live data that does not belong in
an automatic migration.
- **Not a behavior change anywhere else.** Only the `account` table
changes. Hosted tenants resolve their actor from a trusted header and
never read it.

## Model Used

Claude (Anthropic), Claude Opus, 1M context, extended thinking, agentic
tool use via Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
## Thinking Path

> - Paperclip is the open source app that people use to manage AI agents
for work.
> - Paperclip applies database migrations in numeric order.
> - Two branches can create the same migration number before either
branch merges.
> - The existing repository check can find duplicates only after both
histories are present in one checkout.
> - A pull request must compare its new migrations with the target
branch before merge.
> - This pull request adds that comparison to the existing PR policy
job.
> - The benefit is an early failure with exact renumbering instructions.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The PR policy check for files in `packages/db/src/migrations`.

**Current behavior**

A stale branch can add the same migration number as the target branch.
The existing check does not compare PR additions with the target branch
migration tip.

**Proposed behavior**

The policy job fails when a new PR migration number is not greater than
every migration on the target branch. The error names the conflict, the
next safe number, and the related files to update.

**Reason and benefit**

This prevents duplicate or out-of-order migration numbers from reaching
`master`. It also gives contributors and agents a direct repair
procedure.

**Breaking changes**

None. The change rejects migration numbering that is already unsafe.

## What Changed

- Added a dependency-free check that compares new PR migration files
with the target branch tip.
- Added the check to the existing PR policy job.
- Added tests for no-op, valid, duplicate, and lower-number cases.

## Verification

- `node --test '.github/scripts/tests/*.test.mjs'` passed 133 tests.
- `pnpm -r typecheck` passed.
- `pnpm build` passed.
- `pnpm test:run` passed 4,889 tests and failed 24 unrelated macOS path
and wildcard-listener tests that also affect the current `master`
checkout.

## Risks

- Low risk. The check reads Git history and does not modify migrations.
- The check permits gaps. It only requires each new migration number to
follow the target branch tip.
- The existing migration check continues to validate duplicate numbers,
snapshots, and journal entries inside the PR.

## Model Used

OpenAI Codex, GPT-5. The exact serving model ID and context-window size
are not exposed in this session. Reasoning, tool use, web access, and
code execution were enabled.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.