Commit Graph

1059 Commits

Author SHA1 Message Date
Svetlana Zolotenkova 9ac24317e6 fix(issues): ignore cancelled child blockers in attention (#7577)
Docker / build-and-push (push) Failing after 3m33s
Refresh Lockfile / refresh (push) Successful in 10m42s
Release / verify_canary (push) Failing after 16m21s
Release / verify_stable (push) Has been skipped
Release / publish_canary (push) Has been skipped
Release / preview_stable (push) Has been skipped
Release / publish_stable (push) Has been skipped
Fixes #7578

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - Blocked issue health is surfaced through blockerAttention so
operators can see whether blocked work has a live path.
> - The blockerAttention graph uses both explicit blockedBy edges and
direct child issue edges.
> - Done children were already ignored, but cancelled direct children
still appeared as unresolved blockers.
> - Explicit cancelled dependencies should remain visible as dependency
problems, but terminal direct children should not inflate a parent
blocker count.
> - This pull request narrows child-edge traversal to non-terminal
children and adds a regression test for the observed case.
> - The benefit is that cancelled child issues no longer make blocked
parents look like they have extra unresolved blocker attention.

## What Changed

- Added terminal child status filtering for blockerAttention
parent-child traversal so cancelled direct children are ignored with
done children.
- Added a server regression test where a blocked parent has active
explicit blockers plus a cancelled direct child; the cancelled child no
longer increases unresolved counts or becomes the sample blocker.

## Verification

- `perl -e 'alarm shift; exec @ARGV' 300 pnpm exec vitest run
server/src/__tests__/issue-blocker-attention.test.ts` -> 19 tests
passed.

## Risks

- Low risk: the change only affects implicit direct-child
blockerAttention edges.
- Explicit blockedBy edges to cancelled issues are intentionally
unchanged and remain represented as attention-required dependency
problems.

## Model Used

- OpenAI GPT-5, Codex coding agent in tool-enabled CLI environment;
reasoning and code execution used for repository inspection, patching,
git operations, and targeted test 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots (not applicable; no UI change)
- [x] I have updated relevant documentation to reflect my changes (not
applicable; bugfix covered by regression test)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-06-21 00:29:44 -07:00
Devin Foley d1e6662ed8 fix(server): let codex_local agents inherit host Codex login by default (#8425)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The `codex_local` adapter spawns the Codex CLI for an agent, and
`server/src/routes/agents.ts` normalizes each agent's
`adapterConfig.env` on create/hire/update
> - PR #8272 added an isolation guard that, on every codex_local agent,
force-set a per-agent `CODEX_HOME` and injected `OPENAI_API_KEY = ""`,
and rejected any "shared" home
> - This broke the common case: operators who deleted `CODEX_HOME` /
`OPENAI_API_KEY` in the UI saw them silently re-appear on save, and
every agent was forced into an isolated home instead of sharing the
device's existing Codex login (`~/.codex` / `$CODEX_HOME`)
> - This pull request replaces the always-on isolation guard with
key-scoped isolation: a keyless agent gets no env overrides and inherits
the host Codex login at runtime; we only carve out an isolated per-agent
`CODEX_HOME` when the agent sets its own `OPENAI_API_KEY`
> - The benefit is that env-var deletion now persists, agents on one
device share the host login by default, and per-account isolation is
still available by setting a per-agent key

## Linked Issues or Issue Description

No public GitHub issue exists. Bug report (following `bug_report.yml`):

**What happened?** In a `codex_local` agent's configuration, removing
the `CODEX_HOME` and `OPENAI_API_KEY` env vars via the UI (clicking the
X) appears to work, but on save they instantly re-appear. The persisted
config never loses the slots.

**Root cause.** `applyCodexLocalIsolationGuard` in
`server/src/routes/agents.ts` (added in #8272) re-injected `CODEX_HOME`
(defaulting to a per-agent home) and `OPENAI_API_KEY = ""` on every
create/hire/update, and rejected shared homes outright. So a PATCH that
omitted those keys had them written back server-side.

**Expected behavior.** Deleting these env vars should persist. A
codex_local agent with no key should inherit whatever Codex login
already exists on the device.

**Steps to reproduce.**
1. Open a `codex_local` agent's configuration with `CODEX_HOME` and
`OPENAI_API_KEY` set.
2. Remove both env vars and save.
3. Re-open the config — both slots are back.

Related (different approach): Refs #8399 (keeps per-agent isolation,
fixes only the empty `OPENAI_API_KEY` slot), Refs #8272 (introduced the
guard), Refs #8403 (managed-auth seeding into isolated homes).

## What Changed

- `server/src/routes/agents.ts` — replaced
`applyCodexLocalIsolationGuard` (+ `assertCodexLocalHomeIsNotShared` /
`normalizeCodexLocalHomePath`) with `applyCodexLocalKeyIsolation`. A
codex_local agent now receives **no** env overrides unless it explicitly
sets `OPENAI_API_KEY`; only then is an isolated per-agent `CODEX_HOME`
injected (and only if the agent has not set its own `CODEX_HOME`).
Keyless agents fall back to the host Codex login at runtime. Dropped the
now-unused `node:os` import and the shared-home rejection.
- `server/src/__tests__/agent-adapter-validation-routes.test.ts` —
updated the validation-route tests to assert env-var deletion persists,
keyless agents get no overrides, and key-bearing agents still get an
isolated `CODEX_HOME`.

## Verification

```bash
cd server && npx vitest run src/__tests__/agent-adapter-validation-routes.test.ts
```

All 6 tests pass locally. Manually verified in the live UI: removing
`CODEX_HOME` and `OPENAI_API_KEY` from a codex_local agent and saving
now persists the deletion (slots no longer re-appear on reload).

## Risks

- **Host-key leak for keyless agents on a host with `OPENAI_API_KEY`
set.** Low/intended: the product direction here is that codex_local
agents inherit the host's Codex login by default; per-account isolation
is opt-in via a per-agent `OPENAI_API_KEY`, which then gets its own
`CODEX_HOME`.
- **Divergence from #8399.** That PR retains the per-agent isolation
default and only stops injecting the empty key, so it does not address
the `CODEX_HOME` re-injection half of this bug. This PR intentionally
changes the default to host-login inheritance. Reviewers should pick one
direction.
- **No migration.** Existing agents that already store these slots are
not auto-cleaned, but operators can now delete them and the deletion
sticks.

## Model Used

- Provider: Anthropic (Claude)
- Model ID: `claude-opus-4-8`
- Capabilities: tool use, code execution, extended reasoning, agentic
workflow

## 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 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
- [ ] If this change affects the UI, I have included before/after
screenshots
- [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
2026-06-20 22:51:56 -07:00
Devin Foley 2c98c8e1e5 Fix sandbox git publishing and large workspace uploads (#8422)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Sandbox and SSH runtimes need to preserve agent work across isolated
execution environments
> - Git-backed workspaces were being copied mostly as filesystem
archives, which breaks when `.git` points outside the mounted workspace
and makes sandbox agents unable to publish their own branches
> - Large ignored dependency trees could also be swept into the sandbox
overlay, causing multi-GB transfers and max-string failures in some
sandbox clients
> - This pull request makes sandbox runtime setup use a git-backed HEAD
sync plus a small dirty/untracked overlay, and bounds sandbox file
transfers so large archives do not need one huge string
> - The benefit is that sandbox agents can commit and push from a usable
git checkout without uploading dependency trees such as `node_modules`

## Linked Issues or Issue Description

Refs #8395

No public duplicate issue or PR was found after searches for `sandbox
git workspace`, `git push sandbox`, and `node_modules sandbox upload`.

Bug report:

- What happened: sandbox-backed agent workspaces could receive a `.git`
file that pointed at host-only git state, leaving the sandbox unable to
run normal git workflows. The sandbox overlay upload could also include
ignored dependency directories, creating very large transfers.
- Expected behavior: sandbox and remote runtimes should prepare a usable
git-backed workspace, copy only the necessary workspace overlay, and
restore git history plus file changes without depending on a host-only
`.git` path.
- Steps to reproduce:
1. Run an agent in a sandbox-backed workspace whose local git checkout
is a worktree.
2. Ask the agent to complete a GitHub workflow that requires commit/push
access.
3. Observe that git operations can fail inside the sandbox, and ignored
dependency trees can be uploaded as part of the workspace overlay.
- Paperclip version or commit: reproduced against `master` before this
PR, base `7aa212296eb1`.
- Deployment mode: local dev / sandbox-backed runtime.
- Installation method: built from source.
- Agent adapters involved: local adapters using shared adapter-utils
runtime preparation.
- Database mode: not database-related.
- Access context: agent runtime.
- Local verification environment: Node.js v25.6.1, pnpm 9.15.4, macOS
arm64.
- Privacy checklist: all pasted output was reviewed for secrets, private
hostnames, local usernames, and internal instance links.

## What Changed

- Added a GitHub workflow push preflight so agent runs can detect
missing push credentials when a workflow explicitly needs GitHub
publishing.
- Added shared git workspace sync helpers for shallow HEAD import/export
and dirty/untracked overlay tracking.
- Updated sandbox managed runtime setup to use git history plus a
selected overlay instead of uploading the full local workspace for
git-backed workspaces.
- Bounded sandbox archive upload/download paths so large payloads stream
or chunk instead of materializing one oversized string.
- Excluded `.git` and ignored dependency trees from sandbox upload,
download, and restore baselines while preserving local ignored
directories during sync-back.
- Added focused tests for git workspace sync, sandbox overlay selection,
transfer chunking, and heartbeat push-preflight behavior.

## Verification

- `pnpm exec vitest run
packages/adapter-utils/src/git-workspace-sync.test.ts
packages/adapter-utils/src/sandbox-managed-runtime.test.ts
packages/adapter-utils/src/command-managed-runtime.test.ts
server/src/__tests__/heartbeat-project-env.test.ts
server/src/__tests__/heartbeat-workspace-session.test.ts`
- `pnpm --filter @paperclipai/adapter-utils typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm build`
- Public-hygiene scan of the PR diff and commit messages for internal
issue ids, local paths, private hostnames, and obvious token patterns.

## Risks

- Medium risk: this changes sandbox runtime synchronization semantics
for git-backed workspaces, especially around dirty tracked files,
untracked files, deleted paths, and ignored files.
- The main mitigation is focused test coverage for upload contents,
restore exclusions, and git round-trip behavior.
- The SSH runtime keeps the current bundle-based implementation from
`master`; this PR only aligns shared excludes and sandbox behavior with
that model.

## Model Used

OpenAI Codex, GPT-5-based coding agent, tool-enabled shell/git/GitHub
workflow, with code execution and repository inspection.

## 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] If this change affects the UI, I have included before/after
screenshots
- [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
- [ ] 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>
2026-06-20 22:03:55 -07:00
Devin Foley 33353ce62b feat(skills): remove bundled paperclip-dev skill and retire required skill attribute (#7029)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Local adapters (Claude, Codex, Cursor, Gemini, Grok, OpenCode, Pi,
ACPX) ship bundled "skills" — opinionated Markdown prompt bundles
materialized into the agent's runtime
> - One of those bundled skills, `paperclip-dev`, existed to let agents
develop Paperclip itself; it has now moved to its own external repo and
no longer belongs in the core tree
> - The adapter skill model also carried a `required` / `requiredReason`
attribute plus a `paperclip_required` `AdapterSkillOrigin` variant, all
of which only existed to mark bundled skills as non-optional in the UI
and adapter sync logic
> - With `paperclip-dev` gone, no bundled skill is "required" anymore,
and the type / runtime surface for `required` is dead weight — but it is
computed at request time and never persisted, so a clean removal is safe
(no compatibility shim needed)
> - This pull request deletes `skills/paperclip-dev/` and removes every
trace of the `required` / `requiredReason` field and the
`paperclip_required` origin across shared types, validators,
adapter-utils, all eight local adapters, server routes, the
company-skills service, the UI, the storybook fixtures, and the test
suite
> - The benefit is a smaller, simpler adapter-skill surface: one origin
(`company_managed`) for managed bundled skills,
`resolvePaperclipDesiredSkillNames` collapses to "just the configured
desired set", and the AgentDetail skills tab no longer renders a
"Required by Paperclip" section that no longer applies

## Linked Issues or Issue Description

<!-- No existing public GitHub issue; describing the underlying work
inline (feature_request template fields). -->

**Summary**

Remove the bundled `paperclip-dev` skill (now maintained in its own
external repo) and retire the `required` / `requiredReason` skill
attribute and the `paperclip_required` skill origin, which only existed
to support it.

**Problem or motivation**

`paperclip-dev` is the only bundled skill that was ever marked
"required". Now that it lives in a separate repository, shipping it
inside the core tree is wrong, and the entire `required` surface (a type
field, a validator field, a synthesized `paperclip_required` origin, UI
"Required by Paperclip" section, and required-skill merging in the
desired-skills calculation) becomes dead weight. The `required` value is
computed at request time and never persisted, so it can be removed
cleanly without a migration or compatibility shim.

**Proposed solution**

Delete `skills/paperclip-dev/`, drop the `required` / `requiredReason`
fields and `paperclip_required` origin everywhere they are produced or
consumed, collapse managed-skill origin to a single `company_managed`
value, and simplify `resolvePaperclipDesiredSkillNames` to return only
the configured desired set.

**Alternatives considered**

Keeping the `required` attribute as a no-op for forward compatibility —
rejected because it is request-time only (nothing persists it), so
leaving it in place is pure dead surface area with no callers.

**Roadmap alignment**

Internal cleanup / dead-code removal that simplifies the adapter-skill
surface; it does not introduce or duplicate any planned core feature in
ROADMAP.md.

## What Changed

- Deleted bundled `skills/paperclip-dev/` (moved to a separate repo).
- Dropped `required`, `requiredReason`, and the `paperclip_required`
origin from `packages/shared/src/types/adapter-skills.ts`,
`packages/shared/src/validators/adapter-skills.ts`, and
`packages/adapter-utils/src/types.ts`.
- In `packages/adapter-utils/src/server-utils.ts`: removed
`readSkillRequired()`; dropped `required`/`requiredReason` from
`listPaperclipSkillEntries()`,
`normalizeConfiguredPaperclipRuntimeSkills()`,
`buildPersistentSkillSnapshot()`, and `PaperclipSkillEntry`; collapsed
`buildManagedSkillOrigin()` to always return `company_managed`;
simplified `resolvePaperclipDesiredSkillNames()` to return only the
configured desired set (signature preserved so adapter call sites are
untouched).
- Walked all eight local adapters (`acpx-local`, `claude-local`,
`codex-local`, `cursor-local`, `gemini-local`, `grok-local`,
`opencode-local`, `pi-local`) and removed every remaining
`requiredReason` / `paperclip_required` reference.
- `server/src/services/company-skills.ts`: dropped the `required =
sourceKind === "paperclip_bundled"` synthesis when listing runtime skill
entries.
- `server/src/routes/agents.ts`: removed required-skill merging from the
desired-skills calculation in the persist-config path and the
unsupported-snapshot path (keeping the current version-aware
`desiredSkillEntries` structure).
- `ui/src/pages/AgentDetail.tsx`: dropped required-based filters, the
required tooltip, and the entire "Required by Paperclip" section from
the agent skills tab; storybook fixtures in
`ui/storybook/stories/acpx-local.stories.tsx` cleaned up to match.
- Tests: deleted the `required: false` case in
`paperclip-skill-utils.test.ts` and the "keeps required bundled skills
installed" case in every `*-local-skill-sync.test.ts`;
`acpx-local-execute.test.ts`, `cursor-local-execute.test.ts`,
`cursor-local-skill-sync.test.ts`, `agent-skills-routes.test.ts`, and
`packages/adapter-utils/src/server-utils.test.ts` were updated to drop
removed fields and map `origin: "paperclip_required"` →
`"company_managed"`.
- `server/src/adapters/registry.ts`: two `as unknown as
ServerAdapterModule["..."]` casts on `hermesListSkills` /
`hermesSyncSkills` (matching the existing `executeHermesLocal` pattern).
`hermes-paperclip-adapter@0.2.0` still depends on the published
`@paperclipai/adapter-utils` which keeps the retired
`paperclip_required` variant; the cast bridges the
workspace-vs-published type mismatch at the registry seam and can drop
once hermes upgrades.

## Verification

Run from the workspace root:

```sh
grep -rn "skills/paperclip-dev" .
grep -rn "paperclip_required" --include="*.ts" --include="*.tsx" .
grep -rn "requiredReason" --include="*.ts" --include="*.tsx" .

pnpm -w typecheck
pnpm --filter @paperclipai/server exec vitest run paperclip-skill-utils
pnpm --filter @paperclipai/server exec vitest run skill-sync
```

The first three greps return only the explanatory comment in
`server/src/adapters/registry.ts` (no live `paperclip_required` /
`requiredReason` usage) and zero `skills/paperclip-dev` source hits.

Locally:

- `pnpm -w typecheck` → all packages this PR touches pass
(adapter-utils, shared, server, ui, cli, and the
cursor/gemini/opencode/pi adapters).
- Affected vitest suites pass: `paperclip-skill-utils`, `server-utils`,
all eight `*-local-skill-sync`, `agent-skills-routes`, and the
`acpx`/`cursor`/`pi` execute suites.

## Risks

- Behavioral shift in the agent skills UI: the "Required by Paperclip"
section disappears. No bundled skill is required anymore, so this only
affects environments that previously surfaced `paperclip-dev` as a
forced-on row; those installs will see the skill move into the regular
"company-managed" list (and be uninstalled on next sync unless
explicitly listed as desired).
- Existing agents may still have the string `"paperclip-dev"` in their
persisted `desiredSkills`. That entry is inert (no source for it to
install from); a one-time DB cleanup is out of scope. Low risk.
- Hermes adapter type bridge: two casts in `registry.ts` paper over a
type-only divergence between the workspace `@paperclipai/adapter-utils`
and the published version still pinned by
`hermes-paperclip-adapter@0.2.0`. Runtime behavior is unaffected because
the retired `paperclip_required` value is no longer produced by anything
in this tree. The casts can be removed once hermes upgrades its
dependency.

## Model Used

- Provider: Anthropic
- Model: Claude Opus 4.7 (`claude-opus-4-7`)
- Capability: agent tool use via Paperclip's `claude_local` adapter

## 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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-06-20 21:58:44 -07:00
Devin Foley 5ebbe67ebd fix(workspace-runtime): base fresh worktrees on origin/master and refresh unstarted reuses (#8412)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agent work runs inside git worktrees created by the workspace
runtime, each branched from a configured base ref (typically the repo's
default branch)
> - When the local `master` is stale or ahead of `origin/master`
(committed but unpushed work, or local-only commits), a freshly created
worktree inherits that divergence — so an unrelated task branch silently
carries commits it never intended to touch
> - This surfaced as a docs-only task whose PR accidentally pulled in
unrelated changes from a diverged local master
> - The base for a fresh worktree should be resolved authoritatively to
the remote-tracking ref (`origin/<branch>`), and an idle/unstarted
reused worktree should be safely fast-forwarded — without ever
destroying in-progress work
> - This pull request makes both behaviors explicit in the workspace
runtime
> - The benefit is that task branches start from a clean, authoritative
base, eliminating accidental inclusion of unrelated local changes

## Linked Issues or Issue Description

This is a bug fix. No public GitHub issue exists, so describing it
inline following the Bug Report template:

**What happened**

A task intended to change only docs produced a PR that also contained
unrelated changes pulled in from `master`.

The root cause: when a new worktree is created from a configured local
branch (e.g. `master`), the worktree inherits whatever that local branch
points at. If the local `master` has committed divergence from
`origin/master` (unpushed or local-only commits), that divergence leaks
into the new task branch. The leak comes from committed
local-vs-`origin/master` ref drift, not uncommitted working-tree changes
(each worktree has its own working tree).

**Expected behavior**

A fresh worktree should be based on the authoritative `origin/master`
head so unrelated local commits never seed a task branch.

**Steps to reproduce**

1. Have a local `master` that is ahead of `origin/master` (committed but
unpushed work).
2. Create a new worktree/task branched from `master` via the workspace
runtime.
3. Open a PR from that branch — it carries the unrelated local commits.

**Deployment mode**

Self-hosted / local workspace runtime.

## What Changed

- Fresh worktrees now resolve their base ref authoritatively: a
configured local branch (e.g. `"master"`) is mapped to its
`origin/<branch>` remote-tracking counterpart so unpushed/ahead local
commits can never seed a task branch. Remote-tracking refs, SHAs, and
tags are used verbatim; an unset/`HEAD` base falls back to the detected
default branch. The resolved ref is recorded (`repoRef`) so downstream
drift checks stay accurate.
- If a configured local branch has no matching `origin/<branch>`, the
runtime warns and falls back to the local ref rather than failing.
- On reuse, a *provably unstarted* worktree (no commits past base +
clean tree including untracked files) is fast-forwarded to the latest
`origin/master`. Started or dirty worktrees keep the prior warn-only
behavior, so in-progress work is never reset. Only remote-tracking bases
are eligible for the refresh.

## Verification

- `cd server && npx vitest run src/__tests__/workspace-runtime.test.ts`
- 5 new tests cover: local-branch→`origin/<branch>` mapping, no-remote
fallback warning, unstarted-reuse fast-forward, and that started/dirty
worktrees are left untouched.
- Result: 65 passed. 1 pre-existing failure (`auto-detects the default
branch via symbolic-ref when origin/HEAD is set`) is unrelated to this
change and fails only due to the test host's git default-branch config
(test setup runs `git push -u origin main master` but the local default
branch is `main`); it also fails on `master`.

## Risks

- Low risk. The refresh path is intentionally conservative: it only
fast-forwards worktrees that are provably unstarted (zero commits past
base and a fully clean tree, including untracked files) and only when
the base is a remote-tracking ref. Started or dirty worktrees fall
through to the existing warn-only drift behavior, so no in-progress work
can be destroyed.
- Behavioral shift: fresh worktrees configured against a local branch
will now base on `origin/<branch>` instead of the local ref. This is the
intended fix; the only case it changes is when local and remote have
diverged.

## Model Used

Claude — `claude-opus-4-8` (extended thinking, tool use / code execution
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
- [ ] If this change affects the UI, I have included before/after
screenshots (N/A — no UI changes)
- [x] I have updated relevant documentation to reflect my changes (N/A —
no doc changes 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
2026-06-20 18:51:28 -07:00
Devin Foley 67ca8287e8 fix(codex-local): seed managed auth into isolated homes (#8403)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The `codex_local` adapter isolates local Codex runs by assigning
managed `CODEX_HOME` state per company and per agent
> - PR #8272 tightened that isolation, but it left a gap: once
`CODEX_HOME` became explicit, the adapter treated it like a user-managed
override and skipped auth seeding
> - That meant newly isolated agents could launch with no usable
`auth.json`, hit OpenAI unauthenticated, and fail with `401 Missing
bearer`
> - Users who had already persisted one of those broken managed homes
could remain stranded even after config changes unless Paperclip
repaired the home itself
> - This pull request teaches Paperclip to seed managed homes correctly,
backfill already-stranded managed homes on startup, and reject
credential-less managed homes before they reach the provider
> - The benefit is that affected managed `codex_local` agents recover
automatically after upgrade and restart, without manual `CODEX_HOME`
surgery

## Linked Issues or Issue Description

- Fixes #497
- Refs #5028
- Related PR: #8272
- Related PR: #8399

## What Changed

- Distinguished Paperclip-managed `CODEX_HOME` paths from genuine
external overrides and always seeded auth into managed homes, even when
`CODEX_HOME` is explicit in config.
- Wrote API-key-backed `auth.json` files for managed homes when
`OPENAI_API_KEY` is configured, otherwise symlinked the shared Codex
auth for subscription/OAuth flows.
- Added a startup reconciliation pass that backfills already-isolated
managed homes created by the broken release so upgrade plus restart
repairs stranded agents automatically.
- Preserved previously resolved API-key auth when the stored
`OPENAI_API_KEY` binding is secret-backed and startup cannot resolve the
secret value directly.
- Hardened the managed-home preflight to require a credential-bearing
`auth.json`, not just file presence, and documented the recovery
behavior.
- Added regression tests covering managed-home seeding, fail-fast
behavior, and server-side startup reconciliation.

## Verification

```bash
pnpm exec vitest run packages/adapters/codex-local/src/server/codex-home.test.ts packages/adapters/codex-local/src/server/execute.auth.test.ts server/src/__tests__/codex-auth-reconciliation.test.ts server/src/__tests__/codex-local-execute.test.ts server/src/__tests__/server-startup-feedback-export.test.ts
pnpm --filter @paperclipai/adapter-codex-local typecheck
pnpm --filter @paperclipai/server typecheck
pnpm check:tokens
pnpm exec vitest run server/src/__tests__/server-startup-feedback-export.test.ts
```

GitHub Actions `PR` workflow is green on latest head `e5b1e08d4`,
including policy, typecheck, test shards, build, e2e, serialized server
suites, and canary dry run. Greptile Review is green on latest head with
0 comments added. No UI changes.

## Risks

- Startup reconciliation now mutates persisted managed Codex homes at
boot. Risk is low because it only touches Paperclip-managed
company/agent home paths and no-ops when a home already has usable auth.
- Genuine external `CODEX_HOME` overrides remain intentionally
self-managed, so those users still own repair steps inside their custom
home.
- Hosts with neither shared Codex auth nor an explicit per-agent API key
now fail earlier with a clearer adapter error instead of surfacing a
downstream `401`, which changes timing but not capability.

## Model Used

- OpenAI Codex, GPT-5-based coding agent in a local Codex session; exact
served model ID/context window were not exposed to the session. 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 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] If this change affects the UI, I have included before/after
screenshots
- [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>
2026-06-20 16:17:54 -07:00
Neeraj Kumar Singh B d5ceb82571 Prefer loopback runtime API URL for local agents (#5102)
## Thinking Path

> - Paperclip is the open-source app that manages AI agents and their
work coordination
> - The server subsystem exposes a control-plane HTTP API that agents
read and write task state against at runtime
> - On local development machines, Paperclip binds to loopback
(`127.0.0.1:3100`) but also advertises LAN hostnames via
`allowedHostnames` for multi-device setups
> - Agents inherit their API URL from `PAPERCLIP_API_URL` /
`PAPERCLIP_RUNTIME_API_URL` env vars exported during server startup
> - `choosePrimaryRuntimeApiUrl` was selecting the first entry in
`allowedHostnames` (the LAN IP) before the loopback bind host, so agents
on the same machine tried to connect to an unreachable LAN address
(NEE-327)
> - This PR fixes the chooser to return the normalized loopback bind
host first, before considering LAN `allowedHostnames`
> - The benefit is that local agents reliably reach the control plane
regardless of `allowedHostnames` configuration

## What's going on

Local Paperclip agents were sometimes inheriting
`PAPERCLIP_API_URL=http://192.168.1.50:3100` even when the server was
bound to loopback, which made the control plane unreachable from this
workspace. This keeps the runtime API on the loopback bind host for
local startup while still preserving LAN candidates for other callers.

## Problem

`choosePrimaryRuntimeApiUrl` preferred the first allowed hostname over
the actual loopback bind host. In the failing setup from NEE-327, that
exported `http://192.168.1.50:3100` into agent env even though
`http://127.0.0.1:3100` was the reachable control-plane URL.

## Solution

The primary runtime URL chooser now returns the normalized loopback bind
host before considering `allowedHostnames`. I added a focused unit test
for the chooser and a startup regression that verifies
`PAPERCLIP_RUNTIME_API_URL` / `PAPERCLIP_API_URL` stay on `127.0.0.1`
while the candidate list still includes the LAN hostname.

## Testing

- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/runtime-api.test.ts
src/__tests__/server-startup-feedback-export.test.ts`
- Result: `2` files passed, `13` tests passed
- Runtime check: `curl -sS http://127.0.0.1:3100/api/health` returned
`{"status":"ok",...}` during this heartbeat

## Notes

Already-running local Paperclip servers need a restart to export the
corrected `PAPERCLIP_API_URL` into new agent runs. Rollback is revert
`df8d7fcf`.

## Model Used

Claude Sonnet 4.6 (`claude-sonnet-4-6`), 1M context window, via the
Paperclip Founding Engineer agent harness. Capabilities used: extended
tool use, code editing, test execution, shell commands.

## 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
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] 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: Neeraj Kumar Singh <b.nirajkumarsingh@hotmail.com>
2026-06-20 14:03:21 -07:00
Devin Foley c0743482bc fix(claude-local): tolerate sandboxes whose Claude CLI lacks --effort (#8393)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The claude-local adapter launches the Claude CLI inside execution
environments (local, SSH, and ephemeral sandboxes such as Daytona)
> - Newer adapter code passes `--effort` to the CLI, but the Claude
binary baked into some sandbox images is older and rejects it with
`error: unknown option '--effort'`, so every run in those environments
fails
> - This needs addressing because the failure is environment-dependent
and silent from the operator's perspective — the run just dies with a
CLI usage error
> - This pull request probes the in-sandbox CLI for `--effort` support
once, caches the result per environment, and strips the flag (with a
warning) when the CLI does not support it
> - The benefit is that sandboxes with older Claude CLIs keep working
instead of failing, with negligible probe overhead because the
capability check is cached and reused across ephemeral leases

## Linked Issues or Issue Description

No public GitHub issue exists. Describing the bug inline following the
bug report template:

### What happened?

Runs using the `claude-local` adapter inside certain sandbox/execution
environments fail with `error: unknown option '--effort'`. The adapter
unconditionally appends `--effort` to the Claude CLI invocation, but the
Claude CLI version present in some sandbox base images predates that
flag, so the process exits with a usage error and the run dies.

### Expected behavior

The adapter should detect that the target environment's Claude CLI does
not support `--effort` and degrade gracefully — drop the flag and emit a
warning — rather than failing the run.

### Steps to reproduce

1. Configure an execution environment (e.g. a sandbox image) whose
bundled Claude CLI is old enough to predate the `--effort` option.
2. Run any claude-local task that resolves to an effort level (so
`--effort` is appended).
3. Observe the run fail immediately with `error: unknown option
'--effort'`.

### Paperclip version or commit

`master` at the time of this PR (branch forked from current `master`).

### Deployment mode

Self-hosted / local instance using execution environments (reproducible
with ephemeral sandbox providers such as Daytona where `reuseLease:
false`).

### Agent adapter(s) involved

Claude Code (`claude-local`).

## What Changed

- Add a CLI capability probe (`cli-capabilities.ts`) that runs the
target Claude binary's `--help` inside the execution environment to
detect `--effort` support.
- Strip `--effort` from the CLI args (emitting a warning) when the probe
reports the flag is unsupported; keep it otherwise.
- Cache probe results keyed by
`sandbox:providerKey:environmentId:command` (no lease id) so the probe
is reused across ephemeral leases — important for `reuseLease: false`
sandbox configs like Daytona, which would otherwise re-probe on every
run.
- Conservative fallback: if the probe itself can't run/parse, assume the
flag is supported (preserves prior behavior).

## Verification

- `node_modules/.bin/vitest run
src/__tests__/claude-local-execute.test.ts
src/__tests__/claude-local-adapter-environment.test.ts` → **2 files, 30
tests passed**.
- Regression test issues two `execute()` calls with distinct lease ids
and asserts the in-sandbox `--help` probe runs exactly once (cache reuse
across leases).
- Added tests covering: flag stripped when unsupported, flag retained
when supported, warning emitted, and conservative fallback when the
probe fails.

## Risks

Low risk. The change is additive and gated behind a probe with a
conservative default (assume supported on probe failure), so existing
environments that support `--effort` are unaffected. Worst case for an
environment where the probe is unreliable is the prior behavior (flag
passed through).

## Model Used

Claude Opus 4.8 (claude-opus-4-8), extended thinking, with tool use /
code execution 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
- [ ] If this change affects the UI, I have included before/after
screenshots (N/A — no UI change)
- [ ] I have updated relevant documentation to reflect my changes (N/A —
no doc-facing behavior change)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (pending CI)
- [ ] 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
2026-06-20 13:05:10 -07:00
Devin Foley 07e98d2b2c feat(adapter-utils): add observable sandbox sync progress (#8395)
## Thinking Path

> - Paperclip is the control plane for running AI-agent companies, so
long-running remote work needs to stay observable to human operators.
> - Cloud / sandbox agents are an active roadmap area, and their
workspace sync path is part of the runtime substrate every remote coding
run depends on.
> - In the sandbox and SSH execution-target flows, Paperclip logged that
sync had started, then often went silent for the full transfer window.
> - That made large remote syncs feel stalled and also hid a real
performance problem in the command-managed sandbox upload path.
> - The first part of this pull request threads a throttled
progress-reporting surface through the adapter execution-target stack so
sync and restore work can emit meaningful updates.
> - The second part fixes the command-managed sandbox transport itself:
it removes the old serial 32KB append bottleneck, but also falls back
away from the single-stream path when a provider-backed sandbox runner
cannot surface mid-flight stdin progress.
> - The result is that sandbox and SSH transfers are both faster and
more observable, including the live Daytona-style sandbox case that
previously only emitted `0%` and `100%`.

## Linked Issues or Issue Description

No public GitHub issue exists for this bug, so it is described inline
below following the bug report template.

### What happened

- Remote sandbox and SSH workspace syncs could spend a long time
transferring data while only logging a start line (`Syncing workspace
and runtime assets to sandbox environment`) and, at best, a terminal
line.
- In the command-managed sandbox path, the original upload
implementation also paid a large performance cost by appending base64
data in many small sequential remote writes (thousands of serial 32KB
round-trips on a large workspace).
- After the initial transport rewrite, live provider-backed sandbox runs
still only emitted `0%` and `100%` because the single-stream stdin RPC
buffered progress until completion.

### Expected behavior

- Long-running sandbox and SSH syncs should periodically report how much
of the transfer is complete (a percentage and/or MB transferred) so an
operator can tell the run is healthy and making progress rather than
stuck.
- The main sandbox upload path should not be artificially slow.
- A transfer that fails partway should leave an explicit failure marker
in the log rather than a dangling intermediate percentage.

### Steps to reproduce

1. Run an agent against a sandbox (command-managed) or SSH
(remote-managed) execution target with a non-trivial workspace.
2. Watch the run log during the workspace/runtime asset sync phase.
3. Observe that the log shows the sync start line and then stays silent
for the full transfer (live provider-backed sandbox runs only show `0%`
then `100%`).

### Paperclip version or commit

- Branch `PAPA-825-provide-status-updates-when-syncing-sandboxes` off
`master`.

### Deployment mode

- Self-hosted / local instance using sandbox (command-managed) and SSH
(remote-managed) execution targets, including provider-backed sandbox
runners.

## What Changed

- Added shared throttled runtime progress reporting and threaded
`onProgress` through the adapter execution-target surface and adapter
`execute.ts` entrypoints.
- Added sync and restore progress reporting for the command-managed
sandbox path and the SSH/remote-managed path, including git
import/export progress where totals are known.
- Reworked command-managed sandbox transfer behavior so uploads use the
faster single-stream path when appropriate, but fall back to chunked
progress-emitting writes when the runner cannot expose mid-stream stdin
progress.
- Marked provider-backed environment sandbox runners as not supporting
single-stream stdin progress so live sandbox runs emit meaningful
intermediate updates instead of only `0%` and `100%`.
- Emit an explicit terminal failure marker (`failed at NN% (x/y MB)`)
when an SSH/tar transfer rejects, so a failed sync no longer leaves a
dangling intermediate percentage in the log.
- Run the SSH sync/restore size estimate (local directory walk / remote
`du` probe) concurrently with the transfer instead of awaiting it before
opening the pipe, so progress instrumentation no longer adds startup
latency proportional to workspace file count.
- Added and extended focused regression coverage for runtime progress
throttling and the new failure marker, command-managed sandbox
transfers, sandbox orchestration, SSH transfer progress, and environment
execution-target wiring.

## Verification

- `pnpm exec vitest run
packages/adapter-utils/src/runtime-progress.test.ts
packages/adapter-utils/src/ssh-fixture.test.ts
packages/adapter-utils/src/command-managed-runtime.test.ts
packages/adapter-utils/src/sandbox-managed-runtime.test.ts`
- `pnpm exec vitest run
packages/adapter-utils/src/command-managed-runtime.test.ts
server/src/__tests__/environment-execution-target.test.ts`
- `npx tsc --noEmit` for `packages/adapter-utils`

## Risks

- The provider-backed sandbox fallback now prefers chunked
command-managed writes when progress hooks are active, so
small-to-medium uploads may trade some raw throughput for observable
intermediate progress on runtimes that cannot surface true mid-stream
stdin progress.
- Progress percentages on tar-based transfers still depend on estimates
in some cases, so operators may briefly see MB-only lines before the
estimate resolves, then near-final clamping before the terminal `100%`
line.
- This PR changes shared execution-target behavior used by multiple
adapters, so regressions would most likely appear in remote runtime
setup/teardown flows rather than in a single adapter.

## Model Used

- Initial implementation: OpenAI GPT-5.4 via Codex local agent
(`codex_local`), high reasoning mode.
- Observability follow-ups (failure marker, concurrent size estimate,
added tests): Claude Opus 4.8 via Claude Code (`claude_local`).

## 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] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] 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: Paperclip <noreply@paperclip.ing>
2026-06-20 13:03:42 -07:00
Aron Prins e93d78b46c fix(server): harden live events upgrade sockets (#8383)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server exposes live event updates to board and agent clients
over WebSocket upgrade requests
> - WebSocket upgrade authorization can be asynchronous, leaving the raw
HTTP upgrade socket in server-owned code before `ws` takes over
> - If the client disconnects during that authorization window, the
server can still try to reject or upgrade a closed socket
> - A raw socket write after peer disconnect can emit `EPIPE` /
`ECONNRESET`, and without a listener that can become process-fatal
> - This pull request hardens the pre-`ws` upgrade socket path and adds
regression coverage for disconnect/error races
> - The benefit is that live event reconnect churn degrades gracefully
instead of risking a server crash

## Linked Issues or Issue Description

External public context: Refs
https://github.com/aronprins/paperclip-desktop/issues/14

No matching open upstream issue or PR was found when searching
`paperclipai/paperclip` for `EPIPE`, `rejectUpgrade`, `live-events-ws`,
`websocket upgrade`, and related socket-write terms.

### Bug report

**What happened?**

The live events WebSocket upgrade handler could write a rejection
response to the raw upgrade socket after the peer had already
disconnected during async authorization. A transport-level `EPIPE` or
similar socket error on that raw socket can terminate the server process
if it is emitted without an error listener.

**Expected behavior**

The server should not write to destroyed/non-writable upgrade sockets,
should tolerate raw socket errors while authorization is pending, and
should not call `handleUpgrade()` after the socket is no longer
writable.

**Steps to reproduce**

1. Start a Paperclip server with live events enabled.
2. Open a raw WebSocket upgrade request to
`/api/companies/:companyId/events/ws`.
3. Disconnect the client before async authorization resolves.
4. Let the server take the reject or upgrade path.
5. Observe that the pre-fix path can still write to or upgrade a closed
socket.

**Paperclip version or commit**

Reproduced by static inspection on `master` before this PR at
`950484d20`.

**Deployment mode**

Any mode using live event WebSocket upgrades. The report was originally
observed from local Desktop embedding, but the vulnerable code is in the
server package.

## What Changed

- Added a writable-state guard for raw upgrade sockets before rejecting
or completing an upgrade.
- Changed rejection responses from raw `write()` + `destroy()` to
guarded `end()` with warning logging if rejection fails synchronously.
- Attached a temporary raw socket error listener during the async
upgrade authorization window and cleaned it up on socket close or
successful `ws.handleUpgrade()`.
- Added regression tests for rejecting after an early socket close and
for handling raw socket `error` events while authorization is pending.

## Verification

- `pnpm exec vitest run server/src/__tests__/live-events-ws.test.ts`
- `pnpm --filter @paperclipai/server typecheck`
- `git diff --check`

## Risks

- Low risk: the change is scoped to the live-events WebSocket upgrade
path before `ws` takes ownership of the socket.
- Rejected upgrade responses now use graceful `socket.end(...)`; clients
should still receive the same HTTP status text when the socket is
writable.
- The temporary error listener is removed before successful
`handleUpgrade()` so normal WebSocket client error handling remains
owned by the existing `connection` path.

> 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 coding agent with repository-aware tool use, shell
command execution, GitHub connector access, and local code editing.

## 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] If this change affects the UI, I have included before/after
screenshots
- [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: Paperclip <noreply@paperclip.ing>
2026-06-20 11:32:49 -07:00
Devin Foley 547463d3a2 refactor(environments): make execution environments instance-scoped (#8375)
## Thinking Path

> - Paperclip is the control plane for AI-agent companies, so execution
environment selection has to stay inspectable and predictable across
companies, agents, and runs.
> - The environment subsystem decides where an agent heartbeat actually
runs and how remote sandbox state is realized and restored.
> - That subsystem previously mixed company-scoped environment catalogs
with issue-level environment stamping, so a reassigned issue could keep
executing in the previous assignee's sandbox.
> - That behavior breaks the control-plane contract: changing the
assignee should change the executing agent/environment path unless there
is an explicit current override.
> - Fixing it cleanly required more than a narrow patch; the environment
model had to move to instance scope with a single inherited default and
per-agent override semantics.
> - This pull request rewires the schema, server/API surface, runtime
resolution, and UI around that model, then adds regression coverage for
cross-company inheritance and per-agent isolation.
> - The benefit is that environment choice now follows the approved
instance/agent configuration path instead of stale issue state, while
shared environments only need to be configured once per instance.

## Linked Issues or Issue Description

- No directly matching public GitHub issue or PR was found while
searching for this refactor.

### What happened?

Reassigning work between agents with different execution environments
could keep running in the previous sandbox because environment choice
was stamped onto the issue and outranked the current assignee. The same
subsystem also forced environment catalogs to be duplicated per company
even though the underlying execution environments were instance-wide
resources.

### Expected behavior

Execution should resolve through the current instance and agent
configuration path, with one instance-scoped environment catalog, one
instance default, optional per-agent override, and no stale issue-level
environment authority surviving reassignment.

### Steps to reproduce

1. Configure two agents to use different execution environments.
2. Assign an issue to the first agent so the issue records execution
state in that environment.
3. Reassign the same issue to the second agent and run another
heartbeat.
4. Observe that the pre-fix runtime can still sync or execute in the
original sandbox instead of the second agent's environment.

### Paperclip version or commit

Current `master` before this PR.

### Deployment mode

Self-hosted server.

### Installation method

Built from source (`pnpm dev` / `pnpm build`).

### Agent adapter(s) involved

- Claude Code
- Not adapter-specific (core bug in environment authority / resolution)

### Database mode

External Postgres.

### Access context

Both board reassignment and agent heartbeats were involved.

## What Changed

- Moved environments and their default selection contract to instance
scope in DB/shared types, including the migration that dedupes legacy
per-company environments and seeds the instance local default.
- Reworked environment CRUD/auth flows to use instance-scoped APIs and
added route/service coverage for instance-level environment management.
- Changed runtime resolution to prefer `agent default -> instance
default -> built-in local`, removed issue-level environment stamping
from the active execution path, and isolated sandbox/plugin leases by
`(executionWorkspaceId, agentId)`.
- Added environment env-var runtime precedence so environment-provided
values act as the baseline for agent execution.
- Moved the environment UI into instance settings and updated agent
configuration surfaces to reflect inherit/override behavior.
- Added regression coverage for instance-default inheritance across
companies and for the new runtime resolution behavior.
- Fixed a rebase-only duplicate `enableTaskWatchdogs` flag regression in
instance settings types/validators/services so the branch typechecks
cleanly on current `master`.
- Updated stale server tests so CI matches the shipped instance-scoped
environment contract.

## Verification

- `git diff --check`
- `pnpm --filter @paperclipai/shared typecheck`
- `pnpm --filter @paperclipai/db typecheck`
- `pnpm exec vitest run
server/src/__tests__/environment-runtime-driver-contract.test.ts
server/src/__tests__/agent-permissions-routes.test.ts
server/src/__tests__/environment-routes.test.ts
server/src/__tests__/environment-instance-routes.test.ts
server/src/__tests__/execution-workspace-policy.test.ts
server/src/__tests__/heartbeat-plugin-environment.test.ts
server/src/__tests__/instance-settings-routes.test.ts`

## Risks

- The migration changes environment scope and dedupes existing rows, so
installs with unusual legacy environment combinations should be reviewed
carefully during upgrade.
- Remote execution behavior now depends on instance-default inheritance
semantics instead of issue-level stamping, so any remaining code paths
that still assume issue-scoped environment authority would surface as
follow-up bugs.
- This PR includes both server/runtime behavior and UI relocation, so
reviewers should watch for authorization edge cases around instance
settings and environment management.

> I checked [`ROADMAP.md`](ROADMAP.md). This work fits the existing
Cloud / Sandbox agents direction as a bug-fix/refactor to current
behavior, not a new parallel product surface.

## Model Used

- OpenAI Codex coding agent in this Paperclip/Codex session; GPT-5-class
tool-using model with code execution and shell access. The exact backend
model ID is not exposed to the session 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
- [ ] If this change affects the UI, I have included before/after
screenshots
- [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
2026-06-20 09:42:53 -07:00
levineam 631b7806ed Add heartbeat preflight budget caps (#8347)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Heartbeats are the control-plane path that turns scheduled,
comment-driven, or on-demand wakeups into adapter executions.
> - Budgeting and recurring work need enforcement before an adapter
starts, not only after model usage is recorded.
> - Empty timer wakes also need an opt-in fast-exit path so operators
can keep routine schedules without paying for no-op model turns.
> - This pull request adds heartbeat preflight gates for daily run and
daily cost caps, plus an explicit timer no-work skip policy.
> - The benefit is safer autonomous operation: capped agents stop before
new execution, queued work is cancelled cleanly at claim time, and
proactive agents still run by default unless the operator opts into
no-work skipping.

## Linked Issues or Issue Description

No public issue exists for this change. Inline bug report:

### What happened?

Heartbeat execution can start without enforcing per-agent daily
invocation and spend limits at the heartbeat boundary. A run that was
queued before a cap was reached can also be claimed later and invoke the
adapter unless the cap is checked again immediately before execution.
Operators also do not have an explicit opt-in fast-exit policy for
generic timer wakes with no actionable assigned work.

### Expected behavior

Configured daily run and daily cost caps should stop new heartbeat runs
before adapter execution. Already queued runs should be rechecked at
claim time and cancelled cleanly when a cap is now reached. Queued issue
runs cancelled by daily caps should release their issue execution locks
and promote deferred wakeups without entering immediate recovery loops.
Generic timer no-work skipping should be opt-in so proactive agents
continue to run by default.

### Steps to reproduce

1. Configure an agent heartbeat policy with a one-run daily cap or a
daily cost cap.
2. Create or queue heartbeat wakeups for that agent after the cap has
already been consumed.
3. Observe that without preflight and claim-time checks, the heartbeat
path can still enqueue or claim work that should be blocked before
adapter execution.

### Paperclip version or commit

Reproduced against `master` before this branch.

### Deployment mode

Local dev (`pnpm dev`) / built from source.

### Installation method

Built from source (`pnpm dev` / `pnpm build`).

### Agent adapter(s) involved

Not adapter-specific (core heartbeat scheduling and claim logic).

### Database mode

External Postgres in tests via embedded test harness.

### Access context

Not applicable.

### Node.js version

Node 20 in CI-compatible local development.

### Operating system

macOS local development, Linux CI-compatible tests.

### Relevant logs or output

The regression suite added in this PR covers the failing paths:

```shell
pnpm exec vitest run server/src/__tests__/heartbeat-stale-queue-invalidation.test.ts
```

### Relevant config (if applicable)

```json
{
  "heartbeat": {
    "maxDailyRuns": 1,
    "maxDailyCostCents": 1,
    "skipTimerWhenNoActionableWork": true
  }
}
```

### Additional context

This affects recurring/autonomous operation because the safest place to
stop excess work is before adapter execution starts.

### Privacy checklist

Reviewed for sensitive data; no private logs, credentials, or local
instance URLs are included.

## What Changed

- Added heartbeat policy parsing for per-agent daily run caps, daily
cost caps, and opt-in no-actionable-work timer skipping.
- Added pre-queue daily cap checks while preserving same-issue wake
coalescing.
- Added claim-time cap checks so already queued runs are cancelled
before adapter execution when a cap is reached.
- Added skipped wakeup metadata for cap and timer fast-exit decisions.
- Released issue execution locks for queued issue runs cancelled by
daily caps, with deferred wake promotion and without immediate recovery
loops while caps are active.
- Added regression coverage for timer skipping, proactive default
behavior, run caps, cost caps, queued-run cancellation, started
cancelled runs, and deferred issue wake promotion.

## Verification

- `git diff --check`
- `node -c server/src/services/heartbeat.ts`
- `pnpm exec vitest run
server/src/__tests__/heartbeat-stale-queue-invalidation.test.ts`
- `pnpm --filter @paperclipai/server typecheck`
- Local autoreview: `skills/autoreview/scripts/autoreview --mode branch
--base origin/master --engine codex --model gpt-5.5 --thinking high`
  - Result: clean, no accepted/actionable findings

## Risks

- Medium operational risk because this changes heartbeat scheduling and
claim-time behavior.
- The no-actionable-work timer fast-exit is explicitly opt-in to avoid
suppressing proactive agents unexpectedly.
- Daily run caps count runs by `startedAt` so old queued rows do not
consume today’s cap, while started runs still count even if they later
end as cancelled.
- Queued issue-run cap cancellation uses the existing release/promotion
path with immediate recovery suppressed to avoid retry loops while caps
are active.

## Model Used

Codex with GPT-5.5 high reasoning assisted with implementation, local
testing, and autoreview. The final review gate used local autoreview
with `gpt-5.5` high reasoning.

## 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] If this change affects the UI, I have included before/after
screenshots
- [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
2026-06-19 21:39:50 -07:00
Devin Foley 277a9a43d6 fix(recovery): convert review-parked continuations into dependency waits (#8371)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The productivity-recovery subsystem watches for "stranded" assigned
issues — claims whose live run disappeared — and repairs, resumes, or
visibly blocks them
> - When an executor decomposes an umbrella issue into sub-tasks, it
parks its own continuation as "waiting on review/approval" (error code
`issue_continuation_waiting_on_review`) — a deliberate pause, not a lost
run
> - Recovery's staleness gate mistook that deliberate park for a
disappeared run: it retried once, then escalated the issue to `blocked`
with a recovery action and an operator-facing failure notice — even
though nothing had failed and there was nothing for a human to do
> - The user is left staring at an inscrutable, over-technical
"stranded" error on a task they did nothing wrong with, with no idea
what action to take
> - This pull request teaches recovery to recognize a review-parked
continuation and, when the issue has a real waiting target (open
sub-tasks or unresolved blockers), convert it into a first-class
dependency wait: `blocked`-by-children, original assignee kept, plus a
plain-language comment saying it will resume automatically
> - The benefit is that post-decomposition umbrellas sit on a real
waiting path and self-resume through the normal blockers-resolved flow,
while genuine strands (no waiting target) still escalate exactly as
before

## Linked Issues or Issue Description

Refs #6503

## What Changed

- `server/src/services/recovery/service.ts`: add
`resolveContinuationWaitingOnReview`. When a continuation was cancelled
with `issue_continuation_waiting_on_review` and the issue has a real
waiting target — open (non-terminal) sub-tasks or existing unresolved
blockers — recovery sets the issue `blocked` by those issues, keeps the
original assignee, posts a plain-language `system` comment, and logs the
activity. Wired into `reconcileStrandedAssignedIssues` ahead of the
escalation path, with a new `waitingOnReviewResolved` counter on the
result.
- With no waiting target, the code falls through to the existing
escalation, preserving genuine stranded-run detection.
- `server/src/__tests__/heartbeat-process-recovery.test.ts`: two new
tests — (1) a review-parked continuation converts into a dependency wait
on its open sub-tasks (done children excluded, no recovery issue opened,
plain-language comment, raw error code never leaks), and (2) it still
escalates when no open dependency remains.
- `doc/execution-semantics.md`: document the "Deliberate wait is not a
lost run" recovery rule and the requirement that a post-decomposition
umbrella hold a first-class waiting path rather than relying on
`parentId` rollup.

## Verification

- `cd server && npx tsc --noEmit` — passes against current `master`.
- New tests in `server/src/__tests__/heartbeat-process-recovery.test.ts`
(describe: "heartbeat orphaned process recovery"):
- "converts a continuation parked for review into a dependency wait on
its open sub-tasks"
- "still escalates a continuation parked for review when no open
dependency remains"
- Run with the repo's vitest setup, e.g. `pnpm vitest run
server/src/__tests__/heartbeat-process-recovery.test.ts` (requires the
embedded-postgres test harness).

## Risks

Low. The change adds a single guarded pre-check ahead of the existing
escalation path; behavior is unchanged when the cancellation error code
is not `issue_continuation_waiting_on_review` or when the issue has no
open sub-task / unresolved blocker to wait on. No schema or migration
changes.

## Model Used

Claude (Anthropic), Opus-class model, via the Claude Code agent harness
— extended thinking 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 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
- [ ] If this change affects the UI, I have included before/after
screenshots (N/A — server-only)
- [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 (pending CI)
- [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
2026-06-19 20:52:18 -07:00
Nicky Leach fb0e3fb02a Fix mention-granted closed issue comments with resume intent (#8350)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Issue comments are part of the control plane boundary between agent
collaboration and task mutation.
> - Mention grants intentionally let a mentioned peer agent answer on an
issue thread without taking ownership of the task.
> - Closed issue comments have a second mutation guard because comments
can also request resume or reopen behavior.
> - A mention-granted comment should stay append-only unless the actor
also has mutation authority.
> - This pull request preserves the narrow comment path while keeping
explicit `resume` and `reopen` intent behind mutation authorization.
> - The benefit is a smaller authorization exception with regression
coverage for both allowed and denied paths.

## Linked Issues or Issue Description

Refs #2884
Related prior authorization work: #8024, #7863, #6113, #7998.

Bug fix description:
- What happened: a mention-granted non-assignee agent could be treated
as authorized for the closed issue comment route without preserving a
clean distinction between append-only comments and explicit
reopen/resume mutation intent.
- Expected behavior: a mention grant allows a plain comment on a closed
issue, but `resume: true` or `reopen: true` still requires mutation
authorization.
- Steps to reproduce: create a closed issue assigned to one agent, give
a different agent a valid mention-scoped comment grant, then POST a
comment as that agent with and without `resume`/`reopen` intent.
- Deployment mode: server route behavior; covered by focused Vitest
regression tests.

## What Changed

- Preserve the `issue:comment` authorization decision in `POST
/api/issues/:id/comments` so the route can identify legitimate
mention-grant decisions.
- Skip the closed-issue non-assignee mutation fallback only for inert
mention-granted comments.
- Continue requiring mutation authorization when a mention-granted
closed issue comment includes explicit `resume` or `reopen` intent.
- Add regression coverage for the allowed inert comment and denied
resume/reopen cases.

## Verification

- `pnpm vitest run
server/src/__tests__/issue-comment-reopen-routes.test.ts` passed
locally: 72 tests.

## Risks

- Low risk: scoped to the issue comment route and tests for a specific
authorization decision reason.
- Residual risk: CI should run the full PR suite.

## Model Used

OpenAI GPT-5 Codex via Paperclip Git Expert agent, with tool use for
code inspection, test execution, Git, and GitHub operations.

## 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] If this change affects the UI, I have included before/after
screenshots
- [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>
2026-06-19 16:47:06 -07:00
Nicky Leach 67f97e8fb0 fix(server): enforce read auth for single issue comments (#8346)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Issue comments are part of the control-plane audit trail and must
respect the same company and issue authorization boundaries as issue
reads.
> - Mention-scoped commenting gives low-trust agents a narrow way to
reply when an authorized assignee mentions them.
> - The comment list route already enforces issue-read authorization,
but the single-comment read route only checked same-company access
before returning a known comment id.
> - That created a broken object-level authorization gap for
same-company low-trust agents outside the issue boundary.
> - This pull request applies the existing issue-read guard to the
single-comment route before loading the comment.
> - The benefit is consistent comment-read authorization across list and
single-comment endpoints, with regression coverage for the low-trust
boundary.

## Linked Issues or Issue Description

Refs #7389

Bug fix context:
- What happened: `GET /api/issues/:id/comments/:commentId` checked
company access but did not enforce the issue-read authorization boundary
before returning a single comment by known id.
- Expected behavior: single-comment reads should use the same issue-read
boundary as issue thread list reads.
- Steps to reproduce: authenticate as a same-company low-trust agent
outside an issue's readable boundary, then request a known comment id
via the single-comment endpoint.
- Deployment mode: applies to server authorization behavior in
authenticated agent API usage.

## What Changed

- Added `assertIssueReadAllowed` to the single issue-comment read route
before `getComment` is called.
- Added mocked route coverage proving peer agents outside the issue-read
boundary get `403` and the comment is not loaded.
- Added authorization and embedded route coverage for mention-scoped
low-trust comment grants so the intended narrow reply path remains
allowed.

## Verification

- `NODE_ENV=test pnpm exec vitest run
server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts` —
passed, 60 tests.
- `NODE_ENV=test pnpm exec vitest run
server/src/__tests__/authorization-service.test.ts` — passed, 26 tests.
- `NODE_ENV=test pnpm exec vitest run
server/src/__tests__/low-trust-red-team-routes.test.ts` — passed, 8
tests.
- `git diff --check` — passed.

## Risks

Low risk. This reuses the existing issue-read guard for a read endpoint.
The main behavioral shift is that same-company actors who cannot read an
issue can no longer fetch a known comment id from that issue, which is
the intended authorization boundary.

> 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 GPT-5 via Codex, with repository tool use and command execution.
Runtime context-window details were not exposed by the environment.

## 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] If this change affects the UI, I have included before/after
screenshots
- [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>
2026-06-19 15:57:36 -07:00
Nicky Leach 364f0f5a8d fix(server): enforce issue read for issue thread lists (#8331)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents coordinate through issue threads, so issue comments and
interactions are part of the task authorization surface.
> - Low-trust and boundary-limited agents can receive narrow
mention-scoped access, but that must not turn into broad same-company
issue-thread reads.
> - The comment creation path was narrowed to allow explicit mention
replies without granting mutation access.
> - The surrounding list/read routes still needed to enforce the same
`issue:read` boundary before returning thread data.
> - This pull request applies the issue read check to issue comment and
interaction listing routes, and locks that behavior with server
regressions.
> - The benefit is that narrow cross-agent collaboration remains
possible without exposing unrelated issue-thread history.

## Linked Issues or Issue Description

Bug fix:
- What happened: same-company agents outside an issue read boundary
could still hit issue-thread listing routes and receive thread data.
- Expected behavior: issue comments and issue-thread interactions should
only be listed after the actor is allowed to read the issue.
- Steps to reproduce: configure a peer agent denied by the issue read
boundary, then request `GET /api/issues/:id/comments` or the issue
interaction listing route.
- Paperclip version/commit: current `master` before this branch.
- Deployment mode: applies to server authorization in all modes.

Related public context: #7389, #7863, #8024.

## What Changed

- Added issue read enforcement before listing issue comments.
- Added issue read enforcement before listing issue-thread interactions.
- Added server regressions for denied peer-agent issue-thread access
while preserving mention-scoped collaboration behavior.
- Removed an avoidable per-mentioned-comment issue reload in
mention-grant authorization by passing the already-loaded issue assignee
through the helper.
- Documented cross-agent issue read/comment authorization behavior in
the Paperclip API reference.
- Added a resilient fallback for pinned external skills when GitHub tree
fetches are temporarily unavailable during catalog builds.

## Verification

- `pnpm vitest run
server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts
server/src/__tests__/authorization-service.test.ts`
  - 2 test files passed
  - 84 tests passed
- Existing mocked recovery revalidation warnings were emitted by the
route suite and the command exited 0
- Greptile: 5/5 on commit `b73fc323f192dc44f88374c16865008d4348923b`; no
unresolved review threads.
- `pnpm vitest run packages/skills-catalog/src/catalog-builder.test.ts`
  - 1 test file passed
  - 6 tests passed
- CI: all visible PR checks are terminal green on commit
`b73fc323f192dc44f88374c16865008d4348923b`.

## Risks

Low risk. This tightens read authorization on issue-thread listing
routes; any caller that depended on same-company access without
`issue:read` will now receive 403 and must use an explicit grant or
valid issue read path.

## Model Used

OpenAI GPT-5 Codex, Codex coding agent environment, tool use and local
command execution enabled, reasoning mode active. Context window 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] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [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>
2026-06-19 15:16:17 -07:00
Dotta a71c4b6782 [codex] feat(watchdog): add task watchdog control plane (#8339)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The task lifecycle and recovery subsystems decide when agent work is
still productive, stalled, or ready for review.
> - Existing recovery paths can observe stopped or incomplete work, but
there was no first-class per-task watchdog model with scoped review
permissions.
> - Watchdog follow-ups also need strict boundaries so
recovery/status-only runs cannot mutate approvals or perform deliverable
work.
> - This pull request adds the task watchdog data model, API/service
layer, scheduler/review flow, adapter wake context, UI configuration
surfaces, and docs.
> - The branch has been rebased onto current `paperclipai/paperclip`
`master`; the watchdog migration is now ordered after master's latest
migrations as `0104_issue_watchdogs`.
> - The benefit is a more explicit task-review loop that preserves
Paperclip's single-assignee and governance invariants while making
stalled work easier to route.

## Linked Issues or Issue Description

No linked GitHub issue. Paperclip task:
[PAP-11275](/PAP/issues/PAP-11275).

## Problem or motivation

Task recovery needs a first-class watchdog path that can inspect stopped
work and create scoped follow-ups without bypassing normal task
ownership. Board/UI users need a way to configure watchdogs on tasks and
see watchdog-related live work. Recovery/status-only runs must remain
limited to status reporting and must not create approvals, link
approvals, or submit approval comments.

## Proposed solution

Add a task-watchdog data model, scheduler/classifier, scoped mutation
guard, adapter wake context, API/UI configuration surfaces, and
documentation so watchdog agents can review stopped task subtrees under
explicit boundaries.

## Alternatives considered

Reuse the existing recovery-action flow only. That would keep
stopped-work detection implicit, make per-task watchdog assignment
harder to expose in the UI, and would not provide a durable
scoped-review issue for stalled task trees.

## Roadmap alignment

This is Paperclip control-plane lifecycle infrastructure for task
execution and recovery. I checked `ROADMAP.md`; this PR does not
duplicate an existing planned core item.

## What Changed

- Added issue watchdog schema, migration, shared contracts, validators,
CRUD API, and service support.
- Added task watchdog scheduler/classifier behavior, scoped mutation
enforcement, adapter wake context, and default watchdog mandate
guidance.
- Added UI surfaces for configuring watchdogs on new/existing tasks,
viewing watchdog activity, and exposing the experimental setting.
- Added docs for the user-facing task watchdog workflow and
implementation semantics.
- Gated new-task watchdog setup behind `enableTaskWatchdogs` and blocked
cheap status-only recovery runs from approval mutations.
- Rebased onto current `master` and renumbered the idempotent watchdog
migration from the branch-local `0102_issue_watchdogs` slot to
`0104_issue_watchdogs`.
- Addressed Greptile feedback by loading watchdog classifier input with
a recursive subtree query and centralizing the watchdog origin-kind
constant.
- Added and updated focused server/UI tests for watchdog routes,
scheduler/classifier behavior, scope boundaries, live task visibility,
settings, and new issue dialog behavior.

## Verification

- `pnpm vitest run server/src/__tests__/task-watchdogs-scheduler.test.ts
server/src/__tests__/task-watchdogs-classifier.test.ts`
- `pnpm vitest run
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts`
- `pnpm vitest run ui/src/components/NewIssueDialog.test.tsx`
- `pnpm --filter @paperclipai/server typecheck`
- `git diff --check`
- Verified the PR diff does not include `pnpm-lock.yaml` or
`.github/workflows`.

## Risks

- Medium risk: this introduces a new task lifecycle surface touching DB
schema, server routes/services, adapter wake context, and UI task
configuration.
- Watchdog scheduling behavior depends on the new experimental setting
and runtime context checks behaving consistently across local and
production agents.
- The watchdog migration is idempotent (`IF NOT EXISTS` /
duplicate-object guards) so users who tried the previous branch-local
migration number should not get duplicate-object failures.
- CI and the second Greptile pass are pending after the latest
review-fix push.

> 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-class coding agent in the Paperclip workspace. Exact
runtime model id and context window were not exposed to the agent; tool
use and local command 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots — N/A per Paperclip task instruction: do not add
screenshots/images to this PR unless they are specifically part of the
work.
- [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: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 15:38:52 -05:00
Devin Foley 6a3f5b685d fix(agents): clear inbox hire approval when approving/terminating from detail page (#8340)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Hiring a new agent creates a `hire_agent` approval record; until it
is resolved, the agent sits in `pending_approval` and the open approval
surfaces an "Approve/Reject" card in the inbox
> - There are two places to act on that hire: the inbox approval card
and the agent detail page's Approve/Terminate buttons
> - The agent detail page only flipped the agent to `idle` via
`activatePendingApproval`, never resolving the linked approval record
> - So after approving (or terminating) from the detail page, the
approval stayed `pending` and the inbox kept showing a stale
"Approve/Reject" card for an agent that was already decided
> - This pull request routes the detail-page approve through the shared
`approvalsSvc.approve()` (which resolves the approval and runs
activation, budget policy, and the hire-approved notification), and
rejects the linked approval when a still-pending agent is terminated
> - The benefit is a single source of truth: deciding a hire in one
place clears it everywhere, so the inbox no longer asks you to approve
an agent you already approved

## Linked Issues or Issue Description

No public GitHub issue exists for this; describing in-PR (bug report).

**What happened:** Create a new agent, then approve it from the agent
detail page. The agent activates, but the inbox still shows a "Hire
Agent" item with Approve/Reject buttons for it.

**Expected:** Once a hire is approved or rejected anywhere in the UI, it
should be resolved everywhere — the inbox should not re-ask you to
approve an agent that is already decided.

**Root cause:** `POST /agents/:id/approve` called
`activatePendingApproval`, which only changes the agent's status. The
linked `hire_agent` approval row stayed `pending`. The inbox lists
approvals filtered to unresolved statuses, so the card persisted.
Terminating a pending agent from the detail page had the mirror problem
for the "reject" half.

Related (not duplicates): #215 (join-request inbox badge), #1815
(approval detail button loading text).

## What Changed

- Added `approvalService.findOpenHireApprovalForAgent(companyId,
agentId)` to locate the open `hire_agent` approval for an agent. The
company/type/open-status **and** `payload->>'agentId'` predicates all
run in SQL (jsonb operator), so the DB returns only the relevant row
instead of filtering in JS (`server/src/services/approvals.ts`).
- `POST /agents/:id/approve` resolves the linked approval through the
shared `approvalsSvc.approve()` — running activation, budget-policy
upsert, and the hire-approved notification as one path — and falls back
to direct `activatePendingApproval` only when no open approval exists
(legacy agents created before approvals were tracked)
(`server/src/routes/agents.ts`).
- `POST /agents/:id/terminate` now branches the same way: when a
still-`pending_approval` agent has an open hire approval, it delegates
to `approvalsSvc.reject()` (which resolves the approval **and**
terminates the agent internally) and re-reads the agent, otherwise it
terminates directly. This avoids terminating the agent twice (`reject()`
already calls `agentsSvc.terminate()`) (`server/src/routes/agents.ts`).
- The `agent.approved` activity log records the resolved `approvalId`
(or `null` on the fallback path) for traceability.

## Verification

- `pnpm --filter @paperclipai/server typecheck` — clean.
- Targeted server tests pass (57 tests):
`npx vitest run src/__tests__/approvals-service.test.ts
src/__tests__/agent-permissions-routes.test.ts`
- `findOpenHireApprovalForAgent` returns the row the SQL filter yields /
returns null when none matches.
- Approving from the detail page resolves the linked approval via
`approvalsSvc.approve()` and does not double-activate; legacy fallback
still calls `activatePendingApproval`.
- Terminating a still-pending agent with an open approval calls
`approvalsSvc.reject()` and does **not** call `agentsSvc.terminate()` a
second time; terminating with no open approval terminates directly.
- Manual: create an agent → inbox shows the hire card → approve from the
agent detail page → inbox card is gone and the agent is active. Same for
terminate-while-pending clearing the card.

## Risks

Low risk, server-only, no schema or migration changes. The fallback to
`activatePendingApproval` preserves existing behavior for agents with no
tracked approval record, so legacy agents still activate. The shared
approval path is the same one the inbox card already uses, so
approve/terminate-from-detail-page now match approve/reject-from-inbox
exactly.

## Model Used

Claude Opus 4.8 (claude-opus-4-8), extended thinking with 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] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots (server-only change, no UI diff)
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(in progress)
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-06-19 12:15:57 -07:00
Dotta 7069053a1f [codex] Add ask issue work mode (#8334)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Issue work mode controls how a task starts and how the conversation
composer frames the operator's intent.
> - Paperclip already supports standard agent execution and planning
mode, but there is no lightweight mode for asking a question without
immediately implying execution or plan drafting.
> - That gap makes low-commitment clarification workflows look like
normal task execution.
> - This pull request adds an explicit Ask mode and threads it through
shared contracts, server heartbeat context, and the issue composer UI.
> - The benefit is that operators can create or switch a task into a
question-oriented mode while preserving existing agent and planning
flows.

## Linked Issues or Issue Description

No public GitHub issue exists for this change. Inline feature request
follows the repository feature request template.

### Subsystem affected

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

### Problem or motivation

Issue conversations currently distinguish standard agent work from
planning work, but question-first conversations do not have a clear
public mode in the shared contract or UI. Operators who want to ask an
agent a focused question have to use standard mode, which can imply
normal task execution, or planning mode, which asks for a plan rather
than an answer.

### Proposed solution

Add Ask as a first-class issue work mode. It should be selectable from
issue creation and issue chat, cycle alongside Standard and Planning
from the keyboard shortcut/menu, appear distinctly in composer styling,
and be included in heartbeat context so agents know to answer directly
instead of executing or drafting a plan.

### Alternatives considered

- Keep using standard mode for questions: rejected because it does not
communicate answer-only intent to the agent or the UI.
- Reuse planning mode for questions: rejected because planning mode asks
for a plan and is semantically different from asking a question.
- Add only local UI copy: rejected because the mode needs to be
represented in the shared contract and server heartbeat context to be
reliable.

### Roadmap alignment

This is a focused issue-workflow improvement. `ROADMAP.md` was checked
and no duplicate planned core work was found.

### Additional context

Related public searches performed before opening this PR:

- GitHub PR search for `"ask mode" repo:paperclipai/paperclip`
- GitHub issue search for `"ask mode" repo:paperclipai/paperclip`
- GitHub PR search for `"work mode" "ask" repo:paperclipai/paperclip`

No duplicate PR was found.

## What Changed

- Added `ask` to the shared issue work-mode contract and validation
coverage.
- Included issue work mode in heartbeat context summaries so agents can
see standard, planning, and ask state.
- Added Ask mode metadata, styling, composer tone handling, and
selection/cycling behavior in the issue chat/new issue UI.
- Updated focused tests for shared validators, heartbeat context, and
affected UI work-mode flows.

## Verification

- `NODE_ENV=test pnpm exec vitest run
ui/src/components/ChatComposer.test.tsx
ui/src/components/IssueChatThread.test.tsx
ui/src/components/NewIssueDialog.test.tsx
ui/src/lib/work-mode-meta.test.ts`
- `NODE_ENV=test pnpm exec vitest run
packages/shared/src/validators/issue.test.ts
server/src/__tests__/heartbeat-context-summary.test.ts
server/src/__tests__/issues-service.test.ts
ui/src/components/ChatComposer.test.tsx
ui/src/components/IssueChatThread.test.tsx
ui/src/components/NewIssueDialog.test.tsx
ui/src/lib/work-mode-meta.test.ts ui/src/pages/IssueDetail.test.tsx`

The broader targeted command passed 8 test files / 245 tests.

Visual reference for Standard/Planning/Ask composer states:
https://gist.github.com/cryppadotta/714d8590bac55500a65e7e16de5bb4b8

It emitted an expected warning from an existing server test fixture
about a missing run-log fixture while verifying derived issue comment
metadata.

## Risks

Low to moderate risk. This adds a new enum value that crosses shared,
server, and UI contracts. Existing standard and planning modes are
preserved, but any downstream code assuming only two non-terminal work
modes may need to handle `ask`.

> 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 GPT-5 Codex coding agent in Paperclip CodexCoder mode, with
shell, git, GitHub connector, and local test execution tools. Context
window and exact hosted model snapshot are not exposed in this 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)
- [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`,
`feat/...`) 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] If this change affects the UI, I have included before/after
screenshots
- [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>
2026-06-19 13:06:04 -05:00
Dotta aeea5f9195 fix(ui): stabilize routine schedule editor and interrupted run labels (#8333)
## Thinking Path

> - Paperclip is the open source control plane people use to manage AI
agents for work.
> - This change touches the board UI surfaces for issue run timelines
and routine schedule editing.
> - Operators need cancelled runs to distinguish ordinary cancellation
from human interruption, otherwise the run history reads as more severe
than it is.
> - Routine schedule editing also needs to preserve user-entered cron
values while rendering common schedules in a stable, understandable
editor.
> - This pull request keeps the editor state tied to explicit schedule
values, adds coverage for routine editable sections, and makes
interrupted run copy more precise.
> - The benefit is less surprising routine editing and clearer issue run
history for operators.

## Linked Issues or Issue Description

No public GitHub issue is filed for this exact branch. Related public
PRs:

- Refs #3581, which addresses a narrower schedule reset case.
- Refs #1803, which is another open schedule editor UI improvement.

Problem description:

Routine trigger schedules can be edited through the board UI, but the
previous schedule editor path could normalize or reset cron state in
ways that made unsaved edits fragile. Issue run history also labeled
operator-interrupted cancelled runs like ordinary cancellations.
Reviewers should treat this PR as a combined UI stabilization pass for
those two visible operator workflows.

## What Changed

- Added a more stable routine schedule editor flow that preserves
explicit cron values and handles custom/common schedule transitions.
- Wired routine editable-section state so schedule drafts do not get
overwritten by unrelated section refreshes.
- Added tests for schedule editor behavior, routine editable sections,
and routine service schedule preservation.
- Updated issue run timeline copy so operator-interrupted cancelled runs
display as interrupted, while ordinary cancelled runs remain cancelled.
- Kept the classic issue thread run label behavior aligned with the
current issue thread surface.

## Verification

- `NODE_ENV=development pnpm run preflight:workspace-links &&
NODE_ENV=development pnpm exec vitest run
server/src/__tests__/routines-service.test.ts
ui/src/components/IssueChatThread.test.tsx
ui/src/components/ScheduleEditor.test.tsx
ui/src/components/routine-sections/editable-sections.test.tsx` — 103
tests passed.

Note: direct `pnpm exec vitest ...` without `NODE_ENV=development`
loaded a React build where `React.act` is undefined in this workspace.
The same targeted tests pass under the development React build.

## Risks

Low to medium risk. The changes are UI-focused but touch routine
schedule editing, which is a high-frequency operator workflow. The main
risk is that an uncommon cron expression could render as custom when a
user expected a preset; the added tests cover preservation and explicit
custom handling.

> 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-class coding agent. Exact hosted runtime model ID
and context window were not exposed in this session. Tool use and local
command execution were used for inspection, verification, GitHub PR
creation, and Paperclip issue updates.

## 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] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [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: Paperclip <noreply@paperclip.ing>
2026-06-19 13:01:17 -05:00
Zen Process f13fd1157b feat(adapters): stamp agent id via X-Anthropic-Agent-Id for claude_local (#8322)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Local agents run through the `claude_local` adapter, which spawns
Claude Code and routes its Anthropic API traffic through a shared proxy
(e.g. better-ccflare).
> - That proxy's request log feeds observability dashboards that break
down cost and token usage.
> - But requests from local agents arrive without an agent identifier,
so all traffic collapses into a single unattributed bucket and per-agent
telemetry is impossible.
> - Anthropic's CLI forwards `ANTHROPIC_CUSTOM_HEADERS` onto each API
request, and better-ccflare now reads an `X-Anthropic-Agent-Id` header
to attribute requests per agent.
> - This pull request stamps the agent's id into
`ANTHROPIC_CUSTOM_HEADERS` as `X-Anthropic-Agent-Id`, injected into the
env the adapter actually forwards to the spawned process.
> - The benefit is per-agent cost/token attribution in proxy-backed
dashboards, with zero impact on agents that don't run behind such a
proxy.

## Linked Issues or Issue Description

No public GitHub issue exists; describing inline per CONTRIBUTING.md
(feature template).

**Problem or motivation**

When several `claude_local` agents share a proxy (better-ccflare) in
front of the Anthropic API, the proxy cannot tell which agent issued a
given request. Per-agent cost and token dashboards therefore can't be
built — every request is attributed to one undifferentiated bucket.

**Proposed solution**

Have the `claude_local` adapter stamp each spawned Claude Code process
with `ANTHROPIC_CUSTOM_HEADERS: X-Anthropic-Agent-Id: <agentId>`. The
Anthropic CLI forwards that header on every API call, so the proxy can
attribute requests to the specific agent.

**Alternatives considered**

Parsing per-agent identity from workspace paths or session ids in the
proxy — brittle and proxy-specific. A first-class request header is the
stable contract; it pairs with better-ccflare's `X-Anthropic-Agent-Id`
support
([tombii/better-ccflare#260](https://github.com/tombii/better-ccflare/pull/260))
and dashboards like
[danieltamas/axon](https://github.com/danieltamas/axon).

**Roadmap alignment**

Additive observability glue for an existing adapter; no overlap with
planned core work.

## What Changed

- Added `server/src/adapters/claude-agent-id-header.ts` exporting
`stampClaudeAgentIdHeader`, which wraps the `claude_local` execute and
merges `X-Anthropic-Agent-Id: <agentId>` into the run's
`config.env.ANTHROPIC_CUSTOM_HEADERS`.
- `server/src/adapters/registry.ts`: the `claude_local` adapter now uses
the wrapped execute (`stampClaudeAgentIdHeader(claudeExecute)`).
- The header is merged into **`config.env`** — the env the Claude
adapter actually forwards into the spawned process — rather than
`agent.adapterConfig.env`, which is resolved upstream before `execute`
runs and is never read by the Claude adapter.
- `agentId` is sanitized (CR/LF stripped, capped at 256 chars) before
interpolation to prevent HTTP header injection.
- Passthrough when no agent id is available; a pre-existing
`X-Anthropic-Agent-Id` (manual override) is respected and not
duplicated.
- Added `server/src/adapters/claude-agent-id-header.test.ts` covering
injection into `config.env`, append-to-existing
`ANTHROPIC_CUSTOM_HEADERS`, CR/LF sanitization, length bounding,
no-agent-id passthrough, and the duplicate-header guard.

## Verification

- Unit tests: `cd server && npx vitest run
src/adapters/claude-agent-id-header.test.ts` → 6 passing.
- CI: typecheck, build, server suites, and Greptile gates run on this
PR.
- Manual: configure a `claude_local` agent behind better-ccflare, run a
heartbeat, and confirm the proxy log shows `X-Anthropic-Agent-Id:
<agentId>` on that agent's requests. An agent that already sets
`X-Anthropic-Agent-Id` in its adapter config keeps its override (no
duplicate line).

## Risks

Low risk. The change is additive and non-breaking:

- When no agent id is present, or when an `X-Anthropic-Agent-Id` is
already configured, execution is byte-for-byte unchanged (the original
`ctx` is passed straight through).
- The header only has an effect for deployments whose proxy reads it;
agents without such a proxy are unaffected.
- The agent id is sanitized (CR/LF removed, length-bounded) before it is
interpolated into a header value, preventing header injection.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. This is additive
observability glue for an existing adapter and does not overlap planned
core work.

## Model Used

Claude (Anthropic) — Claude Opus 4.8 (`claude-opus-4-8`), via the Claude
Code CLI with extended reasoning and tool use.

## 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] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (N/A — no UI 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: zenprocess <zenprocess@users.noreply.github.com>
Co-authored-by: Devin Foley <devin@paperclip.ing>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-06-19 09:56:28 -07:00
Devin Foley 5e086cb828 fix(server): resolve published skills catalog package root and fallback (#8327)
## Thinking Path

> - Paperclip is the open source control plane people use to run and
supervise AI-agent companies.
> - The skills system is part of that core operator experience because
agents and humans both depend on the catalog-backed Skills Manager
surfaces.
> - In source checkouts, the server can find the catalog manifest and
bundled skill files through monorepo-relative paths, but published
installs do not preserve that layout.
> - That mismatch makes `GET /api/skills/catalog` fail in npm/pnpm
installs even though the catalog package itself is present.
> - The server therefore needs to resolve the catalog from the published
`@paperclipai/skills-catalog` package first, while still keeping a
monorepo fallback for local development.
> - This pull request makes the published package the primary resolution
path, uses the same resolved package root for bundled skill file reads,
and degrades the list route safely when the manifest is unavailable.
> - The benefit is that Skills Manager catalog reads behave correctly in
packaged installs instead of only in repo-local development layouts.

## Linked Issues or Issue Description

Fixes #8316
Refs #7281
Refs #7313
Refs #7350
Refs #7860
Refs #8223
Refs #8227

## What Changed

- Added `@paperclipai/skills-catalog` as a runtime dependency of
`@paperclipai/server`.
- Exported `./package.json` from `@paperclipai/skills-catalog` so the
server can resolve the published package root directly.
- Updated `server/src/services/skills-catalog.ts` to resolve the
manifest and package root from the published package first, with the
monorepo path retained only as a development fallback.
- Applied that resolved package root to bundled catalog file reads so
manifest lookup and skill-file reads use the same published layout.
- Added `listCatalogSkillsOrEmpty()` so `GET /api/skills/catalog`
returns `[]` and logs a warning when the manifest is unavailable instead
of surfacing a 500.
- Added targeted server tests for published-package resolution and
missing-manifest fallback handling.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/skills-catalog-service.test.ts
src/__tests__/company-skills-routes.test.ts`
- `pnpm --filter @paperclipai/server build`
- Packaging smoke:
- pack `@paperclipai/shared` and `@paperclipai/skills-catalog` from this
checkout
  - mount those packed artifacts under `server/dist/node_modules`
  - import `server/dist/services/skills-catalog.js`
- verify a bundled catalog `SKILL.md` resolves and reads successfully
from the packed package layout

## Risks

- Low risk: the change is narrowly scoped to catalog package resolution
and fallback behavior.
- The new `./package.json` export slightly broadens the catalog
package's public surface, so reviewers should confirm that is an
acceptable runtime contract.
- The empty-array fallback intentionally changes failure mode for a
missing manifest from `500` to a warning + empty payload, which is safer
for packaged installs but could hide packaging regressions if logs are
not monitored.

## Model Used

- OpenAI Codex via Paperclip `codex_local` (GPT-5-based coding agent;
exact backend model ID/context window not exposed in this harness), with
tool use, shell execution, git, and local test/build 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] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] 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
2026-06-19 09:14:41 -07:00
dmndbrp-oss 0186e66c7c test(SAG-4328): de-flake checkout-lock race in orphan-recovery test (#8315)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server heartbeat subsystem tracks agent runs and recovers
orphaned processes via `reapOrphanedRuns`
> - When a local process dies mid-run, the recovery path queues a retry
run and releases the checkout lock asynchronously
> - The test `queues exactly one retry when the recorded local pid is
dead` verifies both the retry state and that the checkout lock is
released
> - The `checkoutRunId` null-check was a bare synchronous assertion, but
the checkout-lock release is async — so the test could read
`checkoutRunId` before the write committed
> - This PR wraps the predicate already gating on `executionRunId` to
also wait for `checkoutRunId === null`, so both writes are committed
before the assertions fire
> - The benefit is a deterministic test that does not race the async
checkout-lock release

## Linked Issues or Issue Description

-

## What Changed

- `server/src/__tests__/heartbeat-process-recovery.test.ts`: Extended
the `waitForValue` predicate (line ~1024) from `executionRunId ===
retryRun?.id` to also require `checkoutRunId === null` before returning
the row. This eliminates the race between the assertion and the async
terminal-run checkout-lock release.

## Verification

- No production code changed — test file only.
- Ran the test file 10× locally with `pnpm exec vitest run
server/src/__tests__/heartbeat-process-recovery.test.ts
--no-file-parallelism --maxWorkers=1`:

```
Run  1: Test Files 1 passed (1) | Tests 57 passed (57)
Run  2: Test Files 1 passed (1) | Tests 57 passed (57)
Run  3: Test Files 1 passed (1) | Tests 57 passed (57)
Run  4: Test Files 1 passed (1) | Tests 57 passed (57)
Run  5: Test Files 1 passed (1) | Tests 57 passed (57)
Run  6: Test Files 1 passed (1) | Tests 57 passed (57)
Run  7: Test Files 1 passed (1) | Tests 57 passed (57)
Run  8: Test Files 1 passed (1) | Tests 57 passed (57)
Run  9: Test Files 1 passed (1) | Tests 57 passed (57)
Run 10: Test Files 1 passed (1) | Tests 57 passed (57)
```

10/10 green streak — race eliminated.

## Risks

Low risk — test-only change. No production code modified. The predicate
change only narrows the waitForValue poll to require both writes
committed before asserting, matching the intent already documented in
the comment on line 1031.

> 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 Sonnet 4.6 (claude-sonnet-4-6) — Anthropic, 200K context, 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] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [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: Director of Engineering <cto@paperclip.ing>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Devin Foley <devin@paperclip.ing>
2026-06-19 08:59:23 -07:00
Tattoofamily f672a9e2e5 fix(runtime): keep agent pause durable at execution-start (#8317)
**Issue (described inline; no existing tracking issue):** Pausing an
agent is not durable. Pausing cancels the in-flight run, but a queued or
recovery-dispatched run can clobber the agent back to `running` because
the execution-start status update is unconditional — so a "paused" agent
silently resumes work while `paused_at` is still set.

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies; agent
work executes as "runs" tracked in `heartbeat_runs`, with a
recovery/automation layer that re-dispatches work when a run disappears.
> - The agent lifecycle has a pause control (status `paused`,
`paused_at` set) meant to stop an agent from taking or continuing work.
> - The problem: pause is not durable. Pausing cancels the in-flight
run, but the execution-start path then sets `agents.status = 'running'`
with an unconditional `UPDATE ... WHERE id = ?`, so any queued or
recovery-dispatched run can clobber the paused agent back to `running`
and execute.
> - Why it matters: a "paused" agent silently resuming undermines the
core operational control operators rely on to halt runaway,
cost-sensitive, or unsafe work.
> - This pull request guards the execution-start status flip with an
atomic conditional UPDATE, and tags pause-cancellations for
observability without changing resume behaviour.
> - The benefit is that a paused agent can no longer transition back to
`running`; queued/recovery-dispatched runs are cancelled cleanly instead
of clobbering status, while un-pausing still resumes in-flight work.

## What Changed

- Execution-start guard: replaced the unconditional `UPDATE agents SET
status='running' WHERE id = ?` with an atomic conditional `UPDATE ...
WHERE id = ? AND status NOT IN
('paused','terminated','pending_approval')`. On a zero-row match the run
is cancelled (`errorCode: "agent_not_invokable"`), the issue execution
lock is released, and the path returns — instead of clobbering status.
- Exported `DIRECT_NON_INVOKABLE_STATUSES` from `agent-invokability.ts`
and reused it in `heartbeat.ts` as the single source of truth for the
guard.
- Pause observability: `cancelActiveForAgentInternal` now accepts an
`errorCode` (default `"cancelled"`); the pause-route wrapper
`cancelActiveForAgent` passes `"agent_paused"`. This is
classification-neutral — `agent_paused` is NOT added to
`NON_RETRYABLE_CONTINUATION_ERROR_CODES`, so on un-pause the issue's
continuation re-enqueues and work resumes.
- Exported `classifyContinuationFailure` from `recovery/service.ts` for
unit testing (no logic change).
- Added `server/src/services/recovery/service.pause-durability.test.ts`
covering continuation classification.

## Verification

- `pnpm --filter @paperclipai/server typecheck` — clean.
- `pnpm exec vitest run
server/src/services/recovery/service.pause-durability.test.ts` — 5
passed.
- `pnpm exec vitest run server` — full server suite passes locally. The
only failures are pre-existing and environment-specific, unrelated to
this change (a git default-branch test fixture, and a known
checkout-lock race) — both reproduce identically on clean `master` with
this change stashed out.
- Behavioural: a paused agent's execution-start now aborts cleanly with
no status clobber; non-pause cancellations keep `errorCode "cancelled"`
and existing behaviour; un-pausing resumes the in-flight issue.

## Risks

- Low risk. No schema change, no migration, no new dependency; four
files. The change narrows a single UPDATE to be conditional and adds a
rarely-taken abort branch on the run-start path; behaviour for invokable
agents is unchanged. The abort's `agent_not_invokable` code is already
in `NON_RETRYABLE_CONTINUATION_ERROR_CODES`. The only caller of
`cancelActiveForAgent` is the pause route.

## Related upstream work — not duplicates

This area has prior and in-flight PRs; #8317 was checked against them
and is intentionally distinct:

- **#4503** (`fix(heartbeat): make agent pause status guard atomic with
status update`) targets a different TOCTOU race on the **post-run /
finalize** path (`finalizeAgentStatus`). #8317 targets the
**execution-start** race, where a recovery-dispatched run flips a paused
agent back to `running` *before the run begins*. #4503 does not cover
the proven failure path here: `pause → active run cancelled → recovery
dispatches a new run → execution-start overwrites the paused state`.
#4503 also does not add the resume semantics below.
- **#4356** (`honor system/manual/auto pause at all heartbeat-run
enqueue sites`) and **#1067** (`pause guard on queue drain`) protect the
**enqueue / queue-drain** layer. They are complementary to — not
substitutes for — the execution-start guard, which is the last gate
before a run actually starts.
- **#6944** (`guard executeRun against paused agent`), **#7140**, and
**#7141** attempted similar execution-start ideas but were closed for
implementation hygiene / build issues, not because the guard concept was
wrong. #8317 implements that concept cleanly: a single atomic
conditional UPDATE, a clean abort with `errorCode:
"agent_not_invokable"`, a shared `DIRECT_NON_INVOKABLE_STATUSES` source
of truth, and passing tests + typecheck.

Intentional, minor difference (not a criticism of #4503): #8317's
execution-start deny-list is `paused`, `terminated`, and
`pending_approval` — the full non-invokable set for run-start
invokability — whereas #4503 appears focused on `paused`/`terminated`.
The broader set is deliberate for the execution-start guard.

Resume semantics: #8317 keeps `agent_paused` as observability-only and
classification-neutral (retryable), so a paused agent's in-flight work
resumes on un-pause rather than escalating to blocked.

## Model Used

- Provider: Anthropic. Model: Claude Opus 4 (`claude-opus-4-8`), via the
Claude desktop "Cowork" agent. Mode: agentic/extended reasoning with
tool use (shell, file editing, running `tsc`/`vitest`, git). Used to
investigate the root cause in source, design the fix, implement it, and
validate 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (N/A — no UI change)
- [ ] I have updated relevant documentation to reflect my changes (N/A —
no doc-facing change)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
- [x] I have searched the open and closed PR list for similar/duplicate
PRs and found none
2026-06-19 08:35:53 -07:00
Devin Foley fc95699fde fix(server): enforce agent secret binding sync across lifecycle flows (#8307)
## Thinking Path

> - Paperclip is the control plane people use to create, configure, and
run AI agents for work.
> - This change sits in the server-side agent lifecycle and
secret-binding subsystem, where adapter config `env` entries can
reference company secrets.
> - An incident (while trying to configure a Novita sandbox) showed that
an agent can reach a broken runtime state if `adapterConfig.env`
contains `secret_ref` entries but the matching `company_secret_bindings`
rows are missing.
> - The immediate run-path guard and error-surfacing work made the
failure diagnosable, but they did not fully prevent new broken agents
from being created.
> - The risk came from create and approval flows being responsible for
remembering to sync bindings at each call site, which is easy to miss as
new flows are added.
> - This pull request moves the invariant into `agentService`
create/update/activate paths, keeps the existing hire-flow fix, and adds
regression coverage for create, update, and legacy pending-approval
recovery.
> - The benefit is that agent secret binding integrity is enforced
closer to the data mutation point, so future callers inherit the
protection automatically.

## Linked Issues or Issue Description

Refs #8309

### What happened?
A Paperclip agent could persist `adapterConfig.env` `secret_ref` entries
without matching agent-scoped `company_secret_bindings` rows. When that
happened, the config UI could still look configured, but the real run
path failed pre-dispatch because the secret was not actually bound to
that agent.

### Expected behavior
Every normal agent create, config-update, and pending-approval
activation flow should leave the agent with secret bindings that match
its persisted secret-ref env config.

### Steps to reproduce
1. Create or activate an agent through a flow that persists
`adapterConfig.env` secret refs without synchronizing
`company_secret_bindings`.
2. Observe that the config state can still appear populated.
3. Start a run for that agent.
4. Observe that pre-dispatch binding validation fails because the secret
reference exists but the agent binding does not.

### Deployment mode
Local dev (`pnpm dev`)

### Installation method
Built from source (`pnpm dev` / `pnpm build`)

### Agent adapter(s) involved
- Claude Code
- Not adapter-specific (core bug)

### Database mode
Embedded PGlite / embedded local dev database flow

### Access context
Board (human operator) created or approved the agent; agent runtime
later consumed the config.

### Additional context
This PR focuses on preventing new broken states from normal service
flows and on backfilling the covered legacy pending-approval activation
path.

## What Changed

- Kept the existing branch-local hire-flow fix that synchronized
bindings for route and approval paths.
- Moved the binding integrity invariant into `agentService.create()`,
`agentService.update()` when `adapterConfig` changes, and
`agentService.activatePendingApproval()`.
- Added `server/src/__tests__/agents-service-secret-bindings.test.ts`
covering create-time sync, update-time resync, and backfill for legacy
pending-approval agents.
- Removed now-redundant route-layer and approval-layer binding sync
calls once the service layer became authoritative.
- Simplified the affected unit tests so route/approval tests no longer
assert service-owned binding writes directly.

## Verification

- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run
server/src/__tests__/agents-service-secret-bindings.test.ts
server/src/__tests__/approvals-service.test.ts
server/src/__tests__/agent-skills-routes.test.ts`

## Risks

- Low to medium risk.
- This changes where secret-binding synchronization is enforced, so any
unexpected caller that relied on upper-layer manual sync behavior could
behave differently.
- Agent create/update/activation flows now perform binding
synchronization consistently, which adds binding-table writes at those
mutation points.
- This PR does not retroactively scan and heal every already-broken
historical agent row; it prevents and backfills through the covered
service flows.

> 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 Codex class model via `codex_local`
- Session model family: GPT-5 Codex
- Tool-assisted coding with shell, git, HTTP, and local test execution
- Reasoning mode: medium interactive tool-use workflow

## 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
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [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>
2026-06-18 21:26:36 -07:00
BuyWhere 67c98323b0 fix(recovery): exempt routine-parent issues from missing-disposition handoff (#8157)
Recognize active routine-parent issues as having a valid continuation path during successful-run handoff recovery. This prevents unnecessary missing-disposition corrective wakes when an active routine owns the next scheduled action.

Also keeps the routine-continuation guard before the productivity check so logs surface the decisive skip reason for both productive and non-productive routine-parent runs.

Verification:
- CI status checks passed on PR #8157
- Greptile Review passed at 5/5
- Focused recovery unit test passed locally

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-18 17:59:06 -07:00
dependabot[bot] 5c11725784 build(deps-dev): bump tsx from 4.21.0 to 4.22.4 (#7754)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.21.0 to 4.22.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/privatenumber/tsx/releases">tsx's
releases</a>.</em></p>
<blockquote>
<h2>v4.22.4</h2>
<h2><a
href="https://github.com/privatenumber/tsx/compare/v4.22.3...v4.22.4">4.22.4</a>
(2026-05-31)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>resolve CommonJS directory requires inside dependencies (<a
href="https://redirect.github.com/privatenumber/tsx/issues/803">#803</a>)
(<a
href="https://github.com/privatenumber/tsx/commit/1ce846335b7c445a3328c7d27f06424949356d97">1ce8463</a>)</li>
</ul>
<hr />
<p>This release is also available on:</p>
<ul>
<li><a href="https://www.npmjs.com/package/tsx/v/4.22.4"><code>npm
package (@​latest dist-tag)</code></a></li>
</ul>
<h2>v4.22.3</h2>
<h2><a
href="https://github.com/privatenumber/tsx/compare/v4.22.2...v4.22.3">4.22.3</a>
(2026-05-19)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>decode typed loader source (<a
href="https://github.com/privatenumber/tsx/commit/dce02fc3b8b64a58d24560714902b16f89332f1f">dce02fc</a>)</li>
<li>preserve entrypoint with TypeScript preload hooks (<a
href="https://github.com/privatenumber/tsx/commit/68f72f3304d8c3ff7048bde8571af9c163fcefa2">68f72f3</a>)</li>
</ul>
<hr />
<p>This release is also available on:</p>
<ul>
<li><a href="https://www.npmjs.com/package/tsx/v/4.22.3"><code>npm
package (@​latest dist-tag)</code></a></li>
</ul>
<h2>v4.22.2</h2>
<h2><a
href="https://github.com/privatenumber/tsx/compare/v4.22.1...v4.22.2">4.22.2</a>
(2026-05-18)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>preserve CJS JSON require in ESM hooks (<a
href="https://github.com/privatenumber/tsx/commit/35b700bd8620696df03827068af29dcd0d091a60">35b700b</a>)</li>
<li>preserve named exports from CommonJS TypeScript (<a
href="https://github.com/privatenumber/tsx/commit/11de737dae1fb9dae28db3716df5b1a7e1a6a089">11de737</a>)</li>
<li>support module.exports require(esm) interop (<a
href="https://github.com/privatenumber/tsx/commit/cf8f19918e4e0a0dc5ee5c52d8cc15e5e22d7c49">cf8f199</a>)</li>
</ul>
<hr />
<p>This release is also available on:</p>
<ul>
<li><a href="https://www.npmjs.com/package/tsx/v/4.22.2"><code>npm
package (@​latest dist-tag)</code></a></li>
</ul>
<h2>v4.22.1</h2>
<h2><a
href="https://github.com/privatenumber/tsx/compare/v4.22.0...v4.22.1">4.22.1</a>
(2026-05-17)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>resolve tsconfig path aliases containing a colon (<a
href="https://redirect.github.com/privatenumber/tsx/issues/780">#780</a>)
(<a
href="https://github.com/privatenumber/tsx/commit/6979f28810829dc79ec9baf406e162a18b65ab4b">6979f28</a>)</li>
</ul>
<hr />
<p>This release is also available on:</p>
<ul>
<li><a href="https://www.npmjs.com/package/tsx/v/4.22.1"><code>npm
package (@​latest dist-tag)</code></a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/privatenumber/tsx/commit/1ce846335b7c445a3328c7d27f06424949356d97"><code>1ce8463</code></a>
fix: resolve CommonJS directory requires inside dependencies (<a
href="https://redirect.github.com/privatenumber/tsx/issues/803">#803</a>)</li>
<li><a
href="https://github.com/privatenumber/tsx/commit/dce02fc3b8b64a58d24560714902b16f89332f1f"><code>dce02fc</code></a>
fix: decode typed loader source</li>
<li><a
href="https://github.com/privatenumber/tsx/commit/68f72f3304d8c3ff7048bde8571af9c163fcefa2"><code>68f72f3</code></a>
fix: preserve entrypoint with TypeScript preload hooks</li>
<li><a
href="https://github.com/privatenumber/tsx/commit/69455cfefbfe71100a3c58d3ce7cea42445d9113"><code>69455cf</code></a>
test: cover package exports for ambiguous ESM reexports</li>
<li><a
href="https://github.com/privatenumber/tsx/commit/35b700bd8620696df03827068af29dcd0d091a60"><code>35b700b</code></a>
fix: preserve CJS JSON require in ESM hooks</li>
<li><a
href="https://github.com/privatenumber/tsx/commit/ef807dba6832260fb4cafd78d81f5469a733966b"><code>ef807db</code></a>
chore: update testing dependencies</li>
<li><a
href="https://github.com/privatenumber/tsx/commit/3917090d4f61863ea6ea16e4a9a3722a112cc3f7"><code>3917090</code></a>
test: document compatibility test taxonomy</li>
<li><a
href="https://github.com/privatenumber/tsx/commit/de8113ffa8edbcd4e05fa218324c3e8c2a4afdbe"><code>de8113f</code></a>
refactor: centralize Node capability facts</li>
<li><a
href="https://github.com/privatenumber/tsx/commit/c1f62db45ada60b24ceb3dfdf7f64173d9a15396"><code>c1f62db</code></a>
test: consolidate tsconfig path edge coverage</li>
<li><a
href="https://github.com/privatenumber/tsx/commit/4e08174ec10276ac71c9a69eb28426ad702d0c76"><code>4e08174</code></a>
test: consolidate loader hook coverage</li>
<li>Additional commits viewable in <a
href="https://github.com/privatenumber/tsx/compare/v4.21.0...v4.22.4">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for tsx since your current version.</p>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 17:56:49 -05:00
dependabot[bot] 6f142a60ce build(deps-dev): bump @types/node from 22.19.11 to 22.19.21 (#7748)
Bumps
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)
from 22.19.11 to 22.19.21.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">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>
2026-06-18 15:46:06 -07:00
dependabot[bot] b15fc221ca build(deps): bump @aws-sdk/client-s3 from 3.994.0 to 3.1072.0 (#7752)
Bumps
[@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3)
from 3.994.0 to 3.1072.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/aws/aws-sdk-js-v3/releases">@​aws-sdk/client-s3's
releases</a>.</em></p>
<blockquote>
<h2>v3.1072.0</h2>
<h4>3.1072.0(2026-06-18)</h4>
<h5>Documentation Changes</h5>
<ul>
<li><strong>client-ec2:</strong> Documentation updates clarifying
CancelCapacityReservation cancellable states (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/e3723ba73e2f2d307254d49ec73c5b3d91b8d892">e3723ba7</a>)</li>
</ul>
<h5>New Features</h5>
<ul>
<li><strong>client-compute-optimizer:</strong> This release surfaces two
new metrics Volume IOPS Exceeded and Volume Throughput Exceeded into EBS
volume rightsizing recommendations. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/ded6618d5249ab413bd90b26d4cea91d5f4b9b8f">ded6618d</a>)</li>
<li><strong>client-application-auto-scaling:</strong> Adds support for
ECS high-resolution predefined scaling metrics
(ECSServiceAverageCPUUtilizationHighResolution,
ECSServiceAverageMemoryUtilizationHighResolution) enabling 20-second
metric periods for faster scaling (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/95b3513a224a678fb92dc623f149d24adb96ae7b">95b3513a</a>)</li>
<li><strong>client-cognito-identity-provider:</strong> In order to
support the new TLS Self-Service feature, this change adds
SecurityPolicyType to CustomDomainConfigType. During
CreateUserPoolDomain and UpdateUserPoolDomain this is used to select a
custom domain's TLS enforcement, and for DescribeUserPoolDomain it
informs users about the current TLS. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/e89377876aa392ea403636b6821cdb3df253648b">e8937787</a>)</li>
<li><strong>client-sagemaker:</strong> Adds support for automatic AMI
patching on HyperPod clusters. Customers can configure patching
strategies to automatically apply security patch with zero job
termination. Customers can also specify an AMI version at instance group
level and update cluster software to a certain AMI version. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/fd33a5e46ca314f064b9149900abe4e451661b5e">fd33a5e4</a>)</li>
<li><strong>client-ecs:</strong> Amazon ECS services now support high
resolution (20 second) CloudWatch metrics for CPUUtilization and
MemoryUtilization. Use these metrics for faster service auto scaling.
(<a
href="https://github.com/aws/aws-sdk-js-v3/commit/93055ac93bffca3e2957b2d67cb24ecdc784457a">93055ac9</a>)</li>
<li><strong>client-healthlake:</strong> Adding New Configurations to the
FHIR Create Datastore. The new configurations include NLP Configuration,
AnalyticsConfiguration, ProfileConfiguration (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/494fa59f48705e23ab79da259046966831183c71">494fa59f</a>)</li>
<li><strong>client-gamelift:</strong> Amazon GameLift Servers has
launched support for customizing Linux capabilities in container fleets.
You can now specify additional Linux capabilities for containers in a
container group definition, giving you finer control over the default
Docker capabilities available to your containers. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/93cefd905d4fc0c649b1d7ed69f4c8f0d79d3371">93cefd90</a>)</li>
<li><strong>client-eks:</strong> Adds support for configurable control
plane egress routing in Amazon EKS, allowing you to route control plane
egress traffic through your VPC and control how the control plane
reaches resources in your network such as webhook servers and OIDC
providers. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/693db62958c6818b4cb847887b8e36a66347c119">693db629</a>)</li>
<li><strong>client-lambda:</strong> Converging and fixing existing
documentation gaps in Lambda SDK (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/6555a565348a308dad7a51c85457c2bcee87feb8">6555a565</a>)</li>
<li><strong>client-synthetics:</strong> CloudWatch Synthetics adds
support for multi-location canaries. Customers can now monitor their
endpoints from multiple locations with centralized management from a
primary location. The SDK includes new parameters for configuring
multiple locations and tracking their state. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/f2c8b480812b5ba2d1e173a8a074df6d72654239">f2c8b480</a>)</li>
<li><strong>client-cloudwatch-logs:</strong> Added optional
startFromHead parameter to FilterLogEvents enabling descending timestamp
order (newest first) when set to false. Default true preserves existing
ascending order. Reverse sorting requires a startTime on or after Jan 1,
2024. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/1be63ed942af46df978e55df4bec6fb6c9d16e60">1be63ed9</a>)</li>
<li><strong>client-batch:</strong> Adds Support for ordered allocation
strategies- BEST-FIT-PROGRESSIVE-ORDERED or
SPOT-CAPACITY-OPTIMIZED-PRIORITIZED (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/0e57e53b1e0914a03b5b7c7d346245a1e6b6da11">0e57e53b</a>)</li>
</ul>
<hr />
<p>For list of updated packages, view
<strong>updated-packages.md</strong> in
<strong>assets-3.1072.0.zip</strong></p>
<h2>v3.1071.0</h2>
<h4>3.1071.0(2026-06-17)</h4>
<h5>New Features</h5>
<ul>
<li><strong>client-partnercentral-selling:</strong> Cosell Resonate AND
Prospecing API Launch with ARN correction (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/7e8c98abe070924fbbd1ea2abc183f0715f80945">7e8c98ab</a>)</li>
<li><strong>client-compute-optimizer-automation:</strong> This launch
adds IfExists comparison operators to Compute Optimizer Automation rule
criteria, so a rule can include recommended actions whose specified
attribute isn't present. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/ab2c616d167a0796fba91e44d1118a6a8baee60d">ab2c616d</a>)</li>
<li><strong>client-bedrock-agent:</strong> Launching Bedrock Managed
Knowledge Bases. Added support for resource-based policies on Knowledge
Base resources, enabling cross-account access for Managed Knowledge
Bases. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/de0affe4ad738b7f07d91574e2b2cfd83a447d44">de0affe4</a>)</li>
<li><strong>client-securityagent:</strong> Updated AWS Security Agent
SDK model with new APIs for threat modeling, code review, security
requirements, and additional integration providers. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/9c3d3351842902b1ea1e8be52c4e7f40b868daae">9c3d3351</a>)</li>
<li><strong>client-opensearch:</strong> Adds support for configuring IAM
Identity Center options on existing OpenSearch applications via the
UpdateApplication API. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/94f06a20a15ab75302088d5f3c31d708a8256e5c">94f06a20</a>)</li>
<li><strong>client-glue:</strong> This release adds support for Search
and Discovery in AWS Glue, letting you and your applications search Data
Catalog assets such as table and enrich them with business context and
glossary terms. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/b394fc0b39c814d5f72593dfc4db5ff5c2cbe643">b394fc0b</a>)</li>
<li><strong>client-bedrock-agentcore-control:</strong> AgentCore Gateway
now supports inference targets to LLM providers (direct config or
built-in connectors), HTTP passthrough targets with session stickiness,
runtime target API schemas, AWS WAF web ACL association with
configurable fail-open or fail-close modes, and interceptor payload
filtering. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/75f1d588d526de04060ebd653ca1a96e7ea75ff6">75f1d588</a>)</li>
<li><strong>client-devops-agent:</strong> Adds support for Remote A2A
(Agent-to-Agent) agent registration and management. Adds new Release
Readiness Review and Release Testing capabilities. Adds support for Git
managed skills in AWS DevOps Agent. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/ebc040e1f3fc292d3eee1a9b146c972338e14807">ebc040e1</a>)</li>
<li><strong>client-bedrock-agentcore:</strong> AgentCore Harness service
will be Generally Available at NYS 2026 with this Treb release. Harness
will support invoking specific endpoints via the qualifier parameter,
AWS Skills for pre-built agent capabilities, and improved validation for
skill git source URLs. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/5bf9fcccc3b4cc204df57caea2adcf254c5d3846">5bf9fccc</a>)</li>
<li><strong>client-ecs:</strong> Releasing the ability to bring-your-own
task-definition for CreateExpressGatewayService and
UpdateGatewayExpressService (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/b7b9cb4f46a34c4ab0fedf2138a9a5ba17bdd731">b7b9cb4f</a>)</li>
<li><strong>client-mq:</strong> This release adds private networking
support for Amazon MQ for RabbitMQ. You can now associate AWS RAM
resource shares with your broker and retrieve shared resource details
using the new DescribeSharedResources API. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/e96af4503bfea9a6f211bc905b8e949af6cc38d7">e96af450</a>)</li>
<li><strong>client-bedrock-agent-runtime:</strong> Adds new
AgenticRetrieveStream API for managed knowledge bases to use
conversation history and autonomously plan for multi-hop multi-KB
reasoning with built-in evaluation and access-control. Updates Retrieve
API for access-control-based filtering for managed knowledge bases. (<a
href="https://github.com/aws/aws-sdk-js-v3/commit/557f7b3246fb6530fb8dcb481f1035d7827d2a1e">557f7b32</a>)</li>
</ul>
<h5>Tests</h5>
<ul>
<li><strong>core:</strong>
<ul>
<li>prebuild before integration and e2e (<a
href="https://redirect.github.com/aws/aws-sdk-js-v3/pull/8111">#8111</a>)
(<a
href="https://github.com/aws/aws-sdk-js-v3/commit/363f3fb7165d327e3ca24b3d7beffb19cc528c31">363f3fb7</a>)</li>
<li>prebuild core before unit tests (<a
href="https://redirect.github.com/aws/aws-sdk-js-v3/pull/8108">#8108</a>)
(<a
href="https://github.com/aws/aws-sdk-js-v3/commit/5f789e7fe2089eacff868a6e1d0b6c2c11313bbc">5f789e7f</a>)</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md">@​aws-sdk/client-s3's
changelog</a>.</em></p>
<blockquote>
<h1><a
href="https://github.com/aws/aws-sdk-js-v3/compare/v3.1071.0...v3.1072.0">3.1072.0</a>
(2026-06-18)</h1>
<p><strong>Note:</strong> Version bump only for package
<code>@​aws-sdk/client-s3</code></p>
<h1><a
href="https://github.com/aws/aws-sdk-js-v3/compare/v3.1070.0...v3.1071.0">3.1071.0</a>
(2026-06-17)</h1>
<p><strong>Note:</strong> Version bump only for package
<code>@​aws-sdk/client-s3</code></p>
<h1><a
href="https://github.com/aws/aws-sdk-js-v3/compare/v3.1069.0...v3.1070.0">3.1070.0</a>
(2026-06-16)</h1>
<h3>Features</h3>
<ul>
<li><strong>client-s3:</strong> Added support for annotations. You can
now attach up to 1000 annotations (up to 1 MB each) directly to objects
and create, retrieve, list, and delete them using new annotation APIs.
Also added support for configuring an annotation table in S3 Metadata.
(<a
href="https://github.com/aws/aws-sdk-js-v3/commit/c555874690846b81904a2c0c1e96130bd03bbeaa">c555874</a>)</li>
</ul>
<h1><a
href="https://github.com/aws/aws-sdk-js-v3/compare/v3.1068.0...v3.1069.0">3.1069.0</a>
(2026-06-15)</h1>
<p><strong>Note:</strong> Version bump only for package
<code>@​aws-sdk/client-s3</code></p>
<h1><a
href="https://github.com/aws/aws-sdk-js-v3/compare/v3.1067.0...v3.1068.0">3.1068.0</a>
(2026-06-12)</h1>
<p><strong>Note:</strong> Version bump only for package
<code>@​aws-sdk/client-s3</code></p>
<h1><a
href="https://github.com/aws/aws-sdk-js-v3/compare/v3.1066.0...v3.1067.0">3.1067.0</a>
(2026-06-11)</h1>
<p><strong>Note:</strong> Version bump only for package
<code>@​aws-sdk/client-s3</code></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/aws/aws-sdk-js-v3/commit/501cd332619533ae96f154d7c226dc2f7bf8d615"><code>501cd33</code></a>
Publish v3.1072.0</li>
<li><a
href="https://github.com/aws/aws-sdk-js-v3/commit/3ce820aa54c953459eb58abe4f06e28ba4ceb87d"><code>3ce820a</code></a>
Publish v3.1071.0</li>
<li><a
href="https://github.com/aws/aws-sdk-js-v3/commit/4f2cfe1cfc420d0b5bfa226ae6619dd67de73ccc"><code>4f2cfe1</code></a>
Publish v3.1070.0</li>
<li><a
href="https://github.com/aws/aws-sdk-js-v3/commit/c555874690846b81904a2c0c1e96130bd03bbeaa"><code>c555874</code></a>
feat(client-s3): Added support for annotations. You can now attach up to
1000...</li>
<li><a
href="https://github.com/aws/aws-sdk-js-v3/commit/7058d13814795c6ff06a960077269458520bf161"><code>7058d13</code></a>
Publish v3.1069.0</li>
<li><a
href="https://github.com/aws/aws-sdk-js-v3/commit/67981c5a65d6dd797a065df034a8d0fcdaa9b7bd"><code>67981c5</code></a>
chore(scripts): tuning the build graph (<a
href="https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3/issues/8095">#8095</a>)</li>
<li><a
href="https://github.com/aws/aws-sdk-js-v3/commit/0632f6dc8842caaa916c5f43a5043342ff9ba6bb"><code>0632f6d</code></a>
Publish v3.1068.0</li>
<li><a
href="https://github.com/aws/aws-sdk-js-v3/commit/0a3246f174335af55a6981b4891f0f4c10dfe4c4"><code>0a3246f</code></a>
Publish v3.1067.0</li>
<li><a
href="https://github.com/aws/aws-sdk-js-v3/commit/4b11912aef8adc845f81b7e9922bd180c5cf1d90"><code>4b11912</code></a>
Publish v3.1066.0</li>
<li><a
href="https://github.com/aws/aws-sdk-js-v3/commit/e4ef6c57d8fd97d15e0a7a27a24396990d704307"><code>e4ef6c5</code></a>
test: use crypto.randomUUID for resource names in e2e tests (<a
href="https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3/issues/8091">#8091</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/aws/aws-sdk-js-v3/commits/v3.1072.0/clients/client-s3">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>
2026-06-18 15:30:29 -07:00
dependabot[bot] b29b47ae4a build(deps-dev): bump @types/multer from 2.0.0 to 2.1.0 (#7751)
Bumps
[@types/multer](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/multer)
from 2.0.0 to 2.1.0.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/multer">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/multer&package-manager=npm_and_yarn&previous-version=2.0.0&new-version=2.1.0)](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>
2026-06-18 15:12:24 -07:00
Nicky Leach 6cfc6731d9 test(recovery): fix race condition in workspace validation comment check (#8294)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The heartbeat service manages agent run lifecycles, including
workspace validation before launching git-sensitive local adapters
> - When workspace validation fails, `escalateStrandedAssignedIssue` in
the recovery service sets the issue to `"blocked"` and then
asynchronously posts a system comment
> - A test added in PR #7644 verifies the comment appears, but queried
the database immediately after `waitForValue` resolved the issue status
— creating a race between the status write and the comment write
> - The test was passing before PR #8284 (pnpm lockfile refresh) but
began failing after, suggesting the lockfile update shifted async
scheduling enough to expose the window
> - This pull request wraps the comment check in `waitForValue`,
consistent with two identical polling patterns already used in the same
test file (lines 1159-1162 and 1232-1235)
> - The benefit is a consistently-passing test that unblocks dependabot
PR #8155 from auto-merging via CI

## Linked Issues or Issue Description

Refs PAP-72. Unblocks PAP-68 (dependabot PR #8155 which has auto-merge
enabled and was blocked by this test failure).

## What Changed

- `server/src/__tests__/heartbeat-process-recovery.test.ts`: replaced
direct `db.select()` + `expect(comments.some(...))` with a
`waitForValue` polling loop that waits until a comment containing
`"workspace failed validation"` appears, then asserts it is truthy
- No production code changed

## Verification

The specific test `blocks a git-sensitive local adapter before launch
when a project-workspace-linked issue is missing its project id`
previously failed with:

```
AssertionError: expected false to be true
src/__tests__/heartbeat-process-recovery.test.ts:1429:94
```

After this fix: the test polls with a 3-second timeout and returns once
the comment row exists. The comment IS written by
`escalateStrandedAssignedIssue` — this was purely a timing issue in the
test assertion. CI on this PR serves as the confirmation run.

## Risks

Low risk — test-only change. No production code is modified. The
`waitForValue` helper is already used 10+ times in the same test file
with the same 3-second timeout; this use follows the identical pattern.

## Model Used

- Provider: Anthropic
- Model: Claude Sonnet 4.6 (`claude-sonnet-4-6`)
- Context window: 200k tokens
- Tool use: yes (file read, bash, edit tools)
- Reasoning mode: extended thinking

## 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
- [ ] I have run tests locally and they pass (CI will verify)
- [x] I have added or updated tests where applicable (this IS the test
fix)
- [x] If this change affects the UI, I have included before/after
screenshots (N/A)
- [x] I have updated relevant documentation to reflect my changes (N/A)
- [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
(in progress)
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-06-18 14:52:56 -07:00
MrBob 5f16efb3d0 fix: parse YAML block scalar skill descriptions (#5046)
## Thinking Path

> - Paperclip is the open source control plane teams use to manage AI
agents for work.
> - Company skills are imported from `SKILL.md` files and rely on YAML
frontmatter to describe what each skill does.
> - Multi-line descriptions commonly use YAML block scalars (`>` and
`|`), but the broken parser path behind #4989 reduced those descriptions
to a literal `>` or `|`.
> - The earliest contributor fix for that bug was PR #5046, so this
branch keeps that PR as the canonical merge target instead of replacing
it.
> - Follow-up work from #5071 and #8258 was then transplanted onto this
earlier branch so the final PR preserves contributor credit while still
shipping the strongest complete fix.
> - The resulting change fixes block-scalar parsing in the shared
frontmatter path, aligns server company-skill imports with that shared
parser, and prevents already-stale stored markers from rendering as junk
in the UI.

## Linked Issues or Issue Description

- Fixes #4989
- Refs #2863
- Refs #788
- Related superseded PRs: #5071, #8258

## What Changed

- Kept the original PR #5046 server-side company-skill fix and
regression coverage as the base branch history.
- Added the missing YAML chomping and indicator hardening explored
further in #5071.
- Moved frontmatter parsing to the shared parser path so
`packages/shared`, `packages/skills-catalog`, and server company-skill
imports stay aligned.
- Added UI summary sanitization and fallback handling so stale stored
`>` / `|` values no longer render as visible junk in company-skill
cards.
- Added regression coverage for shared frontmatter parsing,
skills-catalog parsing, company-skill imports, and stale-summary
fallback behavior.

## Verification

- Passed locally: `pnpm exec vitest run
packages/shared/src/frontmatter.test.ts
packages/skills-catalog/src/frontmatter.test.ts
server/src/__tests__/company-skills.test.ts
ui/src/lib/company-skill-summary.test.ts`
- Passed locally: `pnpm --filter @paperclipai/shared typecheck`
- Passed locally: `pnpm --filter @paperclipai/skills-catalog typecheck`
- Not fully runnable in this worktree: `pnpm --filter
@paperclipai/server typecheck` currently fails in `packages/plugins/sdk`
before reaching server code because local workspace `node_modules` type
deps are missing (`TS2688` for `node` / `react`).
- GitHub Actions / PR checks are rerunning on PR #5046 head
`005290b7557725abf748d00f36dd24ea0d919aba`.

## Risks

- Medium-low risk: the fix now touches shared parser code, server
company-skill imports, and UI fallback display rather than only the
server import path.
- The parser is still intentionally narrower than a full YAML
implementation; this change focuses on block-scalar correctness and the
stale-description rendering path relevant to #4989 / #2863.
- This branch intentionally supersedes narrower overlapping work from
#5071 and duplicate work from #8258 once the survivor PR is green.

> 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-based coding agent with local shell and
code-editing tools 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [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: Devin Foley <devin@paperclip.ing>
2026-06-18 12:01:11 -07:00
Svetlana Zolotenkova 5320a44088 Guard codex_local agents from shared OpenAI key (#8272)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - The `codex_local` adapter runs local Codex CLI processes and builds
their environment from persisted agent config plus host process env.
> - A host-level `OPENAI_API_KEY` or shared Codex auth home can silently
make new agents spend through shared credentials.
> - Existing agents can be repaired manually, but new and updated agents
need a persistent guard at the agent configuration boundary.
> - This pull request isolates new and updated `codex_local` agents with
per-agent `CODEX_HOME` and an empty `OPENAI_API_KEY` override.
> - The benefit is that future agent creation or adapter updates cannot
silently fall back to shared OpenAI credentials.

## Linked Issues or Issue Description

Paperclip work item: [ZOL-5477](/ZOL/issues/ZOL-5477).

No matching GitHub issue exists, so the bug is described inline
following `.github/ISSUE_TEMPLATE/bug_report.yml`.

**Pre-submission checklist**

- [x] I have searched existing open and closed issues and this is not a
duplicate.
- [x] I am on the latest `master` commit for this PR branch.
- [x] I have confirmed the error originates in Paperclip's `codex_local`
adapter configuration boundary, not in a provider outage.

**What happened?**

New or updated `codex_local` agents could inherit a host-level
`OPENAI_API_KEY` or use a shared Codex home when their adapter config
did not explicitly isolate those values. That made it possible for
future agents or manual adapter edits to silently fall back to shared
OpenAI credentials.

**Expected behavior**

Creating, hiring, or updating a `codex_local` agent should either
persist isolated per-agent configuration or reject unsafe shared Codex
home configuration with a clear 422 response. The guard must not print
secret values.

**Steps to reproduce**

1. Create or update a `codex_local` agent without an explicit
`adapterConfig.env.OPENAI_API_KEY` override.
2. Run it on a host where the Paperclip server process has
`OPENAI_API_KEY` set.
3. Observe that the adapter process can inherit the host key unless
Paperclip persists a blocking empty override.
4. Set `adapterConfig.env.CODEX_HOME` to a shared path such as
`~/.codex` or the company-level `codex-home`.
5. Observe that the old code allowed the shared auth home instead of
returning a validation error.

**Paperclip version or commit**

- Reproduced by inspection against `master` before this PR.

**Deployment mode**

- Local dev / self-hosted server with `codex_local` agents.

**Installation method**

- Built from source.

**Agent adapter(s) involved**

- Codex.

**Database mode**

- Not database-related.

**Access context**

- Board and agent configuration paths.

**Relevant logs or output**

- No secret-bearing logs included.

**Relevant config**

- Unsafe shape: missing `adapterConfig.env.OPENAI_API_KEY`, or shared
`adapterConfig.env.CODEX_HOME`.
- Fixed shape: per-agent `CODEX_HOME` plus empty `OPENAI_API_KEY`
override.

**Additional context**

Related PR search for `codex_local OPENAI_API_KEY CODEX_HOME` found:

- #3681 `fix: preserve managed Codex auth and repo-root env loading`
- #5621 `fix: copy worktree codex auth locally`

Those are adjacent auth-handling changes, but they do not add the agent
create/update guard implemented here.

**Privacy checklist**

- [x] I have reviewed all pasted output for PII, usernames, file paths,
API keys, tokens, company names, and redacted where necessary.

## What Changed

- Added a `codex_local` config guard in agent create, hire, and update
routes.
- The guard assigns `adapterConfig.env.CODEX_HOME` to
`companies/<companyId>/agents/<agentId>/codex-home` when missing.
- The guard persists `adapterConfig.env.OPENAI_API_KEY = ""` when
missing, preventing host env inheritance.
- Shared `CODEX_HOME` values for the company codex-home, host
`$CODEX_HOME`, or `~/.codex` now fail with a 422 error.
- Added route tests for create, hire, update, and rejected shared host
Codex home.
- Updated `codex_local` and development docs to describe the per-agent
home contract.

## Verification

- `pnpm exec vitest run
server/src/__tests__/agent-adapter-validation-routes.test.ts`
- `pnpm exec vitest run
server/src/__tests__/agent-skills-routes.test.ts`
- `pnpm typecheck`
- `git diff --check upstream/master...HEAD`
- `gh pr list --repo paperclipai/paperclip --state all --search
"codex_local OPENAI_API_KEY CODEX_HOME" --limit 20 --json
number,title,state,url`
- `rg -n "codex|OPENAI_API_KEY|CODEX_HOME|adapter" ROADMAP.md` returned
no roadmap overlap.

## Risks

- Existing legacy `codex_local` agents with shared `CODEX_HOME` will get
a clear 422 when their adapter config is updated until the shared path
is replaced. This is intentional because silent fallback is the bug
being guarded.
- Low migration risk: no database migration and no secret values are
printed or persisted beyond the empty override.

## Model Used

- OpenAI GPT-5.5 Codex, Codex coding-agent session with repository tool
use.

## 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
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [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

## Paperclip

- Issue: [ZOL-5477](/ZOL/issues/ZOL-5477)
- Owner: Разработчик (`6625498c-66c9-429f-b578-4463ddc3ba16`)
- Status: waiting reviewer
- Next action: merge after approval and green CI

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-06-18 10:41:00 -07:00
Devin Foley 04173b341d fix: resolve secret refs before sandbox draft probes (#8256)
## Thinking Path

> - Paperclip is the control plane operators use to manage agent
execution environments, including plugin-declared sandbox providers.
> - The failing user path here was `Test draft` for an unsaved sandbox
environment using a schema field marked `format: "secret-ref"`.
> - Saved environments already resolve secret refs before provider use,
but the unsaved probe path was forwarding the selected secret UUID
directly to the provider, which made Novita draft probes fail.
> - Fixing that safely required a probe-only secret resolution path with
explicit actor authorization and audit context, because an unsaved draft
has no persisted environment binding to authorize against.
> - Once that was fixed, CI and review surfaced follow-up hardening
work: preserve actor source through the draft-probe path, prevent late
heartbeat finalization from overwriting already-terminal runs, avoid
duplicate successful-run handoff wakes for comment-driven runs, make SSH
git ref updates tolerate concurrent managed-runtime restores, and keep
the skills catalog build from failing on transient GitHub errors for
pinned references.
> - The result is that Novita draft probes now behave like saved
environments, the new secret access path is constrained and audited, and
the PR is green end-to-end with Greptile at 5/5.

## Linked Issues or Issue Description

No matching public GitHub issue was found after searching open and
closed Paperclip issues for `novita`. Related PR search found
[#8255](https://github.com/paperclipai/paperclip/pull/8255), but it
addresses Novita/dev-SDK linking rather than this draft probe bug.

Bug summary:
- What happened?
When a board user configured a sandbox environment backed by a
schema-driven plugin provider such as Novita, selecting an existing
company secret for `apiKey` and clicking `Test draft` failed because the
probe received the secret UUID instead of the resolved secret value.
- Expected behavior
`Test draft` should resolve secret-ref fields before calling the
provider probe, just like the saved runtime path does.
- Steps to reproduce
  1. Open `Company Settings -> Environments`.
2. Create or edit a `Sandbox` environment using a provider with a
`format: "secret-ref"` field such as `Novita Agent Sandbox`.
  3. Select an existing company secret for `apiKey`.
  4. Click `Test draft`.
  5. Observe the probe failure before this patch.
- Paperclip version or commit
Reproduced on a local `master` dev checkout; fixed and verified on
branch commit `ed982d0c0`.
- Deployment mode
  Local dev (`pnpm dev`).
- Installation method
  Built from source (`pnpm dev` / `pnpm build`).
- Agent adapter(s) involved
Not adapter-specific in the core bug path; affects schema-driven sandbox
provider plugins such as Novita.
- Database mode
  Not database-related.
- Access context
  Board (human operator).
- Node.js version
  `v25.6.1`.
- Operating system
  `macOS 15.7.4`.
- Relevant logs or output
The user-visible failure was `Novita sandbox probe failed` during `Test
draft`.

## What Changed

- Resolved schema-marked secret-ref fields during unsaved sandbox
environment probes by adding a dedicated probe-time secret resolution
path in `environment-config.ts`.
- Passed `companyId` plus the full authenticated actor context into the
draft probe normalization route so secret resolution stays
company-scoped, authorized, and auditable.
- Hardened ephemeral secret resolution so unsaved probes require
`secrets:read`, preserve the original actor source (`local_implicit`,
`agent_jwt`, etc.), and emit usable audit metadata.
- Added a conditional heartbeat run-status update so late adapter
completions cannot overwrite runs that were already cancelled or
otherwise terminal.
- Skipped successful-run handoff synthesis for comment-driven wakes,
which removes the extra wake/run that was breaking
`heartbeat-comment-wake-batching`.
- Retried managed-runtime SSH git ref updates on concurrent ref-lock
races instead of failing the restore path.
- Reused the previous skills-catalog manifest entry when a pinned GitHub
reference fails with a recoverable transient error during CI catalog
generation.
- Added focused regression coverage for the draft probe, ephemeral
secret access, heartbeat handoff behavior, SSH ref-lock races, and
catalog fallback behavior.

## Verification

- `pnpm vitest run server/src/__tests__/environment-routes.test.ts`
- `pnpm vitest run server/src/__tests__/secrets-service.test.ts`
- `pnpm vitest run
server/src/__tests__/heartbeat-comment-wake-batching.test.ts`
- `pnpm vitest run
server/src/services/recovery/successful-run-handoff.test.ts`
- `pnpm vitest run
server/src/__tests__/openclaw-gateway-adapter.test.ts`
- `pnpm exec vitest run packages/adapter-utils/src/ssh-fixture.test.ts
-t "merges concurrent remote commits through the managed runtime restore
path"`
- `pnpm exec vitest run
packages/skills-catalog/src/catalog-builder.test.ts`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm --filter @paperclipai/skills-catalog build`
- `pnpm --filter @paperclipai/adapter-utils build`
- `gh pr checks 8256`
- Manual/live validation: the same fix was cherry-picked into the
running local dev checkout and the user re-tested the Novita `Test
draft` flow successfully after the server restart.

## Risks

- Low risk: the Novita-specific user-facing fix is isolated to unsaved
sandbox draft probes for plugin schema fields marked `format:
"secret-ref"`.
- The new ephemeral secret resolution path is intentionally stricter
than the original broken behavior; regressions would most likely show up
as denied draft probes rather than accidental secret exposure.
- The heartbeat, SSH, and catalog changes are all defensive; if they
regress, they should affect test/CI orchestration paths rather than
persisted company data.

## Model Used

- OpenAI Codex Local (`codex_local` in Paperclip). The runtime does not
expose the exact backend model ID in agent metadata. GPT-5-class coding
model with shell/tool use, repository editing, test execution, GitHub
review handling, and issue-thread coordination.

## 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
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] 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>
2026-06-18 07:48:09 -07:00
Devin Foley d47b4da655 Auto-build bundled plugins on install (#8254)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Plugins extend the server with worker/UI surfaces, and bundled local
plugins under `packages/plugins/**` ship as TS source — their compiled
`dist/` is not checked in
> - On a fresh checkout, installing a bundled local plugin via the
in-app **Install** button failed because `paperclipPlugin.manifest`
points at `./dist/manifest.js`, which does not exist until the package
is built
> - The error surfaces as `Package … does not appear to be a Paperclip
plugin (no manifest found)`, which is misleading — the manifest is real,
the dist is just missing — and forces every contributor to run `pnpm
--filter … build` by hand before the bundled-plugin installer works at
all
> - This pull request teaches the install path to detect that case and
run the package's build (plus standalone runtime bootstrap for plugins
outside the root workspace) before manifest resolution, gated by a kill
switch and a bounded timeout
> - The benefit is bundled plugins like
`@paperclipai/plugin-workspace-diff` install in one click on a fresh
checkout, with a clear error message and manual fallback when the
autobuild itself fails

## Linked Issues or Issue Description

No existing GitHub issue. Underlying bug, following the bug-report
template:

**What happened?**
Installing a bundled local plugin from a fresh checkout fails with
`Package @paperclipai/plugin-workspace-diff at
packages/plugins/plugin-workspace-diff does not appear to be a Paperclip
plugin (no manifest found)`. The manifest is declared in `package.json`
(`paperclipPlugin.manifest = ./dist/manifest.js`) but `dist/` is not
built/committed, so the loader cannot find it.

**Expected behavior**
Clicking **Install** on a bundled plugin builds it if needed and
registers it, without a manual build step.

**Steps to reproduce**
1. Fresh checkout of `master`
2. Start the server, open Plugin Manager
3. Click **Install** next to `@paperclipai/plugin-workspace-diff`
4. Observe the "no manifest found" failure

**Scope**
Same failure mode affects every bundled plugin without a checked-in
`dist/` (`plugin-llm-wiki`, examples, sandbox-provider plugins, etc.).

## What Changed

- `server/src/services/plugin-loader.ts`: added
`ensureLocalPluginBuilt(packageRoot, pkgJson)` — when the package lives
under `packages/plugins/**` and its declared paperclipPlugin entrypoints
(`manifest`, `worker`, `ui`) are missing, run `pnpm --filter <name>
build` (and a standalone runtime-deps bootstrap for plugins outside the
root pnpm workspace) before manifest resolution
- `server/src/routes/plugins.ts`: invoke the autobuild from the
local-path install path; surface a `hasBuiltEntrypoints` boolean on the
`AvailableBundledPlugin` listing; invalidate the bundled-plugins cache
after a successful install so a freshly built plugin no longer reports
`hasBuiltEntrypoints: false`
- `ui/src/api/plugins.ts` + `ui/src/pages/PluginManager.tsx`: type and
consume `hasBuiltEntrypoints` so the installer can show that an
autobuild will run on install
- `server/src/__tests__/plugin-install-autobuild.test.ts`: new suite — 9
tests covering success, kill-switch, build failure, timeout, manifest
still missing after build, standalone variant, and the existing
`plugin-routes-authz` listing assertion
- `doc/plugins/LOCAL_PLUGIN_DEVELOPMENT.md`: documents the autobuild,
the `PAPERCLIP_DISABLE_PLUGIN_AUTOBUILD=1` kill switch, and the manual
fallback command
- Detect the autobuild timeout via the child-process `killed` flag
rather than string-matching the error message, so the "after timing out"
context is actually emitted

Knobs:

- `PAPERCLIP_DISABLE_PLUGIN_AUTOBUILD=1` — skip autobuild entirely;
restore prior behavior
- Build timeout: 120s, with a clear error that points at the manual
`pnpm --filter <name> build` recovery command

## Verification

- `cd server && pnpm vitest run
src/__tests__/plugin-install-autobuild.test.ts
src/__tests__/plugin-routes-authz.test.ts` → 44/44 pass
- End-to-end on a clean checkout: `rm -rf
packages/plugins/plugin-workspace-diff/dist`, invoke
`ensureLocalPluginBuilt()` against the real package, all declared
entrypoints (`dist/manifest.js`, `dist/worker.js`, `dist/ui/index.js`)
regenerated. The original `no manifest found` symptom no longer
reproduces.

## Risks

Low. The autobuild only fires when (a) the package sits under
`packages/plugins/**`, (b) at least one declared entrypoint is missing,
and (c) the kill switch is not set. In a packaged production server the
`packages/plugins/**` path does not exist on disk, so the helper
short-circuits and never shells out to `pnpm`. Failures from the spawned
build are surfaced as an install error with the exact manual command to
retry, so the worst-case is the same UX as before plus a clearer
message.

## Model Used

Claude Opus 4.7 (claude-opus-4-7), extended thinking enabled, tool use
(filesystem + bash).

## 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
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [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: Paperclip <noreply@paperclip.ing>
2026-06-17 22:39:55 -07:00
Devin Foley f3e01c63bd fix(environments): partial unique index to dedup managed sandbox rows (#8247)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The `server/services/environments.ts` module lazily provisions a
managed Kubernetes sandbox environment for each company on first
heartbeat. Idempotency relies on `ensureKubernetesEnvironment` returning
the single managed row per company.
> - The `(company_id, driver)` unique index in the `environments` schema
is partial on `driver='local'` only, so two concurrent callers (e.g.
simultaneous first heartbeats from a freshly synced tenant) can both
insert a `driver='sandbox'` row before either sees the other.
> - The function tried to converge after the race by re-reading, picking
the oldest managed row as winner, and deleting the loser. Under
autocommit + read-committed, each post-insert SELECT is a fresh snapshot
— A may not see B's row, B may not see A's, so both pick their own and
neither deletes. Two rows survive.
> - The same race fired in CI as `expected 2 to be 1` at
`environment-service.test.ts:293`, gating multiple unrelated PRs on
retries.
> - This pull request encodes the operator-level invariant ("at most one
Paperclip-managed sandbox row per company") at the DB layer with a
partial unique index, then switches `ensureKubernetesEnvironment` to
`INSERT … ON CONFLICT DO NOTHING` keyed on that index. Losers re-read
the surviving row.
> - The benefit is the race is impossible by construction — no
application-side convergence loop, no test flake, and any future
`ensureXyzSandboxEnvironment` that sets `managedByPaperclip=true`
inherits the invariant for free.

## Linked Issues or Issue Description

Paperclip issue: PAPA-783 — implement managed-sandbox dedup fix (phase 2
of the approved plan on the parent flake-investigation issue).

This is the phase-2 fix for the flaky `environmentService > deduplicates
concurrent managed Kubernetes environment creation` test introduced by
`4ad94d0bd` (PR #7938). Failing CI runs since then on at least PRs
#7595, #8233, #8215, #8212. The plan was reviewed and approved on the
parent issue before implementation.

Closely related (not duplicates):
- PR #7938 — introduced the test and the in-process convergence loop
being replaced here.
- PR #7595, PR #8233, PR #8215, PR #8212 — downstream PRs affected by
the flake; one of them will be rebased onto this fix as the acceptance
gate.

## What Changed

- `packages/db/src/schema/environments.ts`: added
`environments_company_managed_sandbox_idx`, a partial unique index on
`(company_id) WHERE driver='sandbox' AND
(metadata->>'managedByPaperclip')::boolean = true`. The umbrella
`managedByPaperclip` predicate covers any current or future
Paperclip-managed sandbox flavor without needing a new index per
provider.
- `packages/db/src/migrations/0102_managed_sandbox_dedup_index.sql`:
one-shot dedup `DELETE` keeping the oldest managed-sandbox row per
`company_id` (scoped to `driver='sandbox' AND managedByPaperclip=true`),
`RAISE NOTICE` if any duplicates were removed, then `CREATE UNIQUE INDEX
IF NOT EXISTS environments_company_managed_sandbox_idx`. `CONCURRENTLY`
is omitted because the codebase's migration runner wraps each file in a
transaction (see `applyPendingMigrationsManually`); the table holds 1–3
rows per company, so the short lock is acceptable and consistent with
every other migration in the repo.
- `server/src/services/environments.ts`: `ensureKubernetesEnvironment`
now uses `INSERT … ON CONFLICT DO NOTHING` keyed on the new index. On
conflict it re-reads the surviving managed-sandbox row and returns it.
Drops the post-insert convergence (re-read by `createdAt ASC, id ASC`,
delete the loser) and the trailing comment that flagged "until a partial
unique index is added via migration" as the proper long-term fix.
- Unused `asc` import removed from
`server/src/services/environments.ts`.

## Verification

Local (matches the success criteria in the issue body):

```
$ cd server
$ passes=0; for i in $(seq 1 20); do
    pnpm vitest run src/__tests__/environment-service.test.ts -t "deduplicates concurrent" \
      && passes=$((passes+1)) || break
  done; echo "$passes/20"
20/20

$ passes=0; for i in $(seq 1 10); do
    pnpm vitest run src/__tests__/environment-service.test.ts \
      && passes=$((passes+1)) || break
  done; echo "$passes/10"
10/10
```

Adversarial fan-out stress (temporarily bumped `Array.from({ length: 8
}, …)` to `length: 32` on both dedup tests; reverted before commit):

```
$ # both dedup tests fan-out-of-32, 10 iterations
10/10
```

Two ensure paths exist in the parent plan, but only
`ensureKubernetesEnvironment` is on `master`.
`ensureManagedSandboxEnvironment` (referenced by the approved plan as
commit `dce9a9622`) lives only on an unmerged feature branch, not
master. The plan's helper-extraction and symmetric dedup test for that
path are deferred to whichever PR lands the second ensure path — it
inherits the same DB invariant by setting `managedByPaperclip=true`.
Discrepancy was flagged on the issue thread before implementation.

Typecheck:

```
$ pnpm -C server typecheck
ok
```

## Risks

Low risk.

- **Migration safety.** `IF NOT EXISTS` on the index makes the migration
idempotent. The dedup `DELETE` is bounded to rows matching the
managed-sandbox predicate; in production this should be a no-op (no race
has been reported in the wild — only in CI). On dev/CI DBs that already
accumulated duplicates, the migration emits a `NOTICE` reporting the
count.
- **No `CONCURRENTLY`.** The migration runner wraps each `.sql` file in
a transaction, which is incompatible with `CREATE INDEX CONCURRENTLY`.
The `environments` table holds 1–3 rows per company and the row count is
bounded by company count; the short ACCESS EXCLUSIVE during `CREATE
UNIQUE INDEX` is acceptable here and matches every other index migration
in the repo.
- **Predicate scope.** The partial index predicate matches exactly the
rows that `ensureKubernetesEnvironment` writes (`driver='sandbox'` with
`metadata.managedByPaperclip=true`). Tenant-created sandbox envs (via
`svc.create`) do not set this marker and are not covered — no false
positives, no surprise constraint violations on unrelated inserts.

## Model Used

Claude (Anthropic), `claude-opus-4-7`. Tool use: code edit + bash +
filesystem search; 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [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: Paperclip <noreply@paperclip.ing>
2026-06-17 16:09:45 -07:00
Dotta 6a684a5053 fix(adapters): pass Hermes custom providers as args (#8231)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent adapters translate Paperclip configuration into the
CLI/runtime flags needed by each provider.
> - Hermes custom providers are represented as `custom:*` values, but
the adapter registry was not passing that provider value through as
Hermes CLI arguments.
> - The fix is isolated to adapter argument construction and its
regression tests.
> - This pull request extracts only the Hermes custom-provider
pass-through fix onto `origin/master`.
> - The benefit is a small adapter PR that can merge independently from
UI, skills catalog, and migration work.

## Linked Issues or Issue Description

No GitHub issue exists for this branch split. Internal source task:
[PAP-11234](/PAP/issues/PAP-11234).

Problem/motivation:
- Hermes `custom:*` providers need to reach the Hermes process as
`--provider <value>`.
- The adapter should preserve existing auth injection and avoid adding
duplicate provider flags when the user already supplied one.

Proposed solution:
- Detect Hermes `custom:*` provider values in adapter registry argument
construction.
- Add `--provider <custom value>` unless an explicit provider arg
already exists.
- Cover both spaced and equals-style existing provider args in
regression tests.

Related PR search:
- Found related Hermes adapter PRs such as #7544 and #3027, but none
duplicates this specific custom-provider arg pass-through behavior.

Roadmap alignment:
- Checked `ROADMAP.md`; no duplicate planned item was found for this
adapter fix.

## What Changed

- Updated Hermes adapter registration argument construction to pass
`custom:*` providers through `extraArgs` as `--provider <value>`.
- Preserved existing auth injection behavior.
- Avoided duplicate provider arguments when `--provider value` or
`--provider=value` is already present.
- Added adapter registry regression coverage.

## Verification

- `CI=true NODE_ENV=development pnpm install --frozen-lockfile
--prefer-offline`
- `pnpm exec vitest server/src/__tests__/adapter-registry.test.ts --run`
— 1 file, 20 tests passed.

## Risks

- Low risk; change is scoped to Hermes adapter argument construction.
- Review should confirm the provider flag syntax matches current Hermes
CLI expectations.

> 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 GPT-5 Codex via Paperclip `codex_local` / CodexCoder, GPT-5-class
coding model with tool use and shell execution. Exact runtime snapshot
and context-window setting were not exposed by the Paperclip run
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 available from the run context)
- [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
- [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 (N/A;
no docs behavior changed)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (pending on draft PR)
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(pending follow-up loop)
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-06-17 11:21:33 -05:00
Jannes Stubbemann 362c30ccdc feat(server): opt-in OpenTelemetry auto-instrumentation (#3735)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Production self-hosters increasingly expect telemetry out of the box
— Jaeger, Tempo, Honeycomb, Datadog, Grafana Cloud, Dynatrace all speak
OTLP
> - Today there is no OpenTelemetry bootstrap in the server, so
operators who want traces have to patch their fork or run a sidecar that
captures only HTTP-level info
> - An opt-in bootstrap that costs nothing when disabled is the
minimum-viable surface for this audience
> - The OpenTelemetry packages are heavyweight enough that we don't want
them in the default dependency graph — they should load only when the
operator configures an OTLP endpoint
> - This pull request adds a self-contained
`server/src/instrumentation.ts` that dynamically imports the OTel SDK
and starts it when `OTEL_EXPORTER_OTLP_ENDPOINT` is set, and is a
complete no-op otherwise

## Linked Issues or Issue Description

No existing issue covers this directly — feature-gap description
following the feature-request template:

**Problem or motivation**

Production self-hosters increasingly expect telemetry out of the box —
Jaeger, Tempo, Honeycomb, Datadog, Grafana Cloud, Dynatrace all speak
OTLP — but the server has no OpenTelemetry bootstrap. Operators who want
traces today must patch their fork or run a sidecar that captures only
HTTP-level information.

**Proposed solution**

An opt-in OTel bootstrap gated on `OTEL_EXPORTER_OTLP_ENDPOINT`, loaded
via dynamic `import()` only when configured, so the heavyweight OTel
packages stay out of the default dependency graph.

**Alternatives considered**

Related open PRs found during the duplicate-PR search approach
observability differently: #4894 adds OTLP instrumentation to Paperclip
core unconditionally, and #3752 proposes an observability plugin. Not
duplicates — different layering: this PR keeps the default install
dependency-free via opt-in dynamic import.

## What Changed

- New `server/src/instrumentation.ts` — opt-in OpenTelemetry
auto-instrumentation. Gated on `OTEL_EXPORTER_OTLP_ENDPOINT`. Respects
the standard OTel env vars (`OTEL_SERVICE_NAME`, `OTEL_SERVICE_VERSION`,
`OTEL_EXPORTER_OTLP_ENDPOINT`). Skips the fs/dns/net
auto-instrumentations (too chatty). `sdk.start()` is wrapped in
try/catch so a bad endpoint or missing native bindings doesn't crash the
server. `process.once("SIGTERM" / "SIGINT", …)` for clean shutdown on
the first signal only. OTel packages are loaded via dynamic `import()`
so they are true optional runtime dependencies — no entries in
`package.json`, no lockfile churn.
- `server/src/index.ts` — import `./instrumentation.js` as the very
first statement so auto-instrumentation can patch `http` / `express` /
`pg` before they are evaluated by downstream modules.

## Verification

- `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 pnpm start` after
`pnpm install
@opentelemetry/{sdk-node,auto-instrumentations-node,exporter-trace-otlp-grpc,resources,semantic-conventions}`
in `server/` — traces show up in the configured collector; HTTP,
Express, and Postgres spans are populated.
- `OTEL_EXPORTER_OTLP_ENDPOINT` unset — server starts with no
OTel-shaped output in logs, no behavior change.
- `OTEL_EXPORTER_OTLP_ENDPOINT=…` set but packages not installed —
single `console.warn` at startup telling the operator which packages to
install.

## Risks

Low. No behavior change unless the env var is set. The bootstrap never
throws into the caller; every failure path ends in `console.warn` /
`console.error` and falls through to non-traced operation.

## Model Used

Claude Opus 4.6 (1M context), extended thinking mode.

## Checklist

- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Tests run locally and pass
- [x] CI green
- [x] Greptile review addressed

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 10:44:22 -07:00
Jannes Stubbemann 937fe62d10 feat(server): TRUST_PROXY supports CIDR list + named subnets (supersedes #3729) (#5872)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Express looks at incoming `X-Forwarded-For` headers only when
`app.set("trust proxy", …)` says it should, and uses that resolved
client IP downstream for rate-limiting, audit logging, and any
auth/abuse signal that ties back to source IP
> - The original PR #3729 added `TRUST_PROXY` accepting only `"true"` or
a positive integer, which forces operators to pick between two unsafe
defaults: hop-count (brittle if topology changes) or boolean-true (any
client can spoof `X-Forwarded-For` and bypass rate-limits or pollute
audit logs)
> - `trust proxy: true` is one of the most common Express
misconfigurations and trivially exploitable for IP-spoofing-based
rate-limit bypass; the safest config — trust only the LB's actual CIDR
or only loopback — was unreachable with the previous parser
> - This pull request replaces the parser with full Express 5 support —
unset / `false` / `0` (Express default), positive integer hop count,
comma-separated CIDR list, named subnets (`loopback`, `linklocal`,
`uniquelocal`) — and emits a startup error naming the offending token on
invalid input
> - The benefit is that operators can now trust *only* their actual
ingress and close the spoofing window without leaking client-IP
integrity to downstream layers, while preserving every
previously-working config as a strict superset

## Linked Issues or Issue Description

Refs #1690 — login returns 500 behind a reverse proxy because Express
`trust proxy` is not enabled; this PR ships the configuration surface
(`TRUST_PROXY` with CIDR lists and named subnets) that lets operators
enable it safely. It does not change the default, so #1690 still
requires the operator to set `TRUST_PROXY` — hence Refs, not Fixes.

No other existing issue covers this directly — remaining problem
described in-PR:

- The original `TRUST_PROXY` parser (PR #3729, which this PR supersedes)
accepted only `"true"` or a hop count, forcing operators to choose
between brittle hop-counting and the spoofable `trust proxy: true`.
- `trust proxy: true` lets any client spoof `X-Forwarded-For` and bypass
rate limits or pollute audit logs; the safest config — trusting only the
LB's actual CIDR or only loopback — was unreachable with the previous
parser.

Duplicate-PR search: #1854 / #1714 are earlier minimal trust-proxy
enablement PRs; this PR supersedes #3729 and generalizes beyond a
boolean enable (CIDR lists + named subnets).

## What Changed

- **`server/src/middleware/trust-proxy.ts`** — new helper exposing
`parseTrustProxyEnv` (testable) and `applyTrustProxy(app)` (one-call
boot wiring). Surface:
- Unset / `""` / `false` / `0` → no `app.set("trust proxy", …)` (Express
default: trust nothing).
- `true` → `app.set("trust proxy", true)`. Documented as unsafe in
untrusted-LB deployments.
- Positive integer (e.g. `"2"`) → hop count. Strict parse: rejects
`"01"`, leading/trailing whitespace.
- Comma-separated list of CIDRs and/or named subnets (e.g.
`"loopback,uniquelocal,10.0.0.0/8,fd00::/8"`) → array passed to
`app.set("trust proxy", [...])`.
  - Anything else → startup error naming the offending token.
- **`server/src/app.ts`** — one import + one call to
`applyTrustProxy(app)`.
- **`server/src/__tests__/trust-proxy.test.ts`** — 12 cases: unset,
`"true"`, `"0"`, `"2"`, `"01"` rejected, `" 2 "` rejected, `"loopback"`,
`"loopback,uniquelocal"`, `"10.0.0.0/8"`, `"10.0.0.0/8,fd00::/8"`,
`"bogus"` rejected (error names the bad token), mixed-list with one bad
token rejected (error names the offending token specifically).

## Verification

- `pnpm --filter @paperclipai/server run typecheck` — clean.
- `npx vitest run trust-proxy` — 12/12 pass.

## Risks

- **No new required env vars.** Unset means default Express behavior
(trust nothing). Pure superset of #3729's surface — anything that worked
under #3729 still works here.
- **Strict parse.** `"01"` and `" 2 "` are rejected on purpose so
configuration mistakes surface at startup, not as silently-degraded
auth/rate-limit behavior. The error message names the offending token.
- **No runtime cost** — the parse runs once at boot. The downstream
`trust proxy` setting is internal to Express.
- Single-tenant local-first deploys unaffected by default.

## Model Used

Claude Opus 4.7 (1M context), extended thinking mode.

## Checklist

- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Checked ROADMAP.md — not in conflict with planned core work
- [x] Tests run locally and pass (`trust-proxy` 12/12)
- [x] Added boundary cases (leading-zero, whitespace, unknown token,
mixed-list-with-bad-token)
- [x] No UI changes
- [x] Documented risks above
- [x] Will address all Greptile and reviewer comments before merge

Closes #3729.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 10:37:55 -07:00
Jannes Stubbemann 3701be76fa fix: read-only agent config/skill endpoints should not require agents:create (#3725)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Access to agents, skills, and configurations is governed by a
per-company permission system
> - `agents:create` is a mutation-tier permission that controls who can
create or modify agents
> - `assertCanReadConfigurations` delegates to
`assertCanCreateAgentsForCompany`, effectively requiring `agents:create`
just to *read* agent configs, skills, and config revisions
> - That's a permission regression: any company member without
`agents:create` hits 403 on the Skills tab, agent config pages, and
revision history — but those responses are already secret-redacted
> - This pull request loosens the read gate to company membership only,
while keeping every mutation-adjacent gate at `agents:create`

## Linked Issues or Issue Description

No existing issue covers this directly — problem described in-PR
following the bug-report template:

**What happened**

`assertCanReadConfigurations` delegates to
`assertCanCreateAgentsForCompany`, effectively requiring the
mutation-tier `agents:create` permission just to *read* agent configs,
skills, and config revisions. Any company member without `agents:create`
hits 403 on the Skills tab, agent config pages, and revision history —
even though those responses are already secret-redacted
(`redactAgentConfiguration`, `redactConfigRevision`).

**Expected behavior**

Read-only configuration/skill/revision endpoints are readable by any
company member; only mutation-adjacent endpoints require
`agents:create`.

**Steps to reproduce**

As a company member without `agents:create`, open the Skills tab or an
agent config page (or `GET` the config/skill/revision endpoints) — the
request fails with 403.

## What Changed

- `server/src/routes/agents.ts`:
- `assertCanReadConfigurations` now requires company membership only
(plus the existing agent-key cross-company check). Previously it
required `agents:create`.
- `actorCanReadConfigurationsForCompany` (the boolean twin, used by `GET
/agents/:id` to decide whether to return a restricted detail) now uses
the standard try/catch-around-`assertCompanyAccess` pattern.
- `POST /companies/:companyId/adapters/:type/test-environment` is not a
pure read (it exercises adapter secrets) and now calls
`assertCanCreateAgentsForCompany` directly instead of going through
`assertCanReadConfigurations`. Behavior for this endpoint is unchanged.

## Verification

- Existing tests pass.
- Manual: log in as a company member without an `agents:create` grant.
Visit the Skills tab on an agent and the agent configuration panel —
both load. Try to edit the agent — blocked, as before.
- Manual: POST to
`/companies/:companyId/adapters/:type/test-environment` as the same user
— still 403.

## Risks

Low. The only behavior change is on read endpoints whose responses were
already redacted (\`redactAgentConfiguration\`,
\`redactConfigRevision\`). No secret escapes anywhere.

## Model Used

Claude Opus 4.6 (1M context), extended thinking mode.

## Checklist

- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Tests run locally and pass
- [x] CI green
- [x] Greptile review addressed

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 10:36:49 -07:00
Jannes Stubbemann c21f70ef1c fix: skip gosu when already running as target user (#2908)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The reference container image must be deployable on both Docker
Compose (where it starts as root and `gosu`'s a `USER_UID`/`USER_GID`
switch) and Kubernetes (where the pod is typically constrained by
PodSecurity)
> - The Kubernetes operator (paperclipinc/paperclip-operator#45) sets
`runAsNonRoot: true`, `runAsUser: 1000`, `allowPrivilegeEscalation:
false`, and `drop: ALL` capabilities by default — the unconditional
`usermod` + `gosu` flow in the entrypoint requires root + `CAP_SETUID` /
`CAP_SETGID`, making the image undeployable on any cluster enforcing
baseline or restricted PodSecurity
> - Without root, neither the user remap nor `gosu` can ever succeed —
so the fix is a runtime branch: non-root starts `exec` the command
directly (warning if the runtime UID/GID differs from the requested
one), while root starts keep the existing `usermod`+`gosu` flow
> - This also covers platforms that assign arbitrary UIDs (OpenShift
restricted SCC), which previously crashed with a cryptic `usermod:
Permission denied`
> - The benefit is one image that works for both deployment shapes with
no operator-side workaround — pure superset, no breaking change

## Linked Issues or Issue Description

Refs paperclipinc/paperclip-operator#45 (cross-repo) — the operator's
default pod security context (`runAsNonRoot: true`, `runAsUser: 1000`,
`allowPrivilegeEscalation: false`, `drop: ALL`) is blocked by this
entrypoint behavior. The operator shipped a stopgap
(paperclipinc/paperclip-operator#46 lets the CRD override the security
context); this PR is the image-side fix that makes the secure defaults
work out of the box. Supersedes #2904 (v1 of this branch).

No in-repo issue covers this directly — problem described in-PR
following the bug-report template:

**What happened**

The entrypoint unconditionally runs `usermod`/`groupmod`/`chown` + `exec
gosu node`, which requires root plus `CAP_SETUID` / `CAP_SETGID` — any
non-root start crashes (`gosu` cannot drop privileges; a mismatched UID
dies earlier at `usermod: Permission denied`), making the reference
image undeployable on clusters enforcing baseline or restricted
PodSecurity.

**Expected behavior**

A non-root container `exec`s the command directly (with a clear warning
if its UID/GID differs from the requested `USER_UID`/`USER_GID`, since a
remap is impossible without root). The existing root + `usermod` +
`gosu` flow is preserved for Docker Compose, where the container starts
as root and switches to the requested UID/GID.

**Deployment mode**

Kubernetes with baseline/restricted PodSecurity and OpenShift-style
arbitrary-UID platforms (failing cases); Docker Compose root-start (must
keep working).

## What Changed

- **`scripts/docker-entrypoint.sh`** — branch on the runtime UID:
- **Non-root start** → `exec "$@"` directly. If the runtime UID/GID
differs from `USER_UID`/`USER_GID`, print a one-line warning to stderr
first (the remap is impossible without root; the warning keeps
volume-permission mismatches diagnosable instead of failing cryptically
inside `usermod`).
- **Root start** → unchanged: `usermod`/`groupmod` remap when requested,
`chown` of `/paperclip` when a remap happened, then `exec gosu node
"$@"`.

## Verification

**Automated:** `server/src/__tests__/docker-entrypoint.test.ts` runs the
real entrypoint with `id`/`usermod`/`groupmod`/`chown`/`gosu` stubbed
via PATH and asserts all five privilege branches (root+defaults,
root+remap, non-root match, arbitrary non-root UID, GID mismatch) — runs
in the regular server suite, no Docker needed.

**Manual (Docker):** ran the entrypoint on `node:lts-trixie-slim` (the
image's actual base) across the full matrix, with `gosu` stubbed to a
marker:

- [x] Root start, defaults → no remap, `gosu node` invoked (Docker
Compose flow unchanged)
- [x] Root start, `USER_UID=1001`/`USER_GID=1001` → `Updating node
UID/GID to 1001` + `gosu node` (remap flow unchanged)
- [x] Non-root `--user 1000:1000` (the operator's `runAsUser: 1000`
shape) → silent direct exec, command runs as 1000:1000
- [x] Non-root `--user 1234:1234` (arbitrary UID) → warning `running
unprivileged as 1234:1234; cannot remap to requested 1000:1000`, then
direct exec (previously: crash)
- [x] Non-root `--user 1000:1001` (GID mismatch) → warning, then direct
exec
- [x] Baseline check: master's entrypoint fails for any non-root start
(gosu/usermod require root)

End-to-end cluster verification under restricted PodSecurity exercises
the same branch as the `--user 1000:1000` case above; the operator
repo's deploy is the natural place for that smoke test once this ships
in an image tag.

## Risks

- **Backward-compatible.** Docker Compose / root-entrypoint path is
byte-for-byte the same flow — `usermod`/`gosu` runs whenever the
container starts as root.
- **Behavior change only for previously-broken starts.** Non-root
containers used to crash; they now run. The only observable difference
for a *working* deployment is none.
- **Mismatched non-root UID/GID warns instead of failing.** Deliberate:
the remap is impossible without root, and arbitrary-UID platforms
(OpenShift) rely on group-writable volumes; a hard fail would keep them
broken. The stderr warning preserves diagnosability.
- **No new env vars, no API surface.** Pure entrypoint behavior change
gated on the runtime UID.
- **Restricted PodSecurity ready.** The non-root branch needs no Linux
capabilities — works under `drop: ALL`.

## Model Used

Claude Opus 4.6; rebase, non-root generalization, and verification
matrix by Claude Fable 5 (1M context).

## Checklist

- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Checked ROADMAP.md — not in conflict with planned core work
(agent-runtime sandbox images use `tini`, no gosu — unaffected)
- [x] Tests run locally and pass (new `docker-entrypoint.test.ts`
covering all five privilege branches, plus a manual Docker matrix on the
real base image; see Verification)
- [x] No UI changes
- [x] Documented risks above
- [x] Will address all Greptile and reviewer comments before merge

Unblocks the default (non-overridden) security context of
paperclipinc/paperclip-operator#45 / #46.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-12 10:33:52 -07:00
Jannes Stubbemann 05bcd3ce84 feat(security): plugin tables get company_id FK for tenant isolation (#5865)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The plugin subsystem persists state into four tables
(`plugin_entities`, `plugin_job_runs`, `plugin_logs`,
`plugin_webhook_deliveries`) and those rows currently have no notion of
an owning tenant — so company-deletion doesn't cascade plugin state, and
operators have no way to query "what does this plugin own for company
X?"
> - The fix is one thin slice of tenant-isolation hygiene that doesn't
change any external API: add a nullable `company_id` FK with `ON DELETE
CASCADE` to the four plugin tables, index it, and scope the
`plugin_entities` external-id uniqueness per-tenant
> - The benefit is plugin-row tenant attribution + cascade cleanup, with
zero impact on single-tenant local-first deploys (`NULL` continues to
mean instance-scope)

> **Rebase note (scope narrowed):** This PR originally also hardened the
schedulers (`heartbeat.tickTimers` / `resumeQueuedRuns` /
`enqueueWakeup` and `routines.tickScheduledTriggers`) to skip archived
companies. That half has since landed on `master` via #7478 (`93206f73`,
"Stop archived companies from waking agents") with a stricter
implementation (`status != 'active'` plus a skipped-request audit row).
On rebase those changes were dropped as redundant — `heartbeat.ts` and
`routines.ts` are now identical to `master`, and the scheduler-specific
tests were removed. **This PR is now DB-only.**

## Linked Issues or Issue Description

No existing issue covers this directly — problem described in-PR:

- Four plugin tables (`plugin_entities`, `plugin_job_runs`,
`plugin_logs`, `plugin_webhook_deliveries`) persist rows with no notion
of an owning tenant.
- Company deletion therefore does not cascade plugin state, and
operators have no way to query "what does this plugin own for company
X?"
- One thin slice of tenant-isolation hygiene fixes this without changing
any external API: a nullable `company_id` FK with `ON DELETE CASCADE`,
an index, and per-tenant scoping of the `plugin_entities` external-id
uniqueness.
- Part of the multi-tenant hardening initiative alongside #3967
(cross-tenant 404 oracle) and #5864 (per-company JWT keys).

## What Changed

**Schema (`packages/db/src/schema/plugin_*.ts`):**
- Nullable `companyId` FK with `onDelete: "cascade"` added to
`plugin_entities`, `plugin_job_runs`, `plugin_logs`,
`plugin_webhook_deliveries`.
- A btree index on each new `company_id` column (`<table>_company_idx`).
- `plugin_entities_external_idx` rescoped from `(plugin_id, entity_type,
external_id)` to `(company_id, plugin_id, entity_type, external_id)` and
switched to `UNIQUE … NULLS NOT DISTINCT` so instance-scope rows
(`company_id IS NULL`) keep their dedup guarantee while tenants get
their own namespace.

**Migration:**
- `0095_plugin_company_id_tenant_isolation.sql` — 14 statements: 4
column adds + 4 FK constraints (`ON DELETE CASCADE`) + 4 indexes +
drop/recreate of the external-id unique constraint.
- Journal entry + regenerated `0095_snapshot.json`.

**Tests:**
- `server/src/__tests__/plugin-tenant-isolation.test.ts` — `NULL`
preserves backward compat; `CASCADE` on company delete across all four
tables; per-tenant external-id namespacing; NULL-NULL collision rejected
(`NULLS NOT DISTINCT`).

## Verification

- `pnpm --filter @paperclipai/db run check:migrations` — pass.
- `pnpm --filter @paperclipai/db typecheck` (`tsc`) — pass.
- `vitest run plugin-tenant-isolation` — **4/4 pass** (embedded Postgres
applies `0095` and exercises cascade + NULLS NOT DISTINCT).

## Notes

- **Clean snapshot, no drift.** The earlier revision of this PR shipped
a ~17.6k-line meta snapshot that was almost entirely pre-existing drift.
On rebase the migration was renumbered (the old `0090_brainy_darkhawk`
collided with `master`'s `0090_resource_memberships … 0094`) and
regenerated from the current `master` baseline via `drizzle-kit
generate`. The result is a 14-statement migration containing **only**
the plugin-table changes — no unrelated drift.
- **Backward-compatible.** `NULL company_id` continues to mean
instance-scope (cron jobs, public webhooks). No new env vars, no API
surface change. Single-tenant local-first deploys unaffected.

## Risks

- **Migration is additive and nullable** — `0095` adds nullable
`company_id` columns, FK constraints, and indexes; existing rows stay
valid (`NULL` keeps meaning instance-scope) and no backfill is required.
- **`ON DELETE CASCADE` is a behavioral change**: deleting a company now
removes its plugin rows across all four tables. Intended (it is the
point of the PR), but operators relying on plugin rows surviving company
deletion would be affected. Covered by the cascade tests.
- **Uniqueness semantics change on `plugin_entities`**: the external-id
constraint is rescoped per-tenant and switched to `UNIQUE … NULLS NOT
DISTINCT`, so two instance-scope rows (`company_id IS NULL`) with the
same external id are now rejected instead of coexisting. Covered by the
NULL-NULL collision test.
- **No API surface change, no new env vars.** Single-tenant local-first
deploys unaffected.

(Section added retroactively to match the PR template; distilled from
the What Changed / Notes sections above.)

## Model Used

Same authoring setup as #5864 (same series, same day): Claude Opus 4.7
(1M context), extended thinking mode. (Section added retroactively.)

## Checklist

- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] Tests run locally and pass (plugin-tenant-isolation 4/4)
- [x] `check:migrations` + db typecheck pass
- [x] No UI changes
- [x] Migration carries only the intended changes (no snapshot drift)
- [x] Scheduler half dropped as superseded by #7478

Part of the multi-tenant hardening initiative — see also #3967
(cross-tenant 404 oracle) and #5864 (per-company JWT keys).

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Devin Foley <devin@paperclip.ing>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-06-12 10:17:19 -07:00
Aron Prins a5b3cc98b0 fix(server): cache Intl.DateTimeFormat per timezone in cron minute-stepper (#8033) (#8034)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The routine scheduler lets agents and users run work on cron
schedules; every 30s a tick computes each trigger's next occurrence
> - `computeNextRun`/`nextCronTickInTimeZone` find that occurrence by
stepping forward one minute at a time (capped at 2.6M iterations),
constructing a fresh `Intl.DateTimeFormat` on every step — ~1ms of ICU
work each
> - Sparse schedules (monthly ≈ 43k steps ≈ 40s) and never-matching
crons (the #7529 midnight bug → full 2.6M steps ≈ 45 min) block the Node
event loop for the whole scan, every tick
> - In production this pegs the server at 100% CPU, health checks time
out, and Paperclip Desktop repeatedly shows "the embedded server is no
longer responding" (diagnosed via a CPU sample: 74% of samples inside
`Builtin_DateTimeFormatConstructor`)
> - This pull request caches the formatter per timezone, since
`Intl.DateTimeFormat` instances are immutable and reusable
> - The benefit is each minute-step pays only `formatToParts` (~1µs): a
43k-step scan drops from ~40s of blocked event loop to under a second,
and the server stays responsive while routines are active

## Linked Issues or Issue Description

- Fixes: #8033
- Refs #7529 — a never-matching midnight cron forces the minute-stepper
through its full 2.6M-iteration cap, which is the worst-case trigger for
this perf bug; the two compound
- Refs #7922 — in-flight fix for #7529 touching the same function
(`getZonedMinuteParts`); the changes are compatible (this PR changes
formatter construction, that PR changes hour normalization)

## What Changed

- `server/src/services/routines.ts`: added a per-timezone
`Intl.DateTimeFormat` cache (`getZonedMinuteFormatter`) used by
`getZonedMinuteParts` and `assertTimeZone`, replacing per-call
construction
- Exported `nextCronTickInTimeZone` so the behavior is testable (same
export PR #7922 makes)
- Added `server/src/services/routines-formatter-cache.test.ts`: verifies
a sparse monthly cron resolves to the correct next occurrence across a
DST-bearing timezone, and asserts at most one formatter construction for
a ~43k-minute-step scan (and zero on a warm cache)

## Verification

- `npx vitest run server/src/services/routines-formatter-cache.test.ts`
— 2 tests pass
- `pnpm --filter @paperclipai/server exec tsc --noEmit` — clean
- Live validation: applied the same patch to the server bundled in
Paperclip Desktop 3.2.9, which was hitting 99.3% CPU with 5s
health-check timeouts within 60s of boot on a real workload; after the
patch, CPU idles at 0–3% across scheduler ticks and `/api/health`
answers in ~1ms (observed over multiple 30s ticks)

## Risks

- Low risk. `Intl.DateTimeFormat` instances are immutable and safe to
reuse; the cache key is the timezone string, and entries are small and
bounded by the number of distinct timezones in use
- Invalid timezones still throw in the constructor before anything is
cached, so `assertTimeZone` semantics are unchanged
- Does not change cron matching semantics; minute-stepping remains
(replacing it with cron-field arithmetic is noted in #8033 as a
follow-up)

## Model Used

- Claude (Anthropic), model ID `claude-fable-5` (Fable 5), via Claude
Code CLI with extended thinking and tool use (profiling, patching, and
live verification performed by the model under user supervision)

## 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
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (N/A — server-only change)
- [ ] I have updated relevant documentation to reflect my changes (N/A —
internal perf fix, no doc surface)
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [ ] 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: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:15:53 -07:00
Devin Foley 3fbab2e6db fix: resolve orphan-sweep null-assignee filter regression (#8018)
> Resubmits #5925 by @digitalflanker-ux (rebased onto current `master`;
original commit authorship preserved).

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The issues list API powers orphan-sweep and board inbox views that
filter by assignee
> - `assigneeAgentId=null` is a valid query-string sentinel for
"unassigned issues"
> - A regression caused that sentinel to throw 500 instead of filtering
correctly
> - This pull request restores null-sentinel parsing in the route and
service layers
> - The benefit is reliable orphan-sweep and unassigned-issue queries
without server errors

## Linked Issues or Issue Description

Refs #5891 (paired fix — land together)

**Bug:** `GET /api/companies/:id/issues?assigneeAgentId=null` returned
HTTP 500. Expected: HTTP 200 with only unassigned issues. Malformed
UUIDs should return 4xx, not 500.

## What Changed

- Parse `assigneeAgentId=null` in the issues list route and pass a JS
`null` filter to the service
- Handle malformed assignee IDs with HTTP 422 in the route layer
- Extend `issueService.list` to treat `assigneeAgentId: null` as `IS
NULL` SQL filter
- Add route-level and service-level regression tests

## Verification

- `pnpm exec vitest run
server/src/__tests__/issue-list-assignee-filter-routes.test.ts
server/src/__tests__/issues-service.test.ts`
- result: 2 files passed, 79 tests passed

## Risks

Low risk — scoped to query-parameter parsing and list filtering; no
schema or API contract changes beyond fixing the regression.

## Model Used

None — human-authored original fix by @digitalflanker-ux; rebased and
test-harness adjustments by Paperclip cluster cleanup.

## 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
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] 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

## Cross-references and status (maintainer)

- Pairs with #5891 — both fix the `assigneeAgentId=null` issues-list
regression and should land together.
- Supersedes #5925 (fork branch could not be force-pushed; this is the
operator-mergeable resubmission).

---------

Co-authored-by: openclaw <digitalflanker@gmail.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-06-12 09:20:00 -07:00
roy493 d9ea1bf9e1 fix: skip same-run self-comments (Path A heartbeat-reopen + Path B implicit-todo move) (#4973)
## Thinking Path

- Paperclip treats issue comments as both communication and wake
signals, so comment attribution affects whether completed work reopens.
- The bug lived in two independent paths: deferred comment wake
promotion in `heartbeat.ts`, and implicit reopen-on-comment logic in
`routes/issues.ts`.
- Both paths need the same core rule: a comment from the same run that
just closed the issue must not look like a fresh human follow-up.
- Deferred wake batches also need one extra safeguard: if a batch mixes
a same-run self-comment with a real human comment, the human follow-up
must still reopen the issue.

## What Changed

- `server/src/services/heartbeat.ts` now suppresses deferred reopen only
when every referenced comment in the batch was created by the closing
run.
- `server/src/routes/issues.ts` now passes `actorRunId`,
`checkoutRunId`, and `executionRunId` into
`shouldImplicitlyMoveCommentedIssueToTodo`, and skips the implicit move
when the comment came from the run that already owns the issue.
- `server/src/__tests__/heartbeat-comment-wake-batching.test.ts` adds
coverage for both Path A cases: same-run self-comment stays closed,
while a mixed self-comment plus human-comment batch still reopens.
- `server/src/__tests__/issue-comment-reopen-routes.test.ts` covers the
same-run guard on both POST and PATCH comment paths, plus the negative
case where a different run still reopens.

## Verification

```bash
pnpm --filter @paperclipai/server exec vitest run src/__tests__/issue-comment-reopen-routes.test.ts
pnpm --filter @paperclipai/server exec vitest run src/__tests__/heartbeat-comment-wake-batching.test.ts -t "self-authored by the closing run|mixes self-authored and human comments"
pnpm --filter @paperclipai/server typecheck
```

## Risks

- Low risk. Both changes are additive guards and preserve existing
behavior for comments that do not originate from the owning run.
- The deferred-wake change now uses all-self semantics, which is the key
correctness detail for mixed batches.
- Full CI is still the authoritative validation for the broader
heartbeat integration surface.

## Model Used

- OpenAI Codex, GPT-5-based coding agent (`codex_local` adapter in
Paperclip).

## 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
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots — N/A, server-only
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

## Cross-references and status (maintainer)

Refs #6601
Refs #3980

---------

Co-authored-by: Paperclip <paperclip@users.noreply.github.com>
2026-06-12 00:33:08 -07:00
Sherman Lye fecc41d4fd fix(recovery): skip stranded-issue recovery when pending wake interaction exists (#4854)
## Thinking Path

> - Paperclip is the open source control plane people use to manage AI
agents and their work.
> - Recovery logic is part of that control plane because it decides when
agent work is truly stranded versus intentionally waiting.
> - Issues can pause on human-gated thread interactions such as
`request_confirmation`, `ask_user_questions`, and `suggest_tasks`.
> - `reconcileStrandedAssignedIssues()` was treating some of those
waiting issues as stranded because it did not check for pending
wake-style interactions.
> - That mismatch created false-positive recovery cascades on work that
was correctly paused for human input.
> - This pull request adds the missing guard and locks it in with
focused regression coverage.
> - The benefit is safer recovery behavior: real stranded work is still
recovered, while human-gated work stays stable and inspectable.

## Linked Issues or Issue Description

- Refs #7403
- Searched open GitHub PRs/issues for the same recovery-interaction bug
before merge prep; no duplicate open PRs found.

## What Changed

- Added `hasPendingWakeInteraction(companyId, issueId)` in
`server/src/services/recovery/service.ts` to detect pending thread
interactions with continuation policies `wake_assignee` and
`wake_assignee_on_accept`.
- Inserted that guard into `reconcileStrandedAssignedIssues()`
immediately after the active-execution-path check so human-gated issues
are skipped instead of escalated.
- Added a parameterized regression test in
`server/src/__tests__/heartbeat-process-recovery.test.ts` that covers
both continuation-policy values and verifies recovery does not fire.
- Appended the maintainer cross-reference section required by merge
prep.

## Verification

- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/heartbeat-process-recovery.test.ts -t "skips
stranded recovery when a pending" --pool=forks --isolate`
- Greptile Summary comment on the latest head reports `Confidence Score:
5/5`.
- Remote Paperclip CI is running on head
`4702684c213b5018e6918cb6176e7ef40f440ebf`.

## Risks

- Low risk. The production change is a read-only early exit in an
existing recovery sweep.
- The main behavioral shift is intentional: issues with pending
wake-style interactions will no longer enter stranded recovery until the
human gate clears.
- If there is a hidden interaction state we should also treat as
waiting, it would need an explicit follow-up rather than falling through
this guard.

> Checked `ROADMAP.md`; this is a focused bug fix, not overlapping
roadmap feature work.

## Model Used

- Original PR authoring: Claude Code using Claude Opus 4.6.
- Merge prep, rebase, verification, PR-body repair, and Greptile
follow-up: OpenAI Codex via the Paperclip ACPX local adapter (exact
model ID not exposed in this workspace), with tool use and code
execution.

## 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
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] 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

## Cross-references and status (maintainer)

Refs #7403

Co-authored-by: Sherman Lye <user@example.com>
2026-06-12 00:31:33 -07:00
Ismaël O. 3b7c42be86 fix(openclaw-gateway): complete and stabilize OpenClaw Gateway integration (#2322)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The `openclaw_gateway` adapter is how operators wire Paperclip
agents to an OpenClaw gateway over WebSocket
> - The adapter UI previously only exposed a handful of config fields in
edit mode; many timeout / auth / session-routing knobs were unreachable
through the form
> - The serializer also forgot to inject the configured `authToken` into
the `x-openclaw-token` header, and the server-side execute path lacked
retries on transient gateway errors and an `OPENCLAW_TOKEN` env fallback
> - This pull request exposes the full set of config fields in both
create and edit modes, fixes the serializer, hardens the server-side
execute path, and pins the existing default request timeouts (120s /
120000ms) — see the dedicated commit and the new unit tests
> - The benefit is operators can configure and reconfigure an
`openclaw_gateway` agent end-to-end through the UI, with no silent
change to the defaults documented in the adapter README and
`doc/ONBOARDING_AND_TEST_PLAN.md`

## Linked Issues or Issue Description

Closes #414
Closes #1901
Closes #2309

## What Changed

- **UI**: Removed the `!isCreate` guard so all `openclaw_gateway` config
fields are visible in both create and edit modes (`authToken`,
`agentId`, `sessionKeyStrategy`, `sessionKey`, `timeoutSec`,
`waitTimeoutMs`, `disableDeviceAuth`, `autoPairOnFirstConnect`, `role`,
`scopes`, `paperclipApiUrl`, `headersJson`, `payloadTemplate`,
`runtimeServices`).
- **Serialization**
(`packages/adapters/openclaw-gateway/src/ui/build-config.ts`): inject
`authToken` into headers as `x-openclaw-token`; apply safe defaults on
create (`timeoutSec=120`, `waitTimeoutMs=120000`,
`sessionKeyStrategy="issue"`, `role="operator"`,
`scopes=["operator.admin"]`).
- **Backend**
(`packages/adapters/openclaw-gateway/src/server/execute.ts`): add
`OPENCLAW_TOKEN` env-var fallback for `authToken`, retry logic (max 2
retries with backoff for transient gateway errors), session-key prefix
`agent:{agentId}:{sessionId}` when `agentId` is configured.
- **Defaults restoration** (dedicated commit): an earlier revision of
this PR lowered the default request timeouts to `60` / `30000`. The
current branch restores the historical `timeoutSec=120` /
`waitTimeoutMs=120000` defaults that match the values documented in
`packages/adapters/openclaw-gateway/src/index.ts`,
`src/server/execute.ts` on master, and the worked example in
`doc/ONBOARDING_AND_TEST_PLAN.md`.
- **Tests** (new):
`packages/adapters/openclaw-gateway/src/ui/build-config.test.ts` pins
the documented timeout and identity defaults so the silent-halve
regression cannot recur.

## Verification

- `pnpm --filter @paperclipai/adapter-openclaw-gateway typecheck`
- `pnpm typecheck` (root)
- Manual: create a new `openclaw_gateway` agent — all fields visible,
defaults populate as documented.
- Manual: edit an existing `openclaw_gateway` agent — every field
round-trips correctly and saves.
- Manual: unset `authToken` in the form and set `OPENCLAW_TOKEN` env var
— adapter picks up the env-var fallback.
- Manual: simulate a transient gateway error — execute retries up to 2
times with backoff before failing.

## Risks

- Low risk. Surface area is one adapter, behind explicit operator
configuration. The defaults change in this PR is a restoration of values
that already exist on master and in the adapter docs, so no production
agent sees a behavioral shift relative to the prior release. Field
exposure in edit mode is purely additive — existing values are preserved
on save.

## Model Used

- Provider/model: Claude (Anthropic) — `claude-opus-4-7`
- Mode: standard tool use, no extended thinking
- Capability notes: code execution + repository file edits via Claude
Code

## Cross-references and status (maintainer)

Closes #414
Closes #1901
Closes #2309

## 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
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [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: Paperclip Bot <bot@paperclip.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Devin Foley <devin@paperclip.ing>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-06-11 23:31:39 -07:00
Vasu Yadav d7049e0cae fix(server): adopt stale checkout run ownership (#5413)
## Thinking Path

> - Paperclip is a control plane for autonomous AI-agent companies.
> - Issue checkout ownership is part of the execution-control layer that
prevents two runs from mutating the same task at the same time.
> - The current lock model should preserve `409` conflicts for live
competing owners, but it should not strand the rightful assignee behind
a stale terminal run.
> - A same-agent follow-up run can encounter an existing `checkoutRunId`
from a failed, timed-out, succeeded, or missing heartbeat run.
> - In that case, the new run should safely adopt ownership instead of
failing with an ownership conflict.
> - This pull request makes stale checkout adoption transactional and
keeps live checkout owners protected.
> - The benefit is safer run recovery without weakening single-owner
checkout semantics.

## Linked Issues or Issue Description

- Fixes #5350
- Closes #1508
- Closes #1970
- Closes #2083
- Closes #3158
- Closes #3190
- Related stale-lock PRs reviewed during dedup search: #7536, #6658,
#5660, #5442, #6223, #7048, #6824, #6799

## What Changed

- Updated issue checkout ownership recovery so the current assignee can
adopt a stale terminal or missing checkout run.
- Added row locking around stale checkout adoption to avoid races while
replacing `checkoutRunId` / `executionRunId`.
- Preserved `409` behavior when a different live checkout owner is still
active.
- Prevented terminal actor runs from reclaiming an unowned checkout lock
after the newer eager stale-checkout clear path.
- Fixed the stale checkout test fixture so same-assignee cases do not
insert duplicate agent rows.
- Added/kept focused coverage for stale checkout adoption and live-owner
conflict behavior.
- Fixes #5350.

## Verification

- Focused tests:

```sh
pnpm exec vitest run server/src/__tests__/issues-service.test.ts server/src/__tests__/issue-stale-execution-lock-routes.test.ts
```

Result:

```text
2 passed, 84 tests passed
```

- Server typecheck:

```sh
pnpm --filter @paperclipai/server typecheck
```

Result:

```text
passed
```

- Live curl smoke confirmed same-agent stale checkout adoption returns
`200` instead of `409`.

```text
old_run_status=succeeded
checkout_http=200
patch_http=200
```

The PATCH response showed `checkoutRunId` and `executionRunId` updated
to the new run id.

### Live curl smoke result

<img width="1498" height="570" alt="Live curl smoke showing stale
checkout adoption returned 200"
src="https://github.com/user-attachments/assets/4bf834de-e3cd-4495-ac5a-74767b439eeb"
/>

### Server request log

<img width="631" height="131" alt="Server logs showing heartbeat,
checkout, and patch requests succeeded"
src="https://github.com/user-attachments/assets/ceaaa403-110e-44e8-bac8-5d8506e79cc3"
/>

## Risks

- Low to medium risk: this touches issue execution lock ownership.
- The behavioral shift is intentionally narrow: only the current
assignee can adopt stale terminal or missing checkout ownership.
- Live checkout owners remain protected with `409`.
- No database migration or API contract 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

- OpenAI GPT-5.5 Codex coding agent with repository tool use, shell
execution, code review, and local 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [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

## Cross-references and status (maintainer)

- Closes #1508
- Closes #1970
- Closes #2083
- Closes #3158
- Closes #3190
- Status: rebased onto current master; focused tests and server
typecheck pass locally; all required CI is green; Greptile is 5/5;
master drift verified.

---------

Co-authored-by: Devin Foley <devin@paperclip.ing>
2026-06-11 22:03:07 -07:00
David Bezar 01e59c074a fix(watchdog): suppress repeat alerts when source issue is blocked or evaluation board-closed (#5942)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies; the
stale-active-run watchdog monitors agent heartbeat runs for extended
output silence and fires evaluation issues to alert the responsible
manager
> - The watchdog uses a unique index on open evaluation issues to
prevent duplicate open issues per run, but this only prevents two
*simultaneous* open issues — not sequential ones created after each
closure
> - When a board reviewer closes an alert as done directly (without
recording a watchdog decision), the dismissed_false_positive guard is
bypassed and `findOpenStaleRunEvaluation` returns null on the next scan
— causing a new alert to fire every 30 minutes until the run terminates
> - The previous fix also removed `blockedByIssueIds` mutation from
`ensureSourceIssueBlockedByStaleEvaluation` to break the
block→silence→new-alert loop, but that left no idempotency guard on the
source-issue escalation comment, so every critical scan re-appended the
comment to the source-issue thread
> - Additionally, runs whose source issue is already `blocked` (agent is
correctly idle, waiting on a human action) should never generate alerts
at all — silence is expected in that state
> - This PR fixes all three gaps in `createOrUpdateStaleRunEvaluation` /
`ensureSourceIssueCommentedForStaleEvaluation`: (1) skip when source
issue is `blocked`, (2) auto-record a dismissed_false_positive decision
when a closed evaluation exists with no prior watchdog decision, (3) add
an activity-log-backed idempotency guard so the source-issue escalation
comment fires exactly once per (sourceIssue, evaluationIssue) pair
across scan cycles and process restarts
> - The benefit is that agents correctly paused waiting on board-gated
blockers no longer generate repeated false-positive noise tickets,
board-closed evaluations are permanently suppressed without requiring a
second interaction, and source-issue threads no longer get spammed with
duplicate escalation comments

## What Changed

- `server/src/services/recovery/service.ts`:
- Added `blocked` source-issue guard: `if (sourceIssue?.status ===
"blocked") return { kind: "skipped" }` — idle output is expected when
the source issue is blocked
- Added `findClosedStaleRunEvaluation()` — queries for `done` evaluation
issues for a given run, ordered by most recent update (scoped to `done`
only so system-cancelled evaluations don't permanently suppress alerts)
- Added `hasDismissedFalsePositiveDecision()` — queries for an existing
dismissed_false_positive watchdog decision record
- Added closed-evaluation auto-dismiss: when a prior evaluation was
closed `done` on the board without any watchdog decision, auto-inserts a
dismissed_false_positive record so future scans skip via the existing
guard. The check-then-insert runs inside a transaction guarded by a
per-(company, run) `pg_advisory_xact_lock` so two overlapping scans
cannot both observe `hasAnyDecision = false` and both insert duplicate
rows
- Removed `blockedByIssueIds` mutation from the escalation path and
renamed `ensureSourceIssueBlockedByStaleEvaluation` →
`ensureSourceIssueCommentedForStaleEvaluation` to reflect that the
function now only adds a comment + activity log (no state mutation) —
evaluation issues are observability-only and adding them as hard
blockers created a self-amplifying loop (blocked→silent→new
alert→blocked again)
- Added activity-log-backed idempotency guard at the top of
`ensureSourceIssueCommentedForStaleEvaluation`: query the activity log
for a `heartbeat.output_stale_escalated` row with the same (sourceIssue,
evaluationIssue) pair and return false when one is present. The single
activity-log row written on the first successful escalation is the
suppression record for all later scans, surviving process restarts
- `server/src/__tests__/heartbeat-active-run-output-watchdog.test.ts`:
- Added: "emits the source-issue escalation comment only once across
repeated critical scans" (covers the comment-spam regression path)
  - Added: "skips ticket creation when the source issue is blocked"
- Added: "suppresses repeat alerts when evaluation is closed on the
board without a watchdog decision"
- Added: "still allows re-arm after continue decision even when
evaluation is board-closed" (exception path: if any watchdog decision
exists, human opted in to lifecycle — honour it)

## Verification

```sh
pnpm exec vitest run server/src/__tests__/heartbeat-active-run-output-watchdog.test.ts
```

- All 18 watchdog tests pass locally
- Regression: source-issue escalation comment emits exactly once across
repeated critical scans
- Blocked source → no evaluation created (result.created === 0,
result.skipped === 1)
- Board-closed evaluation + no decisions → auto-records
dismissed_false_positive; second scan creates nothing
- Board-closed evaluation + continue decision → second scan still
creates (re-arm preserved)

## Risks

- **Low risk.** The blocked-status guard is a pure early-return that
adds no state mutation. The auto-dismiss path only inserts a record when
no decisions exist — it cannot fire for runs where a human has opted in
to the watchdog lifecycle via snooze/continue. Removing
`blockedByIssueIds` from the critical-escalation path is safe because
evaluation issues are already parented under the source issue.
- The `dismissed_false_positive` auto-insert is now race-safe under
concurrent scans via `pg_advisory_xact_lock` keyed on `(companyId,
runId)` so the check-then-insert pair is serialized without requiring a
schema change.
- `findClosedStaleRunEvaluation` is scoped to `done` only (not
`cancelled`) so system code paths that cancel evaluation issues cannot
permanently suppress future watchdog alerts for the same run.

## Model Used

- Provider: Anthropic
- Model: Claude Sonnet 4.6 (`claude-sonnet-4-6`) for original change;
Claude Opus 4.7 (`claude-opus-4-7`) for follow-up review fixes
- Context: full repo read with tool use, running as SADE agent in
Paperclip Claude Code
- Mode: agentic code analysis + targeted edit

## 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
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (no UI changes)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

## Cross-references and status (maintainer)

- Closes #4937
- Closes #5207
- Closes #5767
- Closes #5949

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-06-11 22:00:13 -07:00
Vladimir Balko deef1f479d fix(heartbeat): release execution lock on cross-agent reassignment (#5110)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Each issue can hold an execution lock via `issues.execution_run_id`,
so concurrent wakes for the same task either coalesce into the active
run or wait deferred
> - When the issue is reassigned to a *different* agent (e.g. board
operator changes `assigneeAgentId` from Coder → Reviewer + flips
`status` to `in_review`), the new assignee's wake is correctly sent down
the assignment-wakeup path
> - But the lookup `activeExecutionRun` still finds the previous holder
run as long as it is in `{queued, running, scheduled_retry}` — and
`enqueueAssignmentWakeup` falls through to the deferred-wake branch when
the holder agent does not match the new assignee
> - The trouble is the **queued** holder for the old assignee will never
start (the issue's status / target now belongs to someone else, the
relevant assignment trigger was the original one), so the lock is never
released, the deferred wake is never promoted, and the new assignee
silently never wakes
> - This pull request detects that situation right next to the existing
`cancelStaleScheduledRetry` cleanup: if `activeExecutionRun.status !==
'running'` AND the holder agent differs from `issue.assigneeAgentId`,
cancel the holder run, release the lock, and proceed with a normal
queued wake instead of deferring
> - The benefit is hand-offs across agents become reliable — no more
silent stalls that operators have to unstick by manually cancelling a
queued run

## Linked Issues or Issue Description

- Closes #4058

## What Changed

- One new check in `reapOrphanedRuns()`'s peer function — the
`enqueueAssignmentWakeup` defer-detection block in
`server/src/services/heartbeat.ts` (around the lock-resolution code
immediately following `cancelStaleScheduledRetry`):
- If `activeExecutionRun` exists, its `status !== 'running'`, and
`activeExecutionRun.agentId !== issue.assigneeAgentId`, mark the holder
run as `cancelled` with errorCode `lock_released_on_reassignment`,
cancel its corresponding wakeup request if any, and null
`activeExecutionRun` so the lock-clear branch right below proceeds to
release `executionRunId` / `executionAgentNameKey` / `executionLockedAt`
and the wake gets enqueued normally.
- `running` runs still defer (legitimate concurrency).
- Same-agent queued/scheduled holders still defer (legitimate coalesce).
- Total +37 lines, no API change, no schema change.

## Verification

```sh
# Existing reaper tests still pass — exercises the lock-resolution path
pnpm exec vitest run server/src/__tests__/heartbeat-process-recovery.test.ts --no-coverage
# expected: Tests  39 passed (39)

# New regression test for the cross-agent lock-release race
pnpm exec vitest run server/src/__tests__/heartbeat-lock-release-on-reassignment.test.ts --no-coverage
```

Manual reproduction (matches an incident we hit running a small Coder +
Reviewer company):

1. Coder pickup heartbeat schedule fires; paperclip queues a Coder run
and pre-allocates the lock by recording `issues.execution_run_id =
<queued-coder-run-id>` for the pickup issue.
2. The Coder run sits in `queued` because the agent's slot is busy
elsewhere (`maxConcurrentRuns: 1`).
3. Operator (or CEO) PATCHes the issue: `assigneeAgentId: <coder>` →
`<reviewer>` together with `status: in_progress` → `in_review`.
4. Paperclip creates the Reviewer assignment wakeup, but stores it as
`deferred_issue_execution` because `activeExecutionRun` is the queued
Coder run.
5. **Before this PR**: Reviewer never wakes; the deferred wakeup waits
for the queued Coder lock holder which never starts (the issue is no
longer the Coder's). Operator has to `POST
/api/heartbeat-runs/<queued-coder>/cancel` manually to unstick the
chain.
6. **After this PR**: paperclip recognizes the holder is non-running and
belongs to a now-foreign agent, cancels it inline, releases the lock,
and queues the Reviewer wake normally — Reviewer wakes on the next
heartbeat tick.

## Risks

- **Low**. The new branch only fires when both conditions are true:
- The holder run is **not** `running` — `running` runs still defer (we
never interrupt active work).
- `activeExecutionRun.agentId` is different from the issue's *current*
`assigneeAgentId` — i.e. the assignee was just changed, the old holder
is bound to the prior owner.
- The cancel uses errorCode `lock_released_on_reassignment` so operators
can grep for it; the corresponding wakeup is also cancelled in the same
transaction so we do not leave an orphan wakeup request.
- No DB schema change, no public API change, no UI change.
- Sits next to the existing `cancelStaleScheduledRetry` cleanup pattern,
so the behavior is locally consistent with how stale schedule retries
are already cleared.

## Model Used

- Claude Opus 4.7 (`claude-opus-4-7`), 1M-context build, extended
thinking + tool use enabled. Used to trace the lock-acquire / defer /
promote paths in `heartbeat.ts` from the live incident, design the
minimal-blast-radius fix next to `cancelStaleScheduledRetry`, and
produce this PR description.

## 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 (39 in the directly
affected suite, plus the new regression test)
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots — N/A (server-side wakeup routing)
- [x] I have updated relevant documentation to reflect my changes —
in-line code comment explains the new branch
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

## Cross-references and status (maintainer)

- `Closes #4058`

### Maintainer-added changes on top of the original commit

A second commit was added on top of @vbalko-claimate's original to pin
the cancel `UPDATE` for the queued/scheduled holder to the exact
non-running status read just above it. Without that predicate, a worker
that flipped the holder from `queued` → `running` between the `SELECT`
and the `UPDATE` could have its freshly-claimed `running` row silently
clobbered to `cancelled`. The new commit also gates the wakeup-request
cancellation and the `activeExecutionRun = null` assignment on a
non-empty `RETURNING`, so neither fires when the predicate misses. A
dedicated regression test
(`heartbeat-lock-release-on-reassignment.test.ts`) covers both paths:
the legitimate-running-holder defer case and the queued→running race.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Devin Foley <devin@devinfoley.com>
2026-06-11 21:59:04 -07:00