Registry radix-luma Switch form (border-2 capsule track, oval thumb,
bg-input off-state) on the existing button API — no Radix dep, no
call-site changes. On-state uses var(--status-task-done) per user
ruling instead of bg-primary; deviation documented in the component.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three user rulings from the tune-session gallery (intentional visible
changes; snapshots NOT re-baselined):
A. BudgetIncidentCard hard-stop card was unreadable in LIGHT mode
(pre-existing: dark-tuned red-50/100/200 text over the light pink
gradient, no light variants). Eyebrow/title/description/banner/icon/
validation now use red-600..950 light text with the original dark
classes behind dark:; same treatment for sibling BudgetPolicyCard
statusTone chips + red banner. B1 gradient tokens untouched.
B. Remaining bar-fill stragglers -> canonical status hues:
ClaudeSubscriptionPanel fillClass and ProviderQuotaCard quota-window
fills (red-400 -> bg-(--status-task-blocked), amber/yellow-400 ->
bg-(--status-task-todo), green-400 -> bg-(--status-task-done)).
BudgetSidebarMarker is an icon badge, not a bar — left.
C. Systematic cyan->status-blue liveness sweep (~50 sites / 20 files):
running tones, live dots/pings, Live surfaces (LiveRunWidget,
ActiveAgentsPanel Live-now box, AgentDetail live card, IssueDetail
Live pill), scheduled-retry family, externalObjectStatus running
maps, timeline now-marker #2dd4bf -> #2563eb, and liveness glow
shadow token VALUES --shadow-extract-1/11/14 rgba(6,182,212,0.08) ->
rgba(37,99,235,0.08) (sites unchanged). Non-liveness cyan left and
documented (xterm cursor, on_demand chips, assets chip, import
rename text, recovery_required, ledger Advanced/snoozed, UxLab).
DECISION-SHEET.md: 'Gallery feedback round 2' section. Gates 3/3 CLEAN,
typecheck green, ui build green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sm/md/lg/xl now calc()-derived (monotonic 3.2/5.2/7.2/11.2px at the
preset's 0.45rem), replacing the legacy square-brand lg/xl = 0px.
One token edit retunes every corner in the app.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User approved the status-hue alignment on the before/after contact
sheet. Suite 510/510 against the updated baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
VISIBLE CHANGE, staged for contact-sheet review (DECISION-SHEET.md B5,
DESIGN.md principle 5 — one status vocabulary everywhere): the per-status
chart series in ActivityCharts.tsx re-point at the canonical status
system instead of an independent palette — todo #3b82f6 blue -> amber
(--status-task-todo #f59e0b), in_progress #8b5cf6 violet -> blue
(#2563eb), in_review #a855f7 -> #7c3aed, done #10b981 -> #22c55e,
blocked #ef4444 -> #dc2626, cancelled #6b7280 -> #a8aeb2. 'backlog'
keeps --project-none per the B5 ruling; priority series, success-rate
tints, and the unknown-status fallback are not status hues and are
untouched. The three now-orphaned tokens (--hex-3b82f6/8b5cf6/a855f7)
are deleted; stale status references scrubbed from surviving token
comments. Visual snapshots intentionally NOT updated — diffs exported
for user review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DESIGN.md principle 2 now states Tailwind palette classes are hardcoded
values in spirit — in-scope debt for a dedicated Run 4, not currently
gated — and notes the B1 first-party-decoration allowlist exception.
COMPONENT-INVENTORY.md section 6 items get their decided dispositions
(C1 split deliberate, C2/C3 Run 3 prep investigations, C4 keep five,
C5 Run 3 swap list, C6 dedicated review excluded from Run 3, C7/C8
deliberate custom/intentional exception, C9 deferred to Run 4 with
sonner-facade alternative, agentStatusBadge item marked resolved by A1).
DECISION-SHEET.md: B1 APPROVED (middle path, executed), B4 DEFERRED to
preset session, B5 APPROVED pending contact-sheet review, C1-C10 set.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DECISION-SHEET.md B1 user ruling: first-party intentional one-off
decoration on demo/UX-lab surfaces stays inline; production decoration
tokens stay tokens. Reverted 27 singleton tokens whose only consumers
are *UxLab.tsx pages — 19 gradients (--gradient-extract-5,6,8-24) and 8
shadows (--shadow-extract-15,16,17,19-23) — restoring the original
bracket-literal utilities (verified byte-exact against pre-extraction
commit 38532283e) at 35 call sites, deleting their index.css
definitions, allowlisting the 4 UxLab pages for check-token-gates, and
extending the allowlist criteria doc-comment. KEPT the 22
production-consumed tokens (gradients 1,2,3,4,7,25,26 + 15 shadows);
--shadow-extract-4/5 kept because ChatComposer.tsx and IssueChatThread.tsx
consume them in production, not just ChatComposer.test.tsx. Zero
rendered-output change (visual suite 510/510).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A1-A6 set to APPROVED with per-item results (A4 = policy only, no code
change; A2 consolidated only the provably byte-identical readable-text
pair). B2 set to APPROVED — own Run 4 later; B3 set to APPROVED — ladder
locked, lands with preset session. Verified on the final tree:
check:token-gates 3/3 CLEAN, typecheck green, test:storybook-visual
510/510 vs the Phase 0 baseline at maxDiffPixels 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IssueChatThread's human-message bubble reused --status-task-in_progress
(Batch 1; the shared #2563eb is a semantic coincidence, not a real
dependency). Mint --liveness-blue: #2563eb in the extracted-tokens block
and re-point the bubble class plus the lockstep negative assertion in
IssueChatThread.test.tsx, so a future status-hue change no longer drags
the chat bubble along. Same value today — zero rendered-output change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FileViewerSheet's code-line highlight referenced
--paperclip-code-highlight-bg / --paperclip-code-highlight-border without
either var ever being defined, so rendering always came from the var()
fallbacks. Define both in the extracted-tokens :root block at exactly the
former fallback values (rgba(250,204,21,0.12) / rgb(234,179,8)) and drop
the now-redundant fallbacks from the Batch 4 --code-highlight-*-resolved
wrappers. Call sites in FileViewerSheet.tsx unchanged. Nothing anywhere
sets these vars at runtime (verified via rg), so pixels are identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Of the three flagged contrast-pair sites, only one pair is provably
byte-identical across files: #f8fafc/#111827 (readable light/dark text)
duplicated in lib/color-contrast.ts and lib/worktree-branding.ts. Export
it once from color-contrast.ts as READABLE_TEXT_LIGHT/READABLE_TEXT_DARK
and import it in worktree-branding.ts.
Left untouched as semantically distinct (per DECISION-SHEET.md A2 'verify
per-pair first'): ThemeContext.tsx's <meta theme-color> pair
(#18181b/#ffffff — browser chrome color, not contrast math; and #ffffff
!= #f8fafc), color-contrast.ts's DARK_BG/LIGHT_BG compositing backgrounds
(RGB objects), and worktree-branding.ts's #000000 hex-parse fallback.
Zero rendered-output change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
agentStatusBadge in lib/status-colors.ts was byte-for-byte identical to
brandChipBadge for all 4 of its keys (gray/blue/amber/red) and had zero
importing call sites — delete the duplicate map, keep brandChipBadge as
the single brand .task-chip style source. AgentBadgeColor remains (used
by agentStatusColor/agentStatusCapsule) and is a subset of BrandChipColor.
Zero rendered-output change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Final batch of the design-token extraction: mints tokens for every
remaining value-bearing arbitrary Tailwind bracket in
ui/src/components/** and ui/src/pages/**, then adds a gate script so
the "zero hardcoded value" rule is enforced going forward instead of
re-audited by hand each time.
Codemod: scripts/codemod-extract-misc.mjs (198 sites, 84 files, 96 new
tokens: 58 --gtc-*/3 --gtr-* grid track lists, 19 --tp-*
transition-property lists, 6 --z-* z-index values, --s-*/--e-*/--va-*/
--sw-* scale/ease/vertical-align/stroke-width, 2 --blur-*, 1
--drop-shadow-extract-*, 4 *-resolved wrapper tokens resolving
FileViewerSheet.tsx's half-migrated var(x, fallback) sites Batch 1
deferred, and 2 --gradient-extract-* continuing Batch 1's counter for
2 hsl(var())/color-mix() gradient sites its color-literal audit didn't
catch). Mandatory Step 0 syntax spike confirmed every paren form
byte-equivalent via scratch stories + built-CSS grep, and confirmed
the tw-animate-css plugin utilities (zoom-in-[0.97] etc.) are
currently dead/no-op classes (plugin not installed) — allowlisted,
not rewritten, since there's no visual behavior to preserve or change.
Gate script: scripts/check-token-gates.mjs (new, wired as
`pnpm check:token-gates`) implements the three DONE-WHEN gates
(hex/color-literal, arbitrary-bracket-value, raw-font-size) over
ui/src/components/** and ui/src/pages/**, parsing a new
machine-readable ALLOWLIST block appended to ui/src/index.css that
consolidates Batches 1-3's free-form allowlist prose into
`* allow <path> — <reason>` lines. Resolved TOKEN-AUDIT.md's open
"test file policy" question by allowlisting 43 test-fixture hex-literal
sites across 18 *.test.tsx files (mock prop/API-response data, not
rendered CSS). Gate exits 0 clean.
AGENTS.md's existing Design system section now names the gate command.
TOKEN-AUDIT.md gets a Batch 4 log appendix with counts, allowlist
rationale, and the remaining out-of-scope debt (36 hex sites in
ui/src/lib|context|plugins; the ~3,115-site Tailwind-palette-class
scope question is still unresolved and flagged for a human decision).
Verify: node scripts/check-token-gates.mjs exit 0 (all 3 gates clean);
pnpm build-storybook exit 0; Storybook visual suite 510/510 (Playwright,
tests/storybook-visual); pnpm typecheck (ui/) exit 0; codemod idempotent
on re-run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Batch 3/4 of the design-token extraction (design/token-extraction). Replaces
hardcoded Tailwind arbitrary-bracket values for width/height/min/max,
padding/margin/gap/inset/translate, radius (incl. directional), ring/outline
width, and box-shadow in ui/src/components/** and ui/src/pages/** (including
*.test.tsx companions) with references to new CSS custom-property tokens in
ui/src/index.css. Zero visual change, proven by the Phase 0 Storybook
snapshot baseline.
Counts:
- 407 sites rewritten across 140 files (127 component/page + 13 test files
updated in lockstep with the components they assert against).
- 163 new tokens minted: 127 --sz-* (one shared family across
width/height/spacing/gap/inset/translate lengths, including 42
--sz-calc-N compound calc()/min()/max()/clamp() expressions and 2
--sz-safe-* env(safe-area-inset-*) wrappers), 8 --rad-* (shared across
radius/ring-width/outline-width), 5 --pct-* (incl. --pct-neg-50 kept
distinct per the source bracket's own negative sign), 23
--shadow-extract-* (38 sites, deduped by exact value).
- 11 var()-only passthrough sites (--radix-*-trigger-width/height,
--new-issue-dialog-height) rewritten to the bare paren form with no new
token minted, per DESIGN.md's runtime-library-variable special case.
- 3 sites resolved Tailwind's build-time calc(theme(spacing.N)-Mpx) into a
byte-equivalent runtime calc() (components/IssueRow.tsx), verified via
headless-browser computed-style comparison against the pre-change built
CSS.
- 2 sites skipped/allowlisted: rounded-[inherit] (CSS keyword, not a
literal) and drop-shadow-[...] (different utility than this batch's
shadow-[...] mandate) — logged as a Needs-human-decision item.
Three gotchas found and fixed during verification (all documented in the
codemod header and TOKEN-AUDIT.md's Batch 3 log): a boundary regex bug that
false-positived inside Tailwind's own slide-out-to-top-[1%] compound
utility, a calc()-operator-spacing bug that could corrupt
env(safe-area-inset-*) identifiers, and a generated-CSS-comment bug where a
literal "*/" substring in prose silently dropped the entire token block
from the compiled CSS (root cause of an initial 222-test snapshot failure,
fully resolved to 510/510 passing).
Verify: rg gates clean, pnpm build-storybook exit 0, Storybook visual
snapshot suite 510/510 (509 clean + 1 known-benign flaky retry), pnpm
typecheck exit 0, codemod re-run confirmed idempotent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces hardcoded TYPE values in ui/src/components/** and ui/src/pages/**
with references to new verbatim CSS custom-property tokens in
ui/src/index.css, via a new committed codemod (scripts/codemod-extract-type.mjs).
- 932 sites rewritten across 143 files:
- 728 arbitrary font-size Tailwind classes (text-[Npx])
- 202 arbitrary letter-spacing classes (tracking-[N em])
- 2 inline-style fontSize string literals
- 0 leading-[...] sites found (none existed)
- 17 new tokens minted, no normalizing: 8 --fs-* (9/10/11/12/13/14/15px +
0.7rem, kept in its own unit) and 9 --ls-* (0.08-0.24em, 9 distinct
values) — the micro type-size and letter-spacing clusters TOKEN-AUDIT.md
flagged are now materialized as tokens, still un-collapsed pending a
human scale decision.
- 2 sites allowlisted (both already-documented functional/third-party
fontSize forms: CompanyEnvironments.tsx xterm.js config, CompanySkills.tsx
computed Math.round() value) — no new allowlist entries needed since
neither is a class-string or literal-fontSize site the codemod targets.
- Verified: rg gates clean, pnpm build-storybook exit 0, Storybook visual
suite 510/510 passed (first attempt), pnpm typecheck exit 0, codemod
re-run confirmed idempotent (0 sites on second pass).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mechanical, codemod-driven extraction of hardcoded hex/rgb/rgba color
literals in ui/src/components/** and ui/src/pages/** (including their
*.test.tsx companions) into CSS custom-property tokens in
ui/src/index.css, per DESIGN.md's Phase 2 extraction contract. Zero
visual change: verified against the Phase 0 Storybook snapshot
baseline.
- 69 sites rewritten across 31 files (Tailwind bracket hex classes,
inline style={{ backgroundColor/color: ... }} literals, and
bg-[gradient(...)] arbitrary values).
- 41 new tokens minted verbatim in a new non-@theme :root block
(17 --hex-*, 24 --gradient-extract-*), 2 existing-token reuses
(--status-task-in_progress, --status-task-done — exact
case-insensitive match, mode-independent, no .dark override).
- 8 files allowlisted with inline `token-extraction: allowlisted`
comments for functional/third-party literals that must stay
hardcoded (xterm.js theme config, <input type="color"> value,
color-picker seed state persisted to a create payload, a
persisted/compared skill.color palette, a contrast-math fallback,
a runtime-computed canvas fillStyle, and a half-migrated
var(--x, fallback) pattern left for a human decision).
- Codemod script (scripts/codemod-extract-colors.mjs) uses a fixed,
hand-audited site table rather than a blind hex regex, since a
naive regex false-positives on strings like "acme/web#241".
- Bug caught by the visual suite and fixed before commit: gradient
token values initially kept Tailwind's bracket-arbitrary-value
underscore-for-space escaping (e.g. circle_at_top), which is
invalid inside a real CSS custom property; converted back to
literal spaces.
Verify: rg gate clean outside the allowlist; pnpm build-storybook
exit 0; Storybook visual snapshot suite 510/510 passed; pnpm
typecheck exit 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Baseline for the zero-visual-change token extraction: every Storybook
story (255) in dark and light themes, captured BEFORE any component
change. All 510 pass against this baseline (verified same-build).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Playwright-based screenshot suite over the built Storybook: one test per
story per theme (dark/light), byte-identical comparison (maxDiffPixels 0),
Date frozen via init-script shim (page.clock broke React rendering in
several stories), animations disabled, per-story settle map for delayed
state flips and a mask for one bimodal ::highlight race.
Run with pnpm test:storybook-visual (or :update to re-baseline).
No new dependencies: reuses the repo's @playwright/test and a
dependency-free static server script.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add minimal static stories for the 10 ui/ primitives no existing story
rendered (alert-dialog, avatar, breadcrumb, collapsible, command,
dropdown-menu, radio-card, scroll-area, sheet, skeleton) so the visual
snapshot baseline covers every shared primitive.
- user-secrets: drop the story-level nested MemoryRouter (preview already
provides one; nesting intermittently threw 'Router inside another Router').
- navigation-layout: provide PluginLauncherProvider at the meta level;
BoardChromeMatrix mounts the real Sidebar whose PluginLauncherOutlet
raced company selection and intermittently threw.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/goal rejected the v2 block at 4,648 chars. Block now carries mission +
DONE-WHEN + guardrails; full phase spec moved to a section the run
reads from disk.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - The board UI is the main daily navigation surface for agents,
projects, and their related resources.
> - Operators need a lightweight way to keep frequently used agents and
projects close without changing company-wide ordering or ownership.
> - Resource memberships already model per-user relationships to
projects and agents, so they are the right place to store user-specific
starred state.
> - This pull request extends that membership contract with a starred
timestamp and exposes star controls in list/detail views.
> - The sidebar then uses those starred memberships to show compact,
user-specific shortcuts.
> - The benefit is faster navigation without introducing a separate
favorites system or leaking preferences across users.
## Linked Issues or Issue Description
No public GitHub issue exists.
Feature request:
## Problem or motivation
Users cannot pin frequently used agents or projects into the main
sidebar. Returning to important resources requires scanning full
project/agent lists or navigating through detail pages, which adds
friction to repeated daily workflows.
## Proposed solution
Store a per-user `starred_at` timestamp on agent and project
memberships, expose API actions to set or clear that state, add star
toggle controls to list/detail pages, and render starred projects and
agents as compact sidebar shortcuts.
## Alternatives considered
A separate favorites table would work, but it would duplicate membership
scoping and require another resource relationship model. Keeping starred
state on memberships preserves existing company/user boundaries and
avoids a second source of truth.
## Roadmap alignment
Checked `ROADMAP.md`; no overlapping planned core work for starred
resource/sidebar navigation was found.
## Additional context
The affected subsystems are `packages/db`, `packages/shared`, `server/`,
and `ui/`. The migration is idempotent with `IF NOT EXISTS` guards so
environments that saw an earlier local migration name can still apply
the final ordered migration safely.
## What Changed
- Added idempotent migration `0133_resource_membership_stars` for
`starred_at` columns and lookup indexes on agent/project memberships.
- Extended shared resource membership types and validators with starred
metadata and actions.
- Updated server resource membership services/routes to read and mutate
starred resource state.
- Added reusable star toggle UI and resource membership hook support for
starred state.
- Added starred projects and agents sidebar rendering, plus star
controls on list and detail pages.
- Added focused shared, server, and UI coverage for starred membership
behavior and sidebar rendering.
## Verification
- Rebased and force-with-lease pushed current PR head
`a086fc965391c9e50a51b5b83b5b44a797b2a6f4` onto current
`paperclipai/paperclip:master`; `gh pr view` reports `MERGEABLE` with no
merge conflicts. GitHub checks are green for this fresh head.
- `pnpm exec vitest run packages/shared/src/resource-memberships.test.ts
server/src/__tests__/resource-memberships-routes.test.ts
server/src/__tests__/workspace-runtime.test.ts
ui/src/components/Sidebar.test.tsx
ui/src/components/SidebarAgents.test.tsx
ui/src/components/SidebarStarredProjects.test.tsx
ui/src/components/StarToggle.test.tsx
ui/src/pages/InstanceExperimentalSettings.test.tsx` passed after the
rebase: 8 files, 143 tests.
- Greptile re-review is 5/5; the remaining screenshot thread was
resolved as non-blocking because this task explicitly requested no
screenshots/images in the PR.
- `pnpm exec vitest run
ui/src/components/SidebarStarredProjects.test.tsx` passed after the
mobile pending-spinner fix.
- `pnpm exec vitest run packages/shared/src/resource-memberships.test.ts
server/src/__tests__/resource-memberships-routes.test.ts
ui/src/components/Sidebar.test.tsx
ui/src/components/SidebarAgents.test.tsx
ui/src/components/SidebarStarredProjects.test.tsx
ui/src/components/StarToggle.test.tsx
ui/src/pages/InstanceExperimentalSettings.test.tsx` passed: 7 files, 68
tests.
- `pnpm --filter @paperclipai/db typecheck && pnpm --filter
@paperclipai/shared typecheck && pnpm --filter @paperclipai/server
typecheck && pnpm --filter @paperclipai/ui typecheck` passed
db/shared/server, then failed in pre-existing UI code outside this PR:
`src/pages/CompanyEnvironments.tsx` missing `@xterm/*` type declarations
and `previous` possibly null.
- Checked that the PR diff does not include `pnpm-lock.yaml` or
`.github/workflows` changes.
- Checked `ROADMAP.md` and found no overlapping planned core work for
starred resource/sidebar navigation.
- Searched existing GitHub PRs for duplicate starred-resource/sidebar
work and found none.
## Risks
- Migration touches membership tables. The SQL uses `IF NOT EXISTS` for
columns and indexes so environments that saw an earlier local migration
name can still apply this safely.
- Sidebar ordering and visibility changes could affect users who rely on
the previous flat sidebar layout.
- Starred state is per-user membership metadata; code paths must
continue preserving company/user scoping around memberships.
> 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, tool-enabled coding agent with shell/GitHub access.
Context window not disclosed by the runtime.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Hermes adapter produces terminal output with ANSI color codes on
stdout
> - These escape sequences flow through the UI parsers untouched and
render as raw garbage text
> - This PR adds ANSI stripping at the entry point of all four Hermes
parse-stdout entry points
> - The same regex is already proven in claude-local adapter
> - The benefit is clean, readable terminal output for Hermes agents
## Linked Issues or Issue Description
No existing issue. This is a bug report:
**What happened**
Hermes terminal output displayed ANSI color codes as raw text in the
Paperclip UI, making agent output unreadable.
**Expected behavior**
Terminal output in run transcripts should be clean text without
invisible control characters.
**Steps to reproduce**
1. Connect a Hermes agent to Paperclip
2. Create and assign a task to the agent
3. View the run transcript — ANSI escape codes appear as raw garbage
**Paperclip version or commit**
e6407b322 (upstream master)
**Deployment mode**
local_trusted
## What Changed
- Added `stripAnsi()` function using the same regex pattern from
claude-local adapter (quota.ts) — strips CSI and
OSC sequences
- Applied at entry point of `parseHermesStdoutLine` in hermes_local (TS
+ CJS)
- Applied at entry point of `parseHermesGatewayStdoutLine` in
hermes_gateway (TS + CJS)
- CJS files keep the function inline since the dynamic parser sandbox
has no module loader
- 5 files changed, +123/-8 lines
## Verification
- Smoke tested with real ANSI patterns from Hermes output — all samples
pass
- `pnpm --filter @paperclipai/hermes-paperclip-adapter exec vitest run
src/ui/parse-stdout.test.ts` — 9 passed
- TypeScript compiles clean for both hermes and hermes-gateway packages
- Adapter tests pass (5/6, 1 pre-existing Windows CI failure unrelated)
- Live tested on running Paperclip instance — ANSI codes no longer
appear in transcripts
## Risks
Low risk. Only affects Hermes parser output. Regex already proven in
claude-local adapter. No logic changes to parse
behavior — only strips invisible control characters before parsing.
## Model Used
DeepSeek V4 Pro — reasoning mode, 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] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id
or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - The database layer runs migrations during server startup via
`server.listen`; migrations that block this path delay instance
availability
> - Migration `0126_issue_comment_derived_attribution.sql` backfills
derived attribution columns on `issue_comments` using a LIMIT-5000 loop
with no index or keyset cursor — it re-scans the full table from the
start each batch, giving O(n²) complexity
> - On instances with millions of issue comments this blocked
`server.listen` for ~5 minutes during upgrade, causing CPU pegs and
unavailability
> - Editing 0126 in place is unsafe: the migration runner keys its
applied-set on file **content hash**, so any edit changes the hash,
causing the runner to re-apply the migration on already-migrated
databases and blocking startup
> - The safe remedy is delete-and-relocate: remove 0126 and add a new
forward migration 0132 that uses a temporary partial index + keyset
pagination (`id > last_comment_id ORDER BY id LIMIT 5000`) so every row
is visited exactly once — O(n)
> - This pull request implements that delete-and-relocate with
idempotency guards (IF NOT EXISTS DDL, backfill WHERE clause that skips
already-attributed rows) so it is a safe near-noop on already-migrated,
partially-migrated, and fresh databases alike
## Linked Issues or Issue Description
No pre-existing public GitHub issue. Inline bug report:
**What happened?**
The `0126_issue_comment_derived_attribution` migration runs during
server startup and uses a LIMIT-5000 batch loop that re-scans
`issue_comments` from row 1 each iteration (no index, no keyset cursor).
The result is O(n²) I/O that blocked `server.listen` on large instances.
**Expected behavior**
Backfill migrations should advance with a keyset cursor so each batch
reads a new slice; total work is O(n) and startup is not blocked.
**Steps to reproduce**
Run a Paperclip upgrade on an instance with ≥200k issue comments;
observe `server.listen` blocked for several minutes and CPU peg during
migration.
**Paperclip version or commit**
Reproduced on the current `master` branch prior to this fix.
**Deployment mode**
All deployment modes that run the migration runner at startup.
## What Changed
- **Deleted**
`packages/db/src/migrations/0126_issue_comment_derived_attribution.sql`
— the O(n²) LIMIT-5000 loop with no index/cursor
- **Added**
`packages/db/src/migrations/0132_issue_comment_derived_attribution_fast.sql`:
- Creates a temporary partial index over the eligible predicate before
backfilling
- Uses keyset pagination (`id > last_comment_id ORDER BY id LIMIT 5000`)
— each batch advances to the batch-max id, so every row is visited once
- Drops the temporary index at the end
- Columns/FKs guarded with `IF NOT EXISTS`; Option-A timing-tier cleanup
preserved; human-authored comments never touched
- `WHERE` clause in the backfill excludes rows already attributed (safe
near-noop on already-migrated DBs)
- **Updated** `packages/db/src/migrations/meta/_journal.json` — dropped
0126 entry, appended 0132
- **Added**
`packages/db/src/issue-comment-derived-attribution-migration.test.ts`
(345 lines) — covers fresh-install, already-0126-migrated idempotency,
and partial-backfill completion scenarios using embedded Postgres
## Verification
```bash
# Migration numbering guard
pnpm --filter @paperclipai/db check:migrations
# Migration tests (embedded Postgres, 3 scenarios)
pnpm --filter @paperclipai/db vitest run issue-comment-derived-attribution-migration.test.ts
```
Both pass locally. CI results will appear on this PR.
## Risks
**Migration safety — already-migrated databases:** Deleting 0126 leaves
an orphan row in the runner's applied-set. The runner only checks the
set for "has this been applied" — orphan rows are never re-applied. 0132
runs as a near-noop: IF NOT EXISTS DDL is skipped, and the backfill
WHERE clause excludes rows that already have attribution.
**Migration safety — partially-migrated databases:** Keyset pagination
is idempotent. 0132 picks up from the highest attributed row id, so a
partial prior run is completed correctly.
**No data loss:** The migration never deletes or overwrites
user-authored content. It only writes to derived attribution columns on
rows where attribution is absent.
**Rollback:** 0132 is a forward-only migration. If a rollback is needed,
the attribution columns remain (no harm) and can be ignored or cleaned
up in a subsequent migration.
## Model Used
Claude Sonnet 4.6 (`claude-sonnet-4-6`) via the Paperclip AI agent
harness, with tool use and extended context enabled. Implementation
authored by Priya Raman; PR opened via the Paperclip Git Expert agent.
## 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
_(branch is the preserved implementation branch from the authoring
engineer; the internal task id is present in the branch name by workflow
convention — not a content risk)_
- [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 _(pending — CI running)_
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
_(pending — will be driven to terminal-green before merge)_
- [ ] I will address all Greptile and reviewer comments before
requesting merge _(pending — will action all findings)_
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip stores heartbeat run ownership context so operators can
audit which user was responsible for agent work
> - A migration backfills missing heartbeat run `responsible_user_id`
values from issue references in each run's context snapshot
> - Some context snapshots can store issue identifiers as public ticket
strings rather than UUIDs
> - The migration needs to resolve both UUID issue ids and issue
identifiers without trying to cast identifier strings to UUID
> - This pull request tightens the migration query so UUID matching only
casts validated UUID-shaped values and identifier matching remains a
separate fallback
> - A companion repair migration is needed so installations that already
recorded `0130` still get the corrected heartbeat-run backfill
> - The benefit is that existing installations can apply the
responsible-user invariant migrations without failing on non-UUID issue
references and without leaving already-migrated databases unrepaired
## Linked Issues or Issue Description
No public GitHub issue was found in a quick search for this migration
failure.
Bug report context following `.github/ISSUE_TEMPLATE/bug_report.yml`:
**Pre-submission checklist**
- I searched existing open and closed issues and did not find a
duplicate.
- I can reproduce against current `master` plus the responsible-user
invariant migration path.
- The error originates in Paperclip's database migration, not in an
adapter, API provider, or local configuration.
**What happened?**
Applying the heartbeat run responsible-user backfill migration could
fail when `heartbeat_runs.context_snapshot->>'issueId'` or `taskId`
contained an issue identifier such as `PAP-123` instead of a UUID. The
migration attempted to use issue refs for UUID matching and identifier
fallback, but the UUID path needed to avoid casting non-UUID identifier
strings. Because `0130` may already have been applied in some
installations, a follow-up repair migration is needed as well.
**Expected behavior**
The migration should backfill from UUID issue ids when present, from
issue identifiers when present, and fall back to the company default
responsible user without unsafe UUID casts. Already-migrated
installations should receive the repaired heartbeat-run context-ref
backfill through a new migration.
**Steps to reproduce**
1. Use a migrated database with a company, issue, agent, and heartbeat
run.
2. Store a null `heartbeat_runs.responsible_user_id` and a
`context_snapshot` like `{"issueId":"PAP-123"}`.
3. Replay/apply the run responsible-user repair migration.
4. Observe that the migration must not cast `PAP-123` to UUID and should
backfill from the matching issue identifier.
**Paperclip version or commit**
Current `master` plus this migration fix branch.
**Deployment mode**
Database migration during server startup or explicit migration command.
**Installation method**
Built from source / self-hosted migration path.
**Agent adapter(s) involved**
Not adapter-specific; core database migration bug.
**Database mode**
Postgres migration path, including embedded Postgres in development.
**Access context**
Not applicable; migration-time data backfill.
**Relevant logs or output**
Unsafe UUID casts can surface as Postgres invalid input syntax errors
when a context snapshot issue ref is an identifier rather than a UUID.
**Privacy checklist**
No private logs, paths, API keys, tokens, company names, or internal
Paperclip issue links are included.
## What Changed
- Split heartbeat run context issue reference extraction into reusable
CTEs.
- Only cast `issueId` / `taskId` values to UUID after a UUID-shape regex
check.
- Preserve fallback matching by issue identifier within the same
company.
- Keep deterministic candidate priority with `issueId` before `taskId`
and UUID matches before identifier matches.
- Added `0131_repair_run_responsible_user_context_refs.sql` so
installations that already applied `0130` still receive the corrected
heartbeat-run backfill.
- Added a DB migration regression test that replays the repair migration
with an identifier-style heartbeat run issue ref.
## Verification
- `pnpm --filter @paperclipai/db typecheck`
- `pnpm --filter @paperclipai/db exec vitest run src/client.test.ts`
- Isolated embedded Postgres migration run with a temporary
`PAPERCLIP_CONFIG`: `pnpm --filter @paperclipai/db migrate` applied all
pending migrations successfully.
- GitHub Actions PR workflow is green on commit
`1c2655bc457cef3716a43e66463e1e5bc2fdcfab`.
- Greptile is 5/5 with no unresolved threads on commit
`1c2655bc457cef3716a43e66463e1e5bc2fdcfab`.
- Searched for duplicate public issues/PRs with GitHub search; no direct
duplicate found.
- Checked `ROADMAP.md` for overlap; no related roadmap item found.
## Risks
- Low-to-medium migration risk because this modifies an existing data
backfill migration and adds a companion repair migration.
- The query still relies on `context_snapshot` containing either issue
UUIDs or identifiers that match issues in the same company.
- Installations with unusual malformed context snapshots now skip unsafe
UUID casts and fall through to identifier/default backfill behavior.
> 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 with tool use and local command
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 not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes, or
confirmed no docs update is needed for this migration-only fix
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - Pipeline health reports give operators early warnings when a
workflow step cannot run cleanly.
> - Failed stage automation is surfaced as an `automation_failed` health
warning for the affected item.
> - A single item can have repeated failed automation rows for the same
stage, especially after retries or repeated failed attempts.
> - Rendering every matching row creates duplicate warnings that make
the pipeline look noisier than it is.
> - This pull request deduplicates failed automation warnings by the
item/stage pair before adding them to the health report.
> - The benefit is that repeated failures for the same item in the same
stage produce one actionable warning, while distinct items still remain
visible.
## Linked Issues or Issue Description
Refs #8866
Bug: pipeline health could emit duplicate `automation_failed` warnings
when the input contained repeated failed automation rows for the same
live item and stage. Reviewers should expect one warning per
`stageId:caseId` pair, not one warning per backing execution row.
## What Changed
- Deduplicated failed automation warnings with per-stage case tracking
in `computePipelineHealth`, avoiding collision-prone composite string
keys before pushing `automation_failed` warnings.
- Added shared Vitest coverage for a single automation failure,
duplicate same-stage same-item dedupe, separate warnings for different
item IDs in the same stage, the same item ID in different stages, and
colon-delimited ID collision cases.
- Kept pipeline route behavior unchanged; this PR only changes shared
warning rendering and direct shared tests.
## Verification
- `pnpm vitest packages/shared/src/pipeline-health.test.ts`
- 1 test file passed
- 5 tests passed
- PR #9090 remote checks on `bbbb2d4627f5be17ca210dedb9edb91edd047df8`
- All Paperclip CI/status checks passed
- Greptile Confidence Score: 5/5, 0 comments added, 0 unresolved
Greptile threads
No route test changed because this PR does not change the route's
failed-automation query or normalization behavior.
## Risks
Low risk. The change only suppresses duplicate `automation_failed`
warnings when both `stageId` and `caseId` match. Distinct items in the
same stage still produce separate warnings.
> 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 in the Paperclip local adapter
environment; exact model snapshot and context-window metadata were not
exposed in the runtime. Tool use and code execution were enabled.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - The server reports its own version through `server/src/version.ts`,
which is read by the `/health` endpoint and the telemetry client
> - When running from a cloned source tree, `server/package.json` is
frozen at the last published release version (e.g. `0.3.1`), so the
reported `serverVersion` never reflects how far the local checkout has
drifted from that release
> - Operators and support staff cannot tell from telemetry or health
output whether they are running a tagged release or a development build
with local commits on top
> - A `git describe --tags --match v* --long --dirty` call at startup
gives the exact nearest tag, number of commits since it, the current
SHA, and whether the tree is dirty — all the information needed to
compute a semantically meaningful version
> - This pull request replaces the static `pkg.version` export with a
`resolveServerVersion()` call that parses `git describe` output into
`YYYY.MDD.P+N.git.<sha>` (drift), `YYYY.MDD.P` (clean on-tag), or
appends `.dirty` for a modified tree, with a non-throwing fallback to
`package.json` when git is unavailable
> - The benefit is that from-source installs now report a version string
that lets operators and support quickly identify their exact checkout
state without running additional git commands
## Linked Issues or Issue Description
No pre-existing public issue. Inline description:
**What happened?**
When Paperclip is installed from source (git clone + pnpm), `GET
/health` and the telemetry envelope report the version frozen at the
last published `package.json` value (e.g. `0.3.1`) regardless of how
many commits ahead of that tag the local checkout is.
**Expected behavior**
The reported version should reflect the actual local state — nearest
release tag, number of commits since that tag, abbreviated commit SHA,
and a dirty marker when the working tree has uncommitted changes.
**Steps to reproduce**
Clone the repo, run `pnpm install && pnpm --filter @paperclipai/server
start`, then call `GET /health` or inspect telemetry envelopes. The
`serverVersion` field shows the `package.json` version even when the
checkout is dozens of commits ahead of that tag.
**Paperclip version or commit**
Affects all source-tree installs where `package.json` has not been
updated to match the current HEAD.
**Deployment mode**
Source install (git clone).
## What Changed
- `server/src/version.ts`: extracted `resolveServerVersion()` (replaces
the module-level `const serverVersion`) and `parseGitDescribeVersion()`
(exported for unit testing); the default implementation shells out to
`git describe --tags --match v* --long --dirty` with a 1 500 ms timeout;
falls back to `pkg.version ?? "0.0.0"` without throwing when git is
unavailable or the output cannot be parsed; replaced `logger` import
with a `console.debug`-based default to avoid pulling pino transport
side effects into a zero-dependency utility module
- `server/src/__tests__/version.test.ts`: 7-test unit suite covering
drift, clean on-tag collapse, dirty on-tag edge case, unparseable
fallback, `resolveServerVersion` happy path, and git-unavailable
fallback — all exercised via injected stubs without spawning a real git
process
## Verification
```sh
# Unit tests (7 tests)
pnpm exec vitest run server/src/__tests__/version.test.ts
# Type check
pnpm --filter @paperclipai/server typecheck
# Health and telemetry regression
pnpm exec vitest run server/src/__tests__/health.test.ts server/src/__tests__/telemetry-client-flush.test.ts
# Runtime smoke (from-source checkout)
# git describe --tags --match 'v*' --long => v2026.626.0-58-g518fc71ce
# server startup => serverVersion = 2026.626.0+59.git.3367571cc
```
All commands passed at the committed HEAD.
## Risks
Low. The change is additive and self-contained to
`server/src/version.ts`:
- `git describe` is called once at module load with a 1 500 ms timeout;
failure (non-git environment, git not on PATH, timeout) is silently
caught and falls back to `pkg.version`, preserving existing behavior for
published-package installs
- No API surface, database schema, or migration is touched
- The telemetry envelope already carried `serverVersion`; only the value
changes for source-tree installs
## Model Used
Claude Sonnet 4.6 (`claude-sonnet-4-6`) with tool use and code
execution. Context window: 200 k tokens.
## 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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [ ] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - The browser UI depends on the Inter font family for its intended
visual baseline
> - The app previously referenced remote Google Fonts stylesheets at
runtime
> - That meant self-hosted, offline, or privacy-sensitive deployments
could lose the intended typography or depend on an external request
> - This pull request bundles the required Inter variable font files
with the UI and serves them from the app
> - A normal UI test now covers the static assets and CSS wiring without
adding a package script or build step
> - The benefit is a more reliable, self-contained UI that does not rely
on third-party webfont hosting
## Linked Issues or Issue Description
No public GitHub issue was found for this exact gap.
**Subsystem affected**
ui/ — React + Vite board UI
**Problem or motivation**
Paperclip's UI should ship the webfont assets it references so
production and self-hosted deployments render consistently without
reaching out to Google Fonts at runtime.
**Proposed solution**
Bundle the Inter variable font files under the UI public assets, load
them with local `@font-face` declarations, document the bundled assets,
and cover the source assets/CSS wiring with a normal UI Vitest test.
**Alternatives considered**
Keeping the remote stylesheet dependency is simpler, but leaves
deployments dependent on external font hosting. Using system fonts only
would avoid the asset footprint, but changes the intended UI typography.
**Roadmap alignment**
This is a focused UI reliability/polish fix, not a roadmap-level core
feature.
**Additional context**
Searched public GitHub issues and PRs for `webfonts
repo:paperclipai/paperclip`; no duplicates or closely related open items
were found.
## What Changed
- Added bundled Inter variable font assets and their notice under
`ui/public/fonts/`.
- Replaced remote Google Fonts imports with local `@font-face`
declarations using relative public-asset URLs that remain subpath-safe
from built CSS.
- Documented the local font asset expectation in development and UI spec
docs.
- Removed the follow-up font asset checker scripts and package/build
wiring after review feedback clarified they are not required for
building the UI.
- Added `ui/src/lib/ui-font-assets.test.ts` to verify the shipped WOFF2
files, notice text, and CSS font references through the normal UI test
suite.
## Verification
- `pnpm --filter @paperclipai/ui exec vitest run
src/lib/ui-font-assets.test.ts --config vitest.config.ts`
- Passed.
- Confirms the bundled font files exist, are WOFF2 files, have notice
coverage, and are referenced by `ui/src/index.css`.
- `pnpm --filter @paperclipai/ui build`
- Passed.
- Confirmed the UI still builds after removing the checker from
`ui/package.json`.
- Confirmed `ui/dist/fonts/` contains `InterVariable.woff2`,
`InterVariable-Italic.woff2`, and `NOTICE.md` after the build.
- The UI build emitted existing warnings about `::highlight(...)`, a
dynamic/static import overlap for `MarkdownEditor.tsx`, unresolved
relative public font URLs left for runtime resolution, and large chunks,
but completed successfully.
## Risks
Low risk. This adds static font assets and swaps the font source from a
remote stylesheet to same-origin files. The main tradeoff is a larger
repository/UI asset footprint from the bundled `.woff2` files.
> 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, tool-enabled terminal/GitHub
workflow with reasoning support.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - The board UI is where operators inspect company activity, agent
work, and issue progress.
> - Existing board views show individual issue and run details, but they
do not give operators a compact time-based picture of work across
agents.
> - A work timeline helps operators scan when agents worked, how
handoffs happened, and where overlapping work occurred.
> - This pull request adds a company-scoped Work Timeline page backed by
the existing API surface and renders the timeline as a custom SVG
Gantt-style view.
> - The benefit is faster operator understanding of multi-agent
execution without opening each issue thread individually.
## Linked Issues or Issue Description
No public GitHub issue exists for this change.
Subsystem affected:
- ui/ — React + Vite board UI
Problem or motivation:
- Operators can inspect individual issues and runs, but there is no
compact time-based view of company work across agents.
- This makes it harder to scan overlaps, handoffs, retries, and activity
windows without opening many issue threads.
Proposed solution:
- Add a company-scoped Work Timeline page in the board UI.
- Render agent and system run spans as a custom SVG Gantt-style chart
with packed overlap lanes.
- Show issue color identity, kickoff attribution, hover-revealed
delegation connectors, retry styling, zoom controls, a sticky actor
gutter, and a minimap brush.
- Keep human activity lightweight by showing human kickoff chips without
plotting standalone human event rows.
Alternatives considered:
- Add the same information to existing issue-list or run-list views.
That would preserve simpler UI, but it would not show temporal overlap
or handoff paths clearly.
- Build this as a plugin-only surface. That keeps core smaller, but the
board already has the company-scoped route, navigation, and API client
patterns needed for this operator workflow.
Roadmap alignment:
- `ROADMAP.md` does not list an existing duplicate work-timeline
milestone. This supports the broader operator visibility direction
around artifacts, enforced outcomes, and higher-autonomy execution.
Additional context:
- Storybook includes `Pages/Work Timeline` stories for hour/day zoom and
a human-activity sample so reviewers can inspect the component without a
live backend.
## What Changed
- Added the Work Timeline page, route, sidebar entry, API client, query
key, and company-prefixed route helper coverage.
- Added a pure timeline layout transform for row packing, issue colors,
kickoff attribution, connector calculation, tick selection, and duration
formatting.
- Added the custom SVG timeline chart with sticky actor gutter,
hover-revealed connectors, zoom controls, minimap brushing,
visible-range feedback, and issue navigation.
- Added Storybook coverage plus sample fixtures for the work timeline.
- Added and corrected focused UI tests covering layout, chart behavior,
routing, sidebar behavior, and collapsed-sidebar expectations.
- Addressed Greptile feedback for kickoff fallback ordering, minimap
range math, document drag listener cleanup, and stable default `now`
handling.
## Verification
- `pnpm --filter @paperclipai/ui exec vitest run
src/lib/timeline/layout.test.ts
src/components/timeline/WorkTimelineChart.test.tsx
src/pages/Timeline.test.tsx src/lib/company-routes.test.ts
src/components/Sidebar.test.tsx
src/components/RequestCollapsedSidebar.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- `git merge-tree $(git merge-base HEAD origin/master) HEAD
origin/master | rg -n "<<<<<<<|changed in both|CONFLICT"` returned no
conflicts.
- Attempted Storybook screenshot capture with Playwright; Storybook ran
locally, but Chromium could not launch in this container because native
browser libraries such as `libatk-1.0.so.0` are unavailable and `npx
playwright install-deps chromium` requires interactive sudo.
## Risks
- Medium UI risk: this adds a substantial visual surface with custom SVG
interaction logic, so browser-level review is still useful for
responsive behavior and usability.
- Low backend risk: this PR only adds a UI client/page around the
existing timeline API contract and does not change database schema or
server routes.
> 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 coding agent, with repository file access, shell
command execution, GitHub connector access, and focused test execution.
Exact context-window details 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)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - When you swap an agent's adapter (e.g. from one LLM provider to
another), the server merges the incoming PATCH body with stored config —
keys listed in \`ADAPTER_AGNOSTIC_KEYS\` are preserved regardless of
which adapter is active
> - That constant was defined independently in two places:
\`server/src/agents.ts\` (used by the adapter-swap route) and
\`ui/src/lib/agent-config-patch.ts\` (used by the UI patch builder)
> - PR #8975 fixed the bug where \`paperclipSkillSync.desiredSkills\`
was dropped on adapter swap by adding it to the server-side constant,
but the UI-side copy was not updated in the same PR — creating ongoing
drift risk
> - This pull request hoists \`ADAPTER_AGNOSTIC_KEYS\` into
\`packages/shared\` so both consumers import the same constant
> - The benefit is a single source of truth: any future key addition is
made in one place and both the server route and the UI patch builder
pick it up automatically, with a drift guard to catch any accidental
re-duplication
## Linked Issues or Issue Description
Refs #8975 — follow-up deduplication: #8975 fixed the runtime bug but
left the constant duplicated across server and UI. This PR closes that
gap.
## What Changed
- Added \`ADAPTER_AGNOSTIC_KEYS\` constant and \`AdapterAgnosticKey\`
type to \`packages/shared/src/adapter-agnostic-keys.ts\`
- Updated \`server/src/agents.ts\` to import the shared constant,
removing the local copy
- Updated \`ui/src/lib/agent-config-patch.ts\` to import the shared
constant, removing the local copy
- Added \`packages/shared/src/adapter-agnostic-keys.test.ts\`: drift
guard asserting the expected key set and both consumer import sites
## Verification
\`\`\`bash
pnpm exec vitest run packages/shared/src/adapter-agnostic-keys.test.ts
ui/src/lib/agent-config-patch.test.ts
server/src/__tests__/agent-instructions-routes.test.ts
pnpm --filter @paperclipai/shared typecheck
pnpm --filter @paperclipai/server typecheck
pnpm --filter @paperclipai/ui typecheck
\`\`\`
All 15 tests pass across the three files; all three packages typecheck
clean.
## Risks
Low risk — behavior-preserving refactor. The key set is unchanged; only
the import source changes. The drift guard will fail loudly if someone
accidentally re-introduces a local copy or modifies one without updating
the other.
> 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
- Provider: Anthropic
- Model: Claude Sonnet 4.6 (\`claude-sonnet-4-6\`)
- Context: standard context window, tool use enabled
- Reasoning: standard mode (no 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
- [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
- [ ] 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>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent runs often need provider credentials, API tokens, and other
environment-bound secrets.
> - Company-level secrets work for shared credentials, but they do not
model values that should differ by human operator.
> - Without a user-scoped model, a run can dispatch without knowing
whether the responsible human has supplied the needed value.
> - Paperclip also needs run attribution to make those user-scoped
runtime checks deterministic and auditable.
> - This pull request adds user-specific secret definitions, per-user
values, environment bindings, responsible-user attribution, and runtime
resolution gates.
> - The benefit is that teams can define the secret once, let each user
provide their own value, and block runs before dispatch when required
user secrets or active definitions are unavailable.
## Linked Issues or Issue Description
Refs #224
Refs #6057
This PR implements user-specific secret support as a core
secret-management capability rather than a one-off adapter setting. It
is related to existing public work on company secrets UI and runtime
secret refs, but is distinct because the value is owned by the
responsible user and resolved at run dispatch time.
Related PR search before opening found existing secrets work such as
#1550, #8256, #8614, #8634, and #8647; none of those add the full
user-secret definition/value/runtime gate covered here.
## What Changed
- Added user-secret definitions and per-user "My secrets" values,
keeping stored values out of access metadata.
- Added `user_secret_ref` environment bindings and UI affordances to
pick them alongside existing secret refs.
- Added responsible-user runtime resolution so user-secret refs resolve
against the human responsible for the run.
- Added pre-dispatch missing-secret gates so runs fail before adapter
dispatch when required user values are absent or definitions are
inactive.
- Added low-trust allowlist hardening for user-secret runtime access.
- Added issue, routine, run, and agent API key responsible-user
attribution and fail-closed dispatch behavior when attribution cannot be
resolved.
- Added denial-copy mapping so responsible-user authorization failures
surface as actionable run outcomes instead of opaque setup failures.
- Added OpenAPI documentation for the user-secret routes.
- Rebases cleanly on current `master`; migrations were renumbered
incrementally as `0128_user_specific_secrets`,
`0129_agent_api_key_responsible_user`, and
`0130_run_responsible_user_invariant` after upstream `0126`/`0127`
migrations.
- Removed previously committed local design screenshots so the PR
contains code/docs/tests only.
## Verification
- PASS: PR head `2527febd106bcf3ca264ca0da7fca491084192d6` is based on
`paperclipai/paperclip:master`.
- PASS: `git diff --check`
- PASS: `git diff --name-only public/master...HEAD | rg
'^(pnpm-lock\\.yaml|\\.github/workflows/|screenshots/)' || true`
produced no files.
- PASS: migration journal audit confirmed unique indexes through `130`
with tail entries `0126_issue_comment_derived_attribution`,
`0127_environment_custom_images_instance_scoped`,
`0128_user_specific_secrets`, `0129_agent_api_key_responsible_user`, and
`0130_run_responsible_user_invariant`.
- PASS: `pnpm --filter @paperclipai/ui typecheck`
- PASS: `pnpm --filter @paperclipai/server typecheck`
- PASS: `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/heartbeat-responsible-user-invariant.test.ts`
- PASS: `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/heartbeat-active-run-output-watchdog.test.ts
src/__tests__/heartbeat-stale-queue-invalidation.test.ts
src/__tests__/heartbeat-workspace-finalize-branch.test.ts
src/__tests__/issue-monitor-scheduler.test.ts`
- PASS: `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/heartbeat-comment-wake-batching.test.ts
src/__tests__/heartbeat-retry-scheduling.test.ts
src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts
src/__tests__/heartbeat-plugin-environment.test.ts`
- PASS: `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/low-trust-red-team-routes.test.ts`
- PASS: `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/secrets-service.test.ts` (55 tests)
- PASS: `pnpm vitest run server/src/__tests__/secrets-routes.test.ts
server/src/__tests__/secrets-service.test.ts` (89 tests after final
Greptile cleanup fixes)
- PASS: `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/heartbeat-issue-liveness-escalation.test.ts` (17 tests
after the final rebase CI fix)
- PASS: focused server Vitest batches covering heartbeat recovery,
project env, plugin env, routines, low-trust, pipelines, monitors,
watchdog, and stale queue paths.
- PASS: GitHub checks are green on
`2527febd106bcf3ca264ca0da7fca491084192d6`, including Typecheck +
Release Registry, Build, General tests, serialized server suites, e2e,
Canary Dry Run, verify, security checks, and Greptile Review.
- PASS: Greptile Review completed successfully on
`2527febd106bcf3ca264ca0da7fca491084192d6` with Confidence Score 5/5,
and GraphQL review-thread audit returned zero unresolved non-outdated
threads.
## Risks
- Runtime behavior now depends on a run having a correct responsible
user; missing or incorrect responsibility assignment can block runs
before adapter dispatch.
- `user_secret_ref` bindings intentionally expose metadata without
values, but UI/API callers may need to handle the new binding kind
explicitly.
- External secret providers and IAM policies are not automatically
provisioned by this PR; operators still need to configure provider-side
access for non-local vaults.
- The PR is broad across db/shared/server/UI/runtime paths, so release
validation should include both API and UI secret workflows before merge.
- The migration renumbering is intentionally incremental after upstream
migrations; the branch migrations use guarded
column/table/index/constraint creation so users who tested the older
draft numbering should not hit duplicate DDL for the existing objects.
> 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 (`gpt-5`), Codex local adapter
with shell/tool use and code execution. Context window and internal
reasoning mode are not exposed by the runtime.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Each agent runs on an adapter (`claude_local`, `codex_local`, …) and
can be assigned company skills that are synced into its runtime
> - An agent's desired-skill selection is persisted inside its single
`adapterConfig` JSON blob under `paperclipSkillSync`, even though the
selection is a company-level, adapter-agnostic choice
> - When a user changes an agent's adapter type, both the server PATCH
handler and the UI patch builder rebuild `adapterConfig` and carry over
only a hardcoded allow-list of adapter-agnostic keys (`env`, `cwd`,
instructions bundle, …)
> - `paperclipSkillSync` was missing from both allow-lists, so switching
adapters (e.g. claude_local → codex_local) silently wiped every assigned
skill
> - This pull request adds `paperclipSkillSync` to the adapter-agnostic
preservation list on both layers and covers it with regression tests
> - The benefit is that switching an agent's adapter no longer destroys
its skill configuration — skills are preserved exactly like
env/cwd/instructions already are
## Linked Issues or Issue Description
Fixes#8974
## What Changed
- **Server (authoritative fix)** — `server/src/routes/agents.ts`: added
`"paperclipSkillSync"` to the `ADAPTER_AGNOSTIC_KEYS` list in the
`changingAdapterType` branch of `PATCH /agents/:id`. On an adapter-type
change the handler now restores the skill-sync selection from the
existing persisted config when the incoming config omits it — the same
mechanism already used for `env`, `cwd`, and the instructions bundle.
This protects every API/CLI client, not just the UI.
- **UI (defense in depth)** — `ui/src/lib/agent-config-patch.ts`: added
`"paperclipSkillSync"` to the client-side `ADAPTER_AGNOSTIC_KEYS` in
`buildAgentUpdatePatch`, so the optimistic patch the client builds on an
adapter switch stops stripping the key before it reaches the server.
- **Tests** — added regression tests on both layers:
- `server/src/__tests__/agent-instructions-routes.test.ts`: `PATCH`ing
`adapterType` (claude_local → codex_local) with `replaceAdapterConfig:
true` keeps `adapterConfig.paperclipSkillSync`.
- `ui/src/lib/agent-config-patch.test.ts`: `buildAgentUpdatePatch`
preserves `paperclipSkillSync` when the overlay changes the adapter
type.
## Verification
```
# server (run from repo root)
cd server && ../node_modules/.bin/vitest run \
src/__tests__/agent-instructions-routes.test.ts \
src/__tests__/agent-skills-routes.test.ts \
src/__tests__/agent-adapter-validation-routes.test.ts \
src/__tests__/agent-permissions-routes.test.ts
# 83 passed
../node_modules/.bin/tsc --noEmit -p tsconfig.json # clean
# ui
cd ui && ./node_modules/.bin/vitest run src/lib/agent-config-patch.test.ts # 7 passed
pnpm --filter @paperclipai/ui typecheck # clean
```
Both new tests fail without the corresponding source change (verified
red → green).
Manual: create an agent on `claude_local`, assign skills, switch it to
`codex_local`, and confirm `GET /api/agents/:id/skills` still returns
the desired skills.
## Risks
Low risk.
- The change only *adds* one key to an existing preservation allow-list;
it does not alter how any other key is handled. Behavior for agents
without a `paperclipSkillSync` block is unchanged (the key is simply
absent and nothing is copied).
- `paperclipSkillSync` is adapter-agnostic (company skill keys, not
adapter-specific), so carrying it across an adapter switch is always
safe — a target adapter that does not support skill sync just ignores
it, and switching back restores the selection.
- Same-adapter config edits already merged and preserved the key; this
only closes the adapter-type-change gap, matching the existing
env/cwd/instructions behavior.
- Follow-up (not in this PR to keep it minimal): the server and client
`ADAPTER_AGNOSTIC_KEYS` lists are maintained separately and already
diverge (`instructionsFilePath` is client-only); a shared constant could
prevent future drift.
## Model Used
Claude Opus 4.8 (`claude-opus-4-8`, 1M-token context), extended thinking
enabled, with tool use (file edit, shell, GitHub CLI) via Claude Code. A
read-only sub-agent was used to trace the root cause across the server
and UI layers.
## 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 (bug fix, not a feature)
- [x] I have searched GitHub for duplicate or related PRs and linked
them above (none found)
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change
(`fix/preserve-skills-on-adapter-type-switch`) 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 (N/A —
internal config-preservation fix, no user-facing docs or API contract
change)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (pending CI on this PR)
- [ ] 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
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent heartbeats provision execution workspaces before invoking
local or sandboxed adapters.
> - Some follow-up issues intentionally request `reuse_existing` so they
continue in an inherited execution workspace.
> - The heartbeat provisioning path treated missing or archived
workspace rows as if no explicit reuse request existed.
> - That could silently realize and persist a fresh project/default
workspace over an explicit inherited-workspace binding.
> - This pull request keys explicit reuse off the issue preference and
workspace id, then either restores that workspace or fails with a
structured workspace validation error.
> - The benefit is that intentional workspace inheritance remains
auditable and does not silently degrade into unrelated fallback
workspaces.
## Linked Issues or Issue Description
Refs #8058
Refs #6036
Refs #2203
This fixes a narrower heartbeat provisioning bug around explicit
`reuse_existing` issue runs: if the target inherited execution workspace
is missing, archived, or fails restore, provisioning now reports the
reuse failure instead of replacing the issue's workspace binding with a
freshly realized fallback.
## What Changed
- Added explicit helpers for resolving workspace reuse requests and
deciding whether reuse should restore, refresh metadata, or keep prior
replacement-class drift visible.
- Changed heartbeat workspace provisioning so explicit `reuse_existing`
requests go through restore-or-fail behavior instead of falling back to
`realizeExecutionWorkspace` when the stored workspace row is
unavailable.
- Added structured `workspace_validation_failed` details for inherited
workspace reuse failures.
- Added regression coverage for replacement-class drift, restore errors,
missing rows, archived rows, and restore misses.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm --filter @paperclipai/plugin-sdk ensure-build-deps`
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/heartbeat-workspace-session.test.ts`
- `pnpm --filter @paperclipai/server typecheck`
- `git diff --check origin/master...HEAD`
- Scanned the branch diff and commit messages for credentials, tokens,
private URLs, PII-style values, and internal issue links before pushing;
no unsafe hits remained.
## Risks
- Explicit reuse requests whose stored workspace cannot be restored now
fail the run instead of opportunistically creating a replacement
workspace. That is intentional, but it may surface stale or archived
workspace rows as visible provisioning failures that require repair.
- Non-reuse workspace provisioning still uses the existing realization
path, so the behavior shift is scoped to issues that explicitly request
existing workspace reuse.
> 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 local agent, with shell/tool use enabled for
repository inspection, code editing, verification, git, and GitHub CLI
operations. Runtime context-window details were not exposed by the
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)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - The agent detail page is where operators inspect heartbeat runs and
watch live execution output.
> - Heartbeat runs can publish progress events while longer operations
are happening.
> - The live log viewer already appended streamed log and structured run
events, but it ignored progress events for the same run.
> - That made useful progress text invisible in the run log until
another event type arrived or the operator inspected other surfaces.
> - This pull request renders run progress events as system log lines in
the live agent run viewer.
> - The benefit is clearer live feedback during long-running heartbeat
operations without changing the backend event contract.
## Linked Issues or Issue Description
No public GitHub issue exists, so this PR describes the issue inline
following the bug report template.
### What happened
The agent detail run log subscribed to company live events and handled
`heartbeat.run.log` plus structured `heartbeat.run.event` payloads, but
it ignored `heartbeat.run.progress` events for the active run.
### Expected behavior
When a heartbeat run emits a progress message, the active run log should
show that message immediately as operator-visible system output.
### Steps to reproduce
1. Open an agent detail page for a live heartbeat run.
2. Trigger a run operation that emits `heartbeat.run.progress` events
with a `message` and optional `phase`.
3. Watch the live log viewer.
Before this change, the progress event was ignored by the log viewer.
After this change, it appears as a system log line, prefixed by
`[phase]` when a phase is present.
### Paperclip version / deployment mode
Current `master`; local development and normal board UI deployments.
### Related work search
Searched public GitHub issues and PRs in `paperclipai/paperclip` for
`heartbeat.run.progress AgentDetail` and `run progress log viewer`; no
duplicate issue or PR was found.
## What Changed
- Added live handling for `heartbeat.run.progress` events in
`AgentDetail`'s run `LogViewer`.
- Render progress messages as `system` log lines for the matching run.
- Include the optional progress phase in the displayed line as `[phase]
message`.
- Prefer the event's `updatedAt` timestamp when provided, falling back
to the live event timestamp.
- Added a replay key for progress log lines so WebSocket reconnect
replay does not duplicate the same rendered progress line.
- Added focused formatter/key tests covering phased progress, unphased
progress, empty messages, and replay-key output.
### Visual output example
The rendered log text is covered by the new formatter test:
```text
[workspace] Syncing issue history
Preparing workspace
```
No layout or styling changes are included; this PR only makes existing
log-line UI receive one more live event type.
## Verification
- `pnpm install --frozen-lockfile` — completed; emitted non-fatal
bin-link warnings for the unbuilt plugin SDK dev CLI.
- `pnpm --filter @paperclipai/ui typecheck` — passed.
- `pnpm --filter @paperclipai/ui exec vitest run
src/pages/AgentDetail.progress.test.ts
src/context/LiveUpdatesProvider.test.ts` — passed, 2 files / 26 tests.
- `git diff --check` — passed.
- Local sensitive-content scan over the PR diff using patterns for API
keys, tokens, secrets, passwords, auth headers, private keys,
localhost/private paths, internal ticket ids, agent links, and tailnet
markers — no findings.
## Risks
Low risk. This is a UI-only live-event handling change for an existing
event type. The replay guard is intentionally scoped to progress lines
and uses the rendered timestamp, stream, and chunk as the key, so
repeated progress events with distinct timestamps or messages still
appear.
> 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 tool use, shell
execution, GitHub CLI access, and local test execution. Context window
size was 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)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
Auto-generated lockfile refresh after dependencies changed on master.
This PR only updates pnpm-lock.yaml.
Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - Environment sandboxes already support custom image creation and
refresh through a temporary SSH setup session.
> - The existing workflow makes operators copy an SSH command into an
external terminal before they can install packages or make image
changes.
> - That extra context switch is slower, easier to get wrong, and less
integrated with the setup session Paperclip already tracks.
> - This pull request adds an embedded browser SSH terminal for custom
image setup, so operators can start working in the target sandbox
directly from the environment configuration flow.
> - The implementation uses short-lived websocket attachment tokens,
session-lifetime SSH host-key pinning, and server-managed terminal
cleanup so the feature fits the existing setup-session boundary.
> - The benefit is a smoother custom image creation and refresh
experience without asking users to leave Paperclip for routine sandbox
setup work.
## Linked Issues or Issue Description
No public GitHub issue exists.
### Subsystem affected
Cross-cutting: `server/` custom image setup APIs and websocket handling,
`ui/` environment configuration UI, and shared custom image contracts.
### Problem or motivation
Custom image creation and refresh require an operator to open a separate
SSH client, paste the command shown by Paperclip, perform setup work,
then return to the browser to finish the image flow. This is functional
but awkward for a setup process that already starts and tracks a
temporary sandbox session.
### Proposed solution
Embed an SSH terminal in the custom image setup UI. When a setup session
exposes an SSH payload, Paperclip should open a browser terminal backed
by a server-side websocket session, let the operator run setup commands
in-place, and then close the terminal when setup is finished, cancelled,
expired, or disconnected.
### Alternatives considered
- Keep the existing copy/paste SSH command workflow. This remains a
fallback, but it does not streamline the common path.
- Put SSH credentials directly into websocket URLs. This was avoided so
terminal authentication can happen in an explicit first websocket auth
frame rather than in logged URLs.
- Trust the SSH host blindly for every reconnect. This PR instead pins
the observed host-key fingerprint for the setup-session lifetime.
### Roadmap alignment
This fits the roadmap theme of making agent workspaces usable in more
remote and sandboxed environments while preserving Paperclip's
control-plane model.
### Additional context
Public GitHub search did not find a duplicate issue or PR for `custom
image terminal ssh` in `paperclipai/paperclip`.
## What Changed
- Added server-side terminal session tracking for custom image setup
sessions, including connect-token issuance, websocket attachment,
expiry, resize, input, and shutdown handling.
- Added an embedded browser terminal to the custom image creation and
refresh flow when a setup session provides SSH connection details.
- Moved terminal token authentication out of the websocket URL and into
the first websocket JSON auth frame.
- Added SSH host-key SHA-256 pinning for each terminal session and
documented the provider convention for username-embedded SSH
credentials.
- Updated the custom image environment API and UI so the setup terminal
can open, reconnect, show status, authenticate, resize, and remain
active for the setup-session lifetime once attached.
- Kept custom image setup routes company-scoped and closed active
terminal sessions on setup finish/cancel.
- Added focused unit/integration/UI coverage for token expiry,
setup-session expiry, websocket close paths, host-key pinning, and
terminal session lifecycle behavior.
- Removed the generated lockfile delta from the PR; CI owns temporary
lockfile regeneration for manifest-changing PRs.
## Verification
- `pnpm exec vitest run
server/src/__tests__/server-startup-feedback-export.test.ts
server/src/__tests__/environment-custom-image-terminal-ws.test.ts
server/src/services/environment-custom-image-terminal-sessions.test.ts
server/src/__tests__/environment-custom-image-routes.test.ts
packages/shared/src/environment-custom-images.test.ts
ui/src/pages/CompanyEnvironments.test.tsx`
- 6 test files passed
- 58 tests passed
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm --filter @paperclipai/server build`
- `pnpm --filter @paperclipai/ui build`
- `pnpm run typecheck:build-gaps`
- `git diff --check`
- Local sensitive-content scan over the PR diff using patterns for API
keys, private keys, private hostnames, local paths, token fields, and
credential-like strings.
- Findings were limited to removed URL-token code and synthetic test
placeholders such as `ssh-token-secret` and
`terminal-token-terminal-token-123456`.
- No real credentials, private hostnames, local filesystem paths, or
instance-local links were found.
- Remote PR checks were green after the implementation commit, including
Build, Typecheck + Release Registry, General tests, serialized server
suites, e2e, verify, Socket, Snyk, Superagent, and Greptile 5/5.
- Post-merge PR hardening on July 3, 2026: merged `origin/master` at
`47448721e` into the branch, resolved the `CompanyEnvironments.tsx`
import conflict, reran focused tests, server/UI typechecks, server/UI
builds, `pnpm run typecheck:build-gaps`, and `git diff --check`, scanned
the final diff for sensitive content, pushed `4b43558cc`, and confirmed
all remote checks plus Greptile 5/5 were green.
- PR metadata correction on July 3, 2026: changed the title/body framing
from bug-fix language to feature-request language. No source files
changed for this metadata-only update.
## Risks
- Moderate surface area because this adds websocket routing,
setup-session runtime state, package dependencies, and a new custom
image UI path.
- New websocket attachments still require valid short-lived tokens;
established terminal sessions remain bounded by setup-session expiry,
explicit finish/cancel, client close, or server shutdown.
- The terminal-session store is in-memory, so active terminal websocket
tokens and host-key pins do not survive server restarts.
- SSH host-key verification uses session-lifetime TOFU pinning because
the current provider payload does not expose a trusted host-key
fingerprint.
- The external SSH command remains important as a fallback if a browser,
proxy, or network environment cannot sustain the websocket terminal.
> 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 shell/tool execution. Context
window size was 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)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - The issue details sidebar is a high-traffic operator surface for
scanning task state, ownership, policy, relationships, and external
references.
> - The previous properties pane mixed too much behavior in one large
component and several rows could overflow or become hard to scan with
long labels and relation lists.
> - The UI needed a narrower, more reusable structure so compact
property rows, relation pills, external URL rows, and picker triggers
behave consistently.
> - This pull request splits the properties pane into focused helper
modules and tightens the pane's layout, truncation, popover, and
overflow behavior.
> - The benefit is a denser, more stable properties pane that stays
usable when issues have long titles, many relationships, external URLs,
or execution policy IDs.
## Linked Issues or Issue Description
No public issue exists for this work. I searched GitHub issues and PRs
for `IssueProperties properties pane`; there was no duplicate issue and
the matching PRs were unrelated test stabilization or other work.
### Problem or motivation
The board issue details properties pane is a frequent operator workflow
surface, but it was implemented as one large component and several rows
could become hard to scan with long labels, many relationships, external
URLs, or execution policy IDs.
### Proposed solution
Split the properties pane into focused modules, tighten compact row
layout and truncation behavior, add bounded previews with explicit
expansion controls for long relation and URL lists, and make execution
policy ID generation work on insecure origins as well as normal browser
origins.
### Alternatives considered
A smaller patch inside the existing monolithic component would fix
individual overflow symptoms, but it would keep related primitives,
picker behavior, relation controls, and external URL rendering tangled
in one file. The split keeps the behavior easier to test and review
without changing public APIs.
### Roadmap alignment
This is an incremental UI quality improvement to the existing board task
details surface. I checked `ROADMAP.md` and did not find overlapping
planned core feature work.
## What Changed
- Split the issue properties pane into focused modules for helpers,
primitives, relation controls, property pickers, and external object
rows.
- Cleaned up row spacing, truncation, picker trigger alignment, scroll
behavior, status color coverage, and long-value titles.
- Added bounded previews and expand/collapse controls for blocking,
sub-task, related-task, and external URL lists.
- Fixed execution policy ID generation so insecure origins fall back to
a stable base URL instead of throwing.
- Updated Storybook issue-management stories and expanded unit coverage
for the new properties pane behavior.
## Verification
- `git diff --check origin/master..HEAD`
- `pnpm --filter @paperclipai/ui exec vitest run
src/components/IssueProperties.test.tsx` — 38 tests passed
## Risks
Low to moderate risk. The changes are UI-only, but they touch a
frequently used task details surface. The main risk is a subtle layout
regression in an untested viewport or issue shape; the branch adds
targeted coverage for long relation and external URL lists.
> 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 with repository tool use, shell
execution, GitHub CLI access, and local test execution. Context window
size was 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)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Thinking Path
> - Paperclip is the open source control plane people use to manage AI
agents for work.
> - Environment variables and secrets sit in the configuration surfaces
that let agents, projects, routines, and company environments run with
the right runtime inputs.
> - The previous editor was a single legacy component with cramped row
behavior, weak secret conversion affordances, and duplicated handling
across several call sites.
> - Operators need a clearer editor that handles text values, secret
references, draft rows, and sensitive-value warnings consistently
wherever environment variables are configured.
> - This pull request replaces the legacy editor with a reusable
environment variables editor and migrates the existing configuration
surfaces to it.
> - The benefit is a more reliable editing workflow with targeted test
coverage around row state, dotenv parsing, secret selection, and
affected page integrations.
## Linked Issues or Issue Description
No public GitHub issue exists, so this PR describes the issue inline
following the feature request template.
### Subsystem affected
ui/ — React + Vite board UI
### Problem or motivation
Environment variables are edited in several Paperclip configuration
surfaces, including agent config, project properties, stage secrets,
routine sections, company environments, and company settings. The legacy
editor made common operator work difficult: rows could feel cramped,
secret conversion was inconsistent, draft rows and imported dotenv data
were easy to mishandle, and sensitive-value warnings did not have a
consistent place in the workflow.
### Proposed solution
Introduce a reusable environment variables editor component that
consistently supports text values, secret references, draft rows, dotenv
import parsing, sensitive-value hints, secret picking, secret creation,
and conversion to stored secrets. Migrate the existing
environment-variable call sites to the shared editor so behavior and
tests live in one component family.
### Alternatives considered
Keeping the existing `EnvVarEditor` and patching individual call sites
would preserve duplication and leave each surface responsible for its
own row and secret handling. This PR instead centralizes the behavior so
future fixes cover all migrated surfaces.
### Roadmap alignment
Checked `ROADMAP.md`; this does not duplicate a named roadmap item. It
supports the existing local-first and deployment-oriented product
direction by improving the UI where operators configure runtime
environment values.
### Additional context
The PR includes targeted tests for the editor model, dotenv parsing,
sensitive-value detection, component behavior, affected page
integrations, and Greptile review regressions around external saves and
bulk import immutability.
## What Changed
- Replaced the legacy `EnvVarEditor` with a reusable
`environment-variables-editor` component family.
- Added editor model helpers for draft rows, dotenv parsing,
sensitive-value detection, secret picking, secret creation, and
conversion to secret references.
- Migrated agent config, project properties, stage secrets, routine
editable sections, company environments, company settings, design guide
examples, and Storybook stories to the new editor.
- Added targeted tests for the editor model, parsing, sensitive-value
detection, component behavior, and affected company environment/settings
integrations.
- Fixed the company settings test harness to use the repo’s
`flushSync`-based React test helper pattern under the current React
build.
- Addressed Greptile feedback by flushing pending editor drafts before
enclosing form submits or external save-button clicks, cloning
bulk-import rows before mutation, and deferring the overflow
store-as-secret popover open path.
## Verification
- `pnpm exec vitest run
ui/src/components/environment-variables-editor/EnvironmentVariablesEditor.test.tsx
ui/src/pages/CompanyEnvironments.test.tsx
ui/src/components/AgentConfigForm.render.test.tsx` — passed, 3 files /
40 tests.
- Earlier focused Vitest coverage for model, dotenv parsing, sensitive
detection, company environments, and company settings passed, 6 files /
78 tests.
- `pnpm --filter @paperclipai/ui typecheck` — passed.
- GitHub PR checks on head `0aa49c6f8afed1f62d9e26da07fc466fbf299850` —
passed; CI checks green, security review neutral, Greptile check
success.
- Greptile review — 5/5 confidence on head
`0aa49c6f8afed1f62d9e26da07fc466fbf299850`; 0 unresolved review threads.
## Risks
- Medium UI risk: several environment-variable entry surfaces now share
the new editor, so regressions could affect multiple configuration
workflows at once.
- Secret conversion, draft-row behavior, external save flushing, and
bulk import behavior are covered by targeted tests, but reviewer
attention should still focus on manual editing flows, focus retention,
and save/cancel affordances.
- No database or API contract changes.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5-based coding agent with tool use and local command
execution; medium reasoning mode. Exact context-window metadata is 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)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - A core part of that experience is watching active agent runs without
dropping into raw logs first
> - Local and sandbox-backed adapters already record useful run output,
progress, and tool activity
> - But active issue threads could sit visually stale while the agent
was syncing workspaces, tailing sandbox output, or emitting incremental
tool-call updates
> - Operators need timely, human-readable progress while preserving the
raw transcript underneath
> - This pull request streams sandbox run-log progress into runtime
status, keeps visible issue threads refreshed, and folds repeated ACPX
tool updates into stable transcript cards
> - The benefit is that long-running agent work becomes easier to
supervise without changing the task/comment control-plane model
## Linked Issues or Issue Description
No public GitHub issue exists for this exact change.
Problem/motivation:
- During long-running sandboxed agent work, the issue UI can appear idle
even though the agent is actively syncing, running tools, or producing
incremental output.
- Operators need realtime feedback at the issue-thread layer, not only
after opening raw logs or waiting for the final heartbeat result.
- Related public context: #1808 previously added live-run status dots to
Projects; #4362 touches heartbeat wakeup behavior but is not a duplicate
of this runtime/UI feedback change.
## What Changed
- Added sandbox run-log streaming support and defaulted sandbox-capable
local adapters into the richer live-feedback path.
- Surfaced environment/sandbox sync progress through heartbeat runtime
status with bounded, redacted snippets.
- Added live issue-thread cache patching so visible active runs update
as progress events arrive.
- Folded repeated ACPX `tool_call` updates into one transcript card
instead of stacking duplicate cards.
- Updated adapter docs and added focused regression coverage for sandbox
log streaming, runtime status, ACPX parsing, live updates, transcript
rendering, and issue chat messages.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run ui/src/context/LiveUpdatesProvider.test.ts`
- `pnpm exec vitest run
server/src/services/heartbeat-run-runtime-status.test.ts
server/src/__tests__/heartbeat-runtime-state.test.ts
ui/src/context/LiveUpdatesProvider.test.ts`
- `pnpm exec vitest run
packages/adapter-utils/src/execution-target-sandbox.test.ts
packages/adapter-utils/src/sandbox-managed-runtime.test.ts
server/src/services/heartbeat-run-runtime-status.test.ts
server/src/__tests__/agent-live-run-routes.test.ts
server/src/__tests__/heartbeat-runtime-state.test.ts
packages/adapters/acpx-local/src/ui/parse-stdout.test.ts
ui/src/context/LiveUpdatesProvider.test.ts
ui/src/components/transcript/RunTranscriptView.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/components/IssueChatThread.test.tsx`
- GitHub PR workflow on head `8397953e7b41ccd42e5d9457ee7e4dfb996e4ec5`:
`verify`, build, typecheck/release-registry, e2e, general shards,
serialized server shards, and canary dry run passed.
- Greptile Review on head `8397953e7b41ccd42e5d9457ee7e4dfb996e4ec5`:
Confidence Score 5/5, no unresolved review threads.
## Risks
- Live issue-thread cache patching could miss an edge case for a route
shape not covered by tests.
- Surfacing active-run snippets needs continued care around redaction;
this PR keeps snippets bounded and adds redaction-focused coverage.
- More frequent active-run UI refreshes could expose performance issues
on very large issue threads, though updates are scoped to visible
run/query caches.
## Model Used
OpenAI GPT-5 via Codex, operating as a tool-enabled coding agent with
shell, git, and repository-editing capabilities. Context window size is
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)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - Adapter names are part of the board-facing agent setup and
management experience.
> - The product now treats adapters as harnesses, while execution
environments are modeled separately.
> - Several built-in adapter labels still carried legacy local wording
from the older harness-by-environment model.
> - That wording makes the UI noisier and implies a distinction users no
longer need to reason about.
> - This pull request normalizes adapter display labels while keeping
persisted adapter type identifiers unchanged.
> - The benefit is clearer adapter selection and management copy without
a database migration.
## Linked Issues or Issue Description
No public GitHub issue was found for this exact cleanup.
Related public PRs:
- Supersedes #8910, an earlier branch for the same cleanup that did not
include the later docs/gateway/Cursor alignment.
- Refs #8819, which is related display-registry work for external
multi-segment adapter labels, but not a duplicate of this built-in label
cleanup.
Feature request details:
- Subsystem affected: Cross-cutting (`ui/`, `packages/adapters`, and
docs).
- Problem or motivation: user-facing adapter names include legacy local
qualifiers even though adapters map to harnesses and environments are
first-class elsewhere.
- Proposed solution: remove the legacy local wording from built-in
display labels, keep machine-readable adapter type ids unchanged, and
keep gateway disambiguation where it is useful.
- Alternatives considered: changing persisted adapter type ids was ruled
out because it would create migration and compatibility risk; one-off UI
replacements were ruled out because the display registry is already the
correct central label boundary.
- Roadmap alignment: this is small adapter UX polish, not a new
roadmap-level core feature.
## What Changed
- Updated the adapter display registry so known adapter labels are final
and no built-in local adapter renders a legacy local suffix.
- Preserved clean derived labels for unknown plugin local types while
keeping gateway disambiguation for unknown gateway types.
- Updated `AdapterManager` to prefer registry labels when the server
reports raw adapter type ids for built-ins.
- Removed legacy local wording from built-in adapter metadata labels in
UI and adapter packages.
- Aligned Cursor adapter metadata with the central display registry
label.
- Updated adapter docs and Storybook fixtures to match the new display
names.
- Added focused registry coverage for built-in labels and unknown plugin
suffix behavior.
## Verification
- `pnpm check:tokens`
- `git diff --check origin/master...fix/adapter-display-labels`
- Patch-addition scan for added secrets, private paths, and internal
links: no matches.
- GitHub duplicate search for open adapter-label/local-suffix issues and
PRs; #8910 was identified as the older superseded public PR.
- `pnpm exec vitest run
ui/src/adapters/adapter-display-registry.test.ts`
- `pnpm --filter @paperclipai/ui typecheck`
- Stale-label scan found no remaining user-facing display-label
suffixes; remaining local wording is operational/test terminology such
as adapter ids, docs about running locally, and test descriptions.
## Risks
Low risk. The change is display-label and documentation focused, and
adapter type ids remain unchanged. The main risk is ambiguous gateway
naming, mitigated by keeping explicit gateway labels where variants need
disambiguation.
## Model Used
OpenAI GPT-5 via Codex, tool-enabled coding agent in a local repository
workspace. Context window size is not exposed by this 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] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent runs can execute inside reusable, runtime-created git worktree
execution workspaces.
> - Those managed worktrees record the expected branch so later
dispatches do not accidentally run an agent in the wrong checkout.
> - Successful run finalization already checked branch coherence, but it
treated every unrecorded branch switch as fatal.
> - A common publishing flow can briefly switch a clean worktree to a
PR/publish branch that points at the same commit as the recorded issue
branch, leaving no divergent work to protect.
> - This pull request keeps the strict finalization guard for unsafe
drift, but lets finalization restore the recorded branch when
same-commit repair is provably safe.
> - The benefit is fewer false failed runs after harmless branch
switches while preserving hard failures for divergent or dirty
worktrees.
## Linked Issues or Issue Description
No public issue exists for this exact finalization failure. Related
public worktree-recovery context: #3087 and #3056, but those address
different worktree realization/reuse recovery paths rather than
successful-run finalization branch repair.
Bug report details:
**What happened?**
When an adapter run succeeded after switching a managed git worktree
from its recorded issue branch to a publish/PR branch, finalization
failed with a managed worktree branch mismatch even when the publish
branch and recorded branch pointed at the same commit and the worktree
was clean.
**Expected behavior**
Finalization should restore the recorded branch only when it can prove
the worktree is clean, registered, and the recorded branch points at the
current `HEAD`. If the actual branch has different commits or unsafe
state, finalization should continue to fail with bounded validation
evidence.
**Steps to reproduce**
1. Create a runtime-managed `git_worktree` execution workspace for an
issue run.
2. During the adapter run, create and check out a new publish branch
without committing new changes.
3. Return adapter success and let heartbeat finalization run.
4. Before this change, finalization records a failed branch check and
fails the run even though the branches point at the same commit.
5. With this change, finalization records the repair operation, restores
the recorded branch, and records a successful finalize row.
6. Repeat with a commit on the publish branch; finalization still fails
because the branch heads differ.
**Paperclip version or commit**
Reproduced against `master` at `bac7307ec`; fixed by this PR at
`64ec605cf`.
**Deployment mode**
Local dev / built from source.
**Agent adapter(s) involved**
Not adapter-specific. This is core heartbeat/workspace finalization
behavior.
**Database mode**
Embedded test Postgres in the focused server test.
**Access context**
Agent run finalization.
**Node.js version**
`v25.6.1`
**Operating system**
`Darwin 24.6.0 arm64`
**Relevant logs or output**
The new focused test intentionally exercises both outcomes:
```text
Test Files 1 passed (1)
Tests 3 passed (3)
```
**Relevant config**
Runtime-created `git_worktree` execution workspace.
**Additional context**
The unsafe divergent branch case still fails with
`workspace_validation_failed` and `git_worktree_branch_incoherence`
evidence.
**Privacy checklist**
Reviewed; this description avoids internal task links, local workspace
paths, credentials, and instance-specific URLs.
## What Changed
- Reused the existing guarded branch-coherence repair helper during
heartbeat finalization when the final branch inspection finds clean
same-commit branch drift.
- Recorded repair metadata in the `workspace_finalize` operation so
reviewers/operators can audit whether finalization repaired branch
drift.
- Preserved failure behavior for divergent branch heads and surfaced the
bounded workspace validation evidence from the repair helper.
- Added focused server coverage for safe finalization repair and unsafe
divergent branch failure.
- Updated execution semantics docs to describe the narrower finalization
rule.
## Verification
- `pnpm exec vitest run
server/src/__tests__/heartbeat-workspace-finalize-branch.test.ts`
- `pnpm --filter @paperclipai/server typecheck`
- `git diff --check`
## Risks
Low to medium risk. The change affects successful-run finalization for
runtime-created git worktree execution workspaces. The repair path is
constrained to clean, registered, same-commit branch drift, and the
focused test confirms divergent branch heads still fail instead of being
restored silently.
## Model Used
OpenAI Codex, GPT-5-based coding agent. Exact hosted model ID was not
exposed in the runtime; tool use and local shell execution were enabled.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent adapters share managed-runtime helpers from
`@paperclipai/adapter-utils` so local and sandboxed runs can prepare,
execute, and restore workspaces consistently.
> - The sandbox managed runtime syncs workspaces in both directions by
creating tar archives on the local host and inside the remote sandbox.
> - A prior fix avoided archiving `.` during upload because tar
self-entries can force chmod/utime on a directory the command user does
not own.
> - The restore/download path still archived the remote workspace as
`.`, and failed managed commands only surfaced stderr.
> - This pull request applies entry-based tar creation to the sandbox
restore path and includes stdout in managed command failure diagnostics.
> - The benefit is that restore failures become visible to operators and
sandbox workspace restore avoids the same directory-metadata failure
class already fixed for upload.
## Linked Issues or Issue Description
No existing public issue found; describing in-PR (bug).
- **What happens:** sandbox workspace restore can fail while creating
`workspace-download.tar` from the remote workspace when the tar command
includes a `.` self-entry and the command user cannot update metadata on
the workspace directory. If the failing command writes its diagnostic to
stdout, the managed runtime error can collapse to a generic failed shell
command without the useful tar message.
- **Expected behavior:** restore should archive the workspace entries
without a `.` self-entry, and failed managed runtime commands should
include useful stdout/stderr diagnostics.
- **Where:** `packages/adapter-utils/src/sandbox-managed-runtime.ts`
restore/download path and
`packages/adapter-utils/src/command-managed-runtime.ts` command error
formatting.
- **Related public context:** #7836 fixed the upload side of the same
tar self-entry failure class.
## What Changed
- Added stdout-aware failed-command formatting in the command managed
runtime, keeping diagnostics bounded to the tail of stdout/stderr.
- Added remote workspace tarball creation that names top-level entries
explicitly instead of archiving `.` during sandbox restore.
- Preserved empty-workspace restore support by creating a valid empty
tarball when the remote workspace has no entries.
- Added regression coverage for stdout diagnostics, restore tar members,
and empty workspace restore tarballs.
## Verification
- `pnpm vitest run
packages/adapter-utils/src/command-managed-runtime.test.ts
packages/adapter-utils/src/sandbox-managed-runtime.test.ts` — 2 files,
17 tests passed.
- `pnpm --filter @paperclipai/adapter-utils typecheck` — passed.
- `git diff --check origin/master..HEAD` — passed.
- Local public-hygiene/PII scan of the committed diff checked for
internal ticket refs, local/private URLs, common token/key patterns,
private-key blocks, and email-like values — passed.
- GitHub duplicate/related search found no public issue or PR for
`workspace-download.tar Permission denied` or `sandbox restore tar
permission denied`; #7836 is linked as related prior work.
- PR CI on commit `3ad4c8d` — all GitHub Actions lanes, security scans,
and aggregate `verify` passed.
- Greptile Review on commit `3ad4c8d` — Confidence Score 5/5; the prior
P2 thread is resolved with no open P2s, recommendations, or follow-ups.
## Risks
Low. This is limited to shared adapter runtime error formatting and
sandbox restore archive construction. Archive contents should remain
equivalent apart from the removed `.` self-entry, and the new
diagnostics are bounded to avoid dumping unbounded command output.
## Model Used
OpenAI Codex, GPT-5, tool-enabled coding agent with shell and GitHub CLI
access. Context window size was not reported by the runtime.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes (n/a;
internal adapter-runtime behavior only)
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Issue-thread interactions are the subsystem that lets board users
answer structured prompts and resume agent work
> - Checkbox confirmations capture a selected subset of known options,
then wake the assignee through continuation context
> - The wake context previously carried generic interaction metadata,
but not the accepted checkbox option ids or option labels
> - That meant the resumed agent could be woken after a checkbox
confirmation without seeing the board's selected options in the turn
context
> - This pull request carries accepted checkbox selections through the
interaction continuation wake snapshot and renders them into the adapter
wake prompt
> - The benefit is that agents can act on checkbox-confirmation
selections without refetching or guessing the user's choices
## Linked Issues or Issue Description
No public GitHub issue exists for this bug. Searched public issues and
PRs for checkbox confirmation / continuation selection duplicates and
found no matching issue or PR.
### Pre-submission checklist
- [x] I have searched existing open and closed issues and this is not a
duplicate.
- [x] I am on the latest released version of Paperclip or can reproduce
on `master`.
- [x] I have confirmed the error originates in Paperclip itself, not in
my agent adapter, API provider, or local configuration.
### What happened?
When a board user accepted a `request_checkbox_confirmation`
interaction, the assignee continuation wake included generic interaction
metadata but did not include the accepted checkbox selections. The
resumed agent turn therefore had no in-prompt access to the selected
option ids or option labels/descriptions.
### Expected behavior
When a `request_checkbox_confirmation` interaction is accepted, the
resumed agent wake should include the checkbox prompt, selected option
ids, and selected option labels/descriptions so the agent can act on the
selected subset directly.
### Steps to reproduce
1. Create an issue-thread `request_checkbox_confirmation` interaction
with multiple options and `continuationPolicy: "wake_assignee"`.
2. Accept the interaction with one or more selected options.
3. Inspect the continuation wake payload/prompt received by the
assignee.
4. Observe that the selected checkbox options are missing from the wake
context before this fix.
### Paperclip version or commit
Reproduced against the pre-fix code path on `master`; this PR head is
`9d17e70bce373e4850117f30c015c973c4b61789`.
### 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 bug). The Codex/local adapter path exposed
the missing wake context, but the missing field was in core interaction
continuation payload construction.
### Database mode
Embedded PGlite or external Postgres; the bug is not database-mode
specific.
### Access context
Both. Board users resolve the checkbox interaction, and agent bearer-key
wakes consume the continuation context.
### Node.js version
`v22.22.2`
### Operating system
Linux workspace.
### Relevant logs or output
No runtime exception is required to reproduce this. The failure mode is
missing `checkboxSelection` data in the resolved interaction
continuation wake payload.
### Relevant config
Not config-related.
### Additional context
Root cause: accepted checkbox interaction results were not extracted
into the continuation wake context, and adapter wake payload
normalization/rendering had no typed `checkboxSelection` field.
### 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 checkbox selection extraction for accepted
`request_checkbox_confirmation` interactions and stored it in
interaction continuation wake context.
- Included checkbox selection context in heartbeat wake payload
construction.
- Added adapter-utils normalization and wake prompt rendering for
checkbox prompt, selected ids, and selected option details.
- Added regression coverage for route continuation context, heartbeat
payload summaries, and adapter wake prompt rendering.
## Verification
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/heartbeat-context-summary.test.ts
server/src/__tests__/issue-thread-interaction-routes.test.ts`
- `git diff --check origin/master...HEAD`
- `rg -n "checkbox|confirmation|interaction|wake|continuation"
ROADMAP.md`
- `gh pr list --state all --search "checkbox continuation selection
repo:paperclipai/paperclip" --json number,title,state,url,headRefName
--limit 20`
- `gh issue list --state all --search "checkbox confirmation options
repo:paperclipai/paperclip" --json number,title,state,url --limit 20`
## Risks
Low risk. The new payload field is additive, only populated for accepted
checkbox confirmations, and existing continuation fields are preserved.
The main compatibility risk is downstream code assuming an exact wake
payload shape; adapter normalization treats the new field as optional.
> 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 coding agent based on GPT-5, with shell/tool execution in
this workspace.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>