## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The issue detail page displays comment threads with rich timeline
rendering
> - Long threads (100+ items) cause severe typing lag in the comment
composer because every keystroke re-renders the entire timeline
> - CDP tracing confirmed 110ms avg key→paint latency and 60 long tasks
blocking the main thread for 3.7s total
> - This pull request memoizes the timeline, stabilizes callback props,
debounces editor observers, and reduces idle polling frequency
> - The benefit is responsive typing (21ms avg, 5.3× faster) even on
threads with 100+ timeline items
## What Changed
- **CommentThread.tsx**: Memoize `TimelineList` with `useMemo` so typing
state changes don't re-render 143 timeline items; extract
`handleFeedbackVote` to `useCallback`; added missing deps
(`pendingApprovalAction`, `onApproveApproval`, `onRejectApproval`) to
useMemo array
- **IssueDetail.tsx**: Extract inline callbacks (`handleCommentAdd`,
`handleCommentVote`, `handleCommentImageUpload`,
`handleCommentAttachImage`, `handleInterruptQueued`) to `useCallback`
with `.mutateAsync` deps (not full mutation objects) for stable
references; add conditional polling intervals (3s active / 30s idle) for
`liveRuns`, `activeRun`, `linkedRuns`, and timeline queries
- **MarkdownEditor.tsx**: Debounce `MutationObserver` and
`selectionchange` handlers via `requestAnimationFrame` coalescing
- **LiveRunWidget.tsx**: Accept optional `liveRunsData` and
`activeRunData` props to reuse parent-fetched data instead of duplicate
polling
## Verification
- Navigated to [IP address]:3105/PAPA/issues/PAPA-32 (thread with 100+
items)
- Typed in comment composer — lag eliminated, characters appear
instantly
- CDP trace test script (`test-typing-lag.mjs`) confirmed: avg 21ms
key→paint (was 110ms), 5 long tasks (was 60), 0.5s blocking (was 3.7s)
- Ran `pnpm test:run` locally — all tests pass
## Risks
- Low risk. All changes are additive memoization and callback
stabilization — no behavioral changes. Polling intervals are only
reduced for idle state; active runs still poll at 3–5s.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI, with tool use
and extended context
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
- backup-lib: delete uncompressed .sql file in catch block when gzip
compression fails, preventing silent disk usage accumulation
- server: replace stale retentionDays scalar with retentionSource in
startup log since retention is now read from DB on each backup tick
Replace single retentionDays with a three-tier BackupRetentionPolicy:
- Daily: keep all backups (presets: 3, 7, 14 days; default 7)
- Weekly: keep one per calendar week (presets: 1, 2, 4 weeks; default 4)
- Monthly: keep one per calendar month (presets: 1, 3, 6 months; default 1)
Pruning sorts backups newest-first and applies each tier's cutoff,
keeping only the newest entry per ISO week/month bucket. The Instance
Settings General page now shows three preset selectors (no icon, matches
existing page design). Remove Database icon import.
Compress database backups with gzip (.sql.gz), reducing file size ~83%.
Add backup retention configuration to Instance Settings UI with preset
options (7 days, 2 weeks, 1 month). The backup scheduler now reads
retention from the database on each tick so changes take effect without
restart. Default retention changed from 30 to 7 days.
On resumed sessions, skipping --append-system-prompt-file (the original
fix) left two secondary issues:
- commandNotes still claimed the flag was injected, producing misleading
onMeta logs on every resumed heartbeat
- The instructions file was still read from disk and a combined temp file
written on every resume, even though effectiveInstructionsFilePath was
never consumed
Hoist canResumeSession before the I/O block and gate both the disk
operations and commandNotes construction on !canResumeSession / !sessionId.
Adds three regression tests: commandNotes is populated on fresh sessions,
empty on resume; and no agent-instructions.md is written on resume.
On resumed sessions the agent instructions are already present in the
session cache. Unconditionally passing --append-system-prompt-file
re-injects 5-10K redundant tokens per heartbeat and may be rejected by
the Claude CLI when combined with --resume.
Guard the flag behind `!resumeSessionId` so it is only appended on
fresh session starts.
Fixes: #2848
Previously, --model was completely skipped for Bedrock users, so the
model dropdown selection was silently ignored and the CLI always used
its default model. Selecting Haiku would still run Opus.
- Add listClaudeModels() that returns Bedrock-native model IDs
(us.anthropic.*) when Bedrock env is detected
- Register listModels on claude_local adapter so the UI dropdown
shows Bedrock models instead of Anthropic API names
- Allow --model to pass through when the ID is a Bedrock-native
identifier (us.anthropic.* or ARN)
- Add isBedrockModelId() helper shared by execute.ts and test.ts
Follows up on #2793 which added basic Bedrock auth detection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Set server.keepAliveTimeout to 185s to safely outlive default Traefik/AWS ALB idle timeouts (typically 60-180s)
- Resolves random "Failed to fetch" edge cases caused by Node.js's notoriously short 5s default timeout
Closes#3008
## Thinking Path
Paperclip orchestrates AI agent runs and reports their success or
failure. The Pi adapter spawns a local Pi process and interprets its
JSONL output to determine the run outcome. When Pi hits a quota limit
(429 RESOURCE_EXHAUSTED), it retries internally and emits an
`auto_retry_end` event with `success: false` — but still exits with code
0. The current adapter trusts the exit code, so Paperclip marks the run
as succeeded even though it produced no useful work. This PR teaches the
parser to detect quota exhaustion and synthesize a failure.
Closes#2234
## Changes
- Parse `auto_retry_end` events with `success: false` into
`result.errors`
- Parse standalone `error` events into `result.errors`
- Synthesize exit code 1 when Pi exits 0 but parsed errors exist
- Use the parsed error as `errorMessage` so the failure reason is
visible in the UI
## Verification
```bash
pnpm vitest run pi-local-execute
pnpm vitest run --reporter=verbose 2>&1 | grep pi-local
```
- `parse.test.ts`: covers failed retry, successful retry (no error),
standalone error events, and empty error messages
- `pi-local-execute.test.ts`: end-to-end test with a fake Pi binary that
emits `auto_retry_end` + exits 0, asserts the run is marked failed
## Risks
- **Low**: Only affects runs where Pi exits 0 with a parsed error — no
change to normal successful or already-failing runs
- If Pi emits `auto_retry_end { success: false }` but the run actually
produced valid output, this would incorrectly mark it as failed. This
seems unlikely given the semantics of the event.
## Model Used
- Claude Opus 4.6 (Anthropic) — assisted with test additions and PR
template
## Checklist
- [x] Thinking path documented
- [x] Model specified
- [x] Tests pass locally
- [x] Test coverage for new parse branches (success path, error events,
empty messages)
- [x] No UI changes
- [x] Risk analysis included
---------
Co-authored-by: Dawid Piaskowski <dawid@MacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Closes#2412
Related: #2681, #498, #128
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The Claude Code adapter spawns the `claude` CLI to run agent tasks
> - The adapter detects auth mode by checking for `ANTHROPIC_API_KEY` —
recognizing only "api" and "subscription" modes
> - But users running Claude Code via **AWS Bedrock**
(`CLAUDE_CODE_USE_BEDROCK=1`) fall through to the "subscription" path
> - This causes a misleading "ANTHROPIC_API_KEY is not set;
subscription-based auth can be used" message in the environment check
> - Additionally, the hello probe passes `--model claude-opus-4-6` which
is **not a valid Bedrock model identifier**, causing `400 The provided
model identifier is invalid` and a probe failure
> - This pull request adds Bedrock auth detection, skips the
Anthropic-style `--model` flag for Bedrock, and returns the correct
billing type
> - The benefit is that Bedrock users get a working environment check
and correct cost tracking out of the box
---
## Pain Point
Many enterprise teams use **Claude Code through AWS Bedrock** rather
than Anthropic's direct API — for compliance, billing consolidation, or
VPC requirements. Currently, these users hit a **hard wall during
onboarding**:
| Problem | Impact |
|---|---|
| ❌ Adapter environment check **always fails** | Users cannot create
their first agent — blocked at step 1 |
| ❌ `--model claude-opus-4-6` is **invalid on Bedrock** (requires
`us.anthropic.*` format) | Hello probe exits with code 1: `400 The
provided model identifier is invalid` |
| ❌ Auth shown as _"subscription-based"_ | Misleading — Bedrock is
neither subscription nor API-key auth |
| ❌ Quota polling hits Anthropic OAuth endpoint | Fails silently for
Bedrock users who have no Anthropic subscription |
> **Bottom line**: Paperclip is completely unusable for Bedrock users
out of the box.
## Why Bedrock Matters
AWS Bedrock is a major deployment path for Claude in enterprise
environments:
- **Enterprise compliance** — data stays within the customer's AWS
account and VPC
- **Unified billing** — Claude usage appears on the existing AWS
invoice, no separate Anthropic billing
- **IAM integration** — access controlled through AWS IAM roles and
policies
- **Regional deployment** — models run in the customer's preferred AWS
region
Supporting Bedrock unlocks Paperclip for organizations that **cannot**
use Anthropic's direct API due to procurement, security, or regulatory
constraints.
---
## What Changed
- **`execute.ts`**: Added `isBedrockAuth()` helper that checks
`CLAUDE_CODE_USE_BEDROCK` and `ANTHROPIC_BEDROCK_BASE_URL` env vars.
`resolveClaudeBillingType()` now returns `"metered_api"` for Bedrock.
Biller set to `"aws_bedrock"`. Skips `--model` flag when Bedrock is
active (Anthropic-style model IDs are invalid on Bedrock; the CLI uses
its own configured model).
- **`test.ts`**: Environment check now detects Bedrock env vars (from
adapter config or server env) and shows `"AWS Bedrock auth detected.
Claude will use Bedrock for inference."` instead of the misleading
subscription message. Also skips `--model` in the hello probe for
Bedrock.
- **`quota.ts`**: Early return with `{ ok: true, windows: [] }` when
Bedrock is active — Bedrock usage is billed through AWS, not Anthropic's
subscription quota system.
- **`ui/src/lib/utils.ts`**: Added `"aws_bedrock"` → `"AWS Bedrock"` to
`providerDisplayName()` and `quotaSourceDisplayName()`.
## Verification
1. `pnpm -r typecheck` — all packages pass
2. Unit tests added and passing (6/6)
3. Environment check with Bedrock env vars:
| | Before | After |
|---|---|---|
| **Status** | 🔴 Failed | ✅ Passed |
| **Auth message** | `ANTHROPIC_API_KEY is not set; subscription-based
auth can be used if Claude is logged in.` | `AWS Bedrock auth detected.
Claude will use Bedrock for inference.` |
| **Hello probe** | `ERROR · Claude hello probe failed.` (exit code 1 —
`--model claude-opus-4-6` is invalid on Bedrock) | `INFO · Claude hello
probe succeeded.` |
| **Screenshot** | <img height="500" alt="Screenshot 2026-04-05 at 8 25
27 AM"
src="https://github.com/user-attachments/assets/476431f6-6139-425a-8abc-97875d653657"
/> | <img height="500" alt="Screenshot 2026-04-05 at 8 31 58 AM"
src="https://github.com/user-attachments/assets/d388ce87-c5e6-4574-b8d2-fd8b86135299"
/> |
4. Existing API key / subscription paths are completely untouched unless
Bedrock env vars are present
## Risks
- **Low risk.** All changes are additive — existing "api" and
"subscription" code paths are only entered when Bedrock env vars are
absent.
- When Bedrock is active, the `--model` flag is skipped, so the
Paperclip model dropdown selection is ignored in favor of the Claude
CLI's own model config. This is intentional since Bedrock requires
different model identifiers.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`, 1M context window) via Claude Code
CLI
## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Compare Buffer byte lengths instead of string character lengths before
timingSafeEqual to avoid potential mismatch with multi-byte input.
Add comment explaining the hubSignatureHeader ?? signatureHeader fallback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds two new webhook trigger signing modes for external provider
compatibility:
- github_hmac: accepts X-Hub-Signature-256 header with
HMAC-SHA256(secret, rawBody), no timestamp prefix. Compatible with
GitHub, Sentry, and services following the same standard.
- none: no authentication; the 24-char hex publicId in the URL acts
as the shared secret. For services that cannot add auth headers.
The replay window UI field is hidden when these modes are selected
since neither uses timestamp-based replay protection.
Closes#1892
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pino-pretty's translateTime: "HH:MM:ss" formats all timestamps in UTC,
ignoring the process TZ environment variable. Changing the prefix to
"SYS:HH:MM:ss" instructs pino-pretty to format timestamps in the local
system timezone, so operators running in non-UTC zones see correct
wall-clock times in both the console and the server.log file.
Fixes: #2879
Express 5 (path-to-regexp v8+) dropped support for the *paramName
wildcard syntax from Express 4. The route registered as
'/api/auth/*authPath' silently fails to match any sub-path, causing
every /api/auth/* request to return 404 instead of reaching the
better-auth handler.
Fixes: #2898
Change the route to '/api/auth/{*authPath}', the correct named
catch-all syntax in Express 5.
`jwtConfig()` in `agent-auth-jwt.ts` only read `PAPERCLIP_AGENT_JWT_SECRET`.
Deployments that set `BETTER_AUTH_SECRET` (required for authenticated mode)
but omit the separate `PAPERCLIP_AGENT_JWT_SECRET` variable received the
warning "local agent jwt secret missing or invalid; running without injected
PAPERCLIP_API_KEY" on every `claude_local` / `codex_local` heartbeat run,
leaving agents unable to call the API.
Every other auth path in the server (`better-auth.ts`, `index.ts`) already
falls back from `BETTER_AUTH_SECRET` to cover this case — align `jwtConfig()`
with the same pattern.
Adds a test for the fallback path.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Bumps multer from ^2.0.2 to ^2.1.1 in server/package.json to resolve
three HIGH-severity DoS vulnerabilities:
- GHSA-xf7r-hgr6-v32p (incomplete cleanup)
- GHSA-v52c-386h-88mc (crafted multipart)
- GHSA-2m88-8c7h-36gr (resource exhaustion)
All three are fixed in multer >= 2.1.0.
Fixes#2753
Use GREATEST(counter, MAX(issue_number)) + 1 when incrementing the
company issue counter. This self-corrects any desync between the
companies.issue_counter column and the actual max issues.issue_number,
preventing duplicate key violations on the identifier unique index.
Fixes#2705
Replace the client-side-only override store with a real server-side
toggle. When a developer pauses the external override, the server swaps
ALL adapter behavior back to the builtin — execute handler, model listing,
config schema, detection — not just the UI parser.
Server changes:
- registry.ts: builtinFallbacks map + pausedOverrides set + setOverridePaused()
- routes/adapters.ts: PATCH /api/adapters/:type/override endpoint + overridePaused in list
UI changes:
- adapters.ts: setOverridePaused API method + overridePaused on AdapterInfo
- AdapterManager: overrideMutation calls server, instant feedback via invalidate()
- use-disabled-adapters.ts: reads adapter.overridePaused from server response
Removed:
- disabled-overrides-store.ts: no longer needed (server is the source of truth)
Note: already-running agent sessions keep the adapter they started with.
Only new sessions use the swapped adapter.
Builtin adapter types (hermes_local, openclaw_gateway, etc.) could not
be overridden by external adapters on the UI side. The registry always
returned the built-in parser, ignoring the external ui-parser.js shipped
by packages like hermes-paperclip-adapter.
Changes:
- registry.ts: full override lifecycle with generation guard for stale loads
- disabled-overrides-store.ts: client-side override pause state with
useSyncExternalStore reactivity (persisted to localStorage)
- use-disabled-adapters.ts: subscribe to override store changes
- AdapterManager.tsx: separate controls for override pause (client-side)
vs menu visibility (server-side), virtual builtin rows with badges
- adapters.ts: allow reload/reinstall of builtin types when overridden
When an external plugin overrides a built-in adapter type, the
GET /api/adapters response now includes overriddenBuiltin: true. The
Adapter Manager shows an 'Overrides built-in' badge on such adapters.
Previously external adapters matching a built-in type were skipped with
a warning. Now they override the built-in, so plugin developers can ship
improved versions of existing adapters (e.g. hermes-paperclip-adapter)
without removing the built-in fallback for users who haven't installed
the plugin.
- Sort GET /api/adapters alphabetically by type (reload no longer shuffles)
- Show red Package icon for npm adapters, amber FolderOpen for local path
- Add reinstall confirmation dialog with current vs latest npm version
- Enable Vite polling when running on /mnt/ (WSL inotify doesn't work on NTFS)
- Fix external adapters (hermes, droid) not auto-selected when
navigating with ?adapterType= param — was using a stale
module-level Set built before async adapter registration
- Move SchemaConfigFields to render after thinking effort (same
visual area as Claude's chrome toggle) instead of bottom of
config section
- Extract SelectField into its own component to fix React hooks
order violation when schema fields change between renders
- Add onAdapterChange() subscription in registry.ts so
registerUIAdapter() notifies components when dynamic parsers
load, fixing stale parser for old runs
- Add parserTick to both RunTranscriptView and
useLiveRunTranscripts to force recomputation on parser change
Re-align phase1 with upstream: hermes_local ships via hermes-paperclip-adapter on the server and UI (hermes-local module). Fixes ERR_PNPM_OUTDATED_LOCKFILE from server/package.json missing a dep still present in the lockfile.
Add shared BUILTIN_ADAPTER_TYPES and skip external plugin registration when it would override a built-in type. Docs list Hermes as built-in; Droid remains the primary external example.
Made-with: Cursor
- Plugin loader: install/reload/remove/reinstall external adapters
from npm packages or local directories
- Plugin store persisted at ~/.paperclip/adapter-plugins.json
- Self-healing UI parser resolution with version caching
- UI: Adapter Manager page, dynamic loader, display registry
with humanized names for unknown adapter types
- Dev watch: exclude adapter-plugins dir from tsx watcher
to prevent mid-request server restarts during reinstall
- All consumer fallbacks use getAdapterLabel() for consistent display
- AdapterTypeDropdown uses controlled open state for proper close behavior
- Remove hermes-local from built-in UI (externalized to plugin)
- Add docs for external adapters and UI parser contract
The PII sanitizer's phone regex matches digit pairs like "4880-8614"
that span UUID segment boundaries. Random UUIDs occasionally produce
these patterns, causing flaky test failures where sourceRun.id gets
partially redacted as [REDACTED_PHONE].
Use a fixed hex-letter-heavy UUID for runId so no cross-boundary
digit sequence triggers the phone pattern.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Root cause: when someone commented on an in_review task, the heartbeat
wakeup was triggered but the agent couldn't re-checkout the task because
expectedStatuses only included todo/backlog/blocked. The in_review status
was never handled in the checkout flow or the heartbeat procedure.
Changes:
- Add wakeCommentId to issue_commented and issue_reopened_via_comment
context snapshots (consistent with issue_comment_mentioned)
- Add in_review to checkout expectedStatuses in heartbeat skill
- Update Step 3 fallback query to include in_review status
- Update Step 4 to prioritize in_review tasks when woken by comment
- Add explicit issue_commented wake reason handling in Step 4
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
Pino logged full Authorization headers in plaintext to server.log,
exposing JWT tokens to any process with filesystem read access.
Add redact paths so Bearer values appear as [Redacted] in log output.
Closes#2385
Add test confirming that when a package's .paperclip.yaml extension
block omits the role field, the agent role is read from AGENTS.md
frontmatter instead of defaulting to "agent".
Package imports defaulted every agent's role to "agent" when the
extension block omitted the role field, even when the YAML frontmatter
contained the correct role (e.g. "ceo"). Read from frontmatter as a
fallback before the "agent" default so imported CEOs retain their role.
Closes#1990
Issue 1: add executionAgentNameKey = null alongside executionRunId in
Fix B (status change, reassignment) and Fix C (staleness clear UPDATE),
matching the existing pattern used everywhere else in the codebase.
Issue 2: wrap Fix C staleness pre-check in a db.transaction with
SELECT ... FOR UPDATE to make the read + conditional clear atomic,
consistent with the enqueueWakeup() pattern.
Part A: Move executionRunId assignment from enqueueWakeup() to
claimQueuedRun() — lazy locking prevents stale locks on queued runs.
Part B: Clear executionRunId when assigneeAgentId changes in issues.ts
line 759, matching existing checkoutRunId clear behavior.
Part C: Add staleness detection at checkout path.
Fixes: 4 confirmed incidents where stale executionRunId caused 409
checkout conflicts on new and reassigned issues.
Two code paths in issueService.checkout() used rows[0]! when
re-reading an issue after stale-run adoption or self-ownership
verification. If the issue is deleted concurrently (company cascade,
API delete), rows[0] is undefined and withIssueLabels crashes with
an unhandled TypeError.
Replace both with rows[0] ?? null and throw notFound when the row
is missing, returning a clean 404 instead of an uncaught exception.
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Agents run shell commands during workspace provisioning (git
worktree creation, runtime services)
> - When `process.env.SHELL` is unset, the code falls back to `/bin/sh`
> - But on Windows with Git Bash, `/bin/sh` doesn't exist as an absolute
path — Git Bash provides `sh` on PATH instead
> - This causes `child_process.spawn` to throw `ENOENT`, crashing
workspace provisioning on Windows
> - This PR extracts a `resolveShell()` helper that uses `$SHELL` when
set, falls back to `sh` (bare) on Windows or `/bin/sh` on Unix
> - The benefit is that agents running on Windows via Git Bash can
provision workspaces without shell resolution errors
## Summary
- `workspace-runtime.ts` falls back to `/bin/sh` when
`process.env.SHELL` is unset
- On Windows, `/bin/sh` doesn't exist → `spawn /bin/sh ENOENT`
- Fix: extract `resolveShell()` helper that uses `$SHELL` when set,
falls back to `sh` on Windows (Git Bash PATH lookup) or `/bin/sh` on
Unix
Three call sites updated to use the new helper.
Fixes#892
## Root cause
When Paperclip spawns shell commands in workspace operations (e.g., git
worktree creation), it uses `process.env.SHELL` if set, otherwise
defaults to `/bin/sh`. On Windows with Git Bash, `$SHELL` is typically
unset and `/bin/sh` is not a valid path — Git Bash provides `sh` on PATH
but not at the absolute `/bin/sh` location. This causes
`child_process.spawn` to throw `ENOENT`.
## Approach
Rather than hard-coding a Windows-specific absolute path (e.g.,
`C:\Program Files\Git\bin\sh.exe`), we use the bare `"sh"` command which
relies on PATH resolution. This works because:
1. Git Bash adds its `usr/bin` directory to PATH, making `sh` resolvable
2. On Unix/macOS, `/bin/sh` remains the correct default (it's the POSIX
standard location)
3. `process.env.SHELL` takes priority when set, so this only affects the
fallback
## Test plan
- [x] 7 unit tests for `resolveShell()`: SHELL set, trimmed, empty,
whitespace-only, linux/darwin/win32 fallbacks
- [x] Run a workspace provision command on Windows with `git_worktree`
strategy
- [x] Verify Unix/macOS is unaffected
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Devin Foley <devin@devinfoley.com>
Agents receiving issue context via GET /issues/:id/heartbeat-context
had no way to discover file attachments — the endpoint returned issue
metadata, ancestors, project, goal, and comment cursor but omitted
attachments entirely. Users attaching files through the UI would then
see agents ask for documents that were already uploaded.
Fetch attachments in parallel with the existing queries and append a
lightweight summary (id, filename, contentType, byteSize, contentPath)
to the response so agents can detect and retrieve attached files on
their first heartbeat without an extra round-trip.
Closes#2536
The agent.task_completed event was sending adapterType (e.g. "claude_local")
as the agent_role dimension instead of the actual role (e.g. "engineer").
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Expose telemetry.track through the plugin SDK and server host bridge, forward plugin-prefixed events into the shared telemetry client, and demonstrate the capability in the kitchen sink example.\n\nCo-Authored-By: Paperclip <noreply@paperclip.ing>
Restructure the TelemetryClient to send the correct backend envelope
format ({app, schemaVersion, installId, events: [{name, occurredAt, dimensions}]})
instead of the old per-event format. Update all event dimension names
to match the backend registry (agent_role, adapter_type, error_code, etc.).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The TelemetryClient only flushed at 50 events, so the server silently
lost all queued telemetry on restart. Add startPeriodicFlush/stop methods
to TelemetryClient, wire up 60s periodic flush in server initTelemetry,
and flush on SIGTERM/SIGINT before exit.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add the shared telemetry sender, wire the CLI/server emit points,
and cover the config and completion behavior with tests.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Update line 3 to describe personal files relative to the instructions
directory, consistent with the ./path changes in the rest of the file.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
$AGENT_HOME resolves to the workspace directory, not the instructions
directory where sibling files (HEARTBEAT.md, SOUL.md, TOOLS.md) live.
This caused ~25% of agent runs to fail. Relative paths align with the
adapter's injected directive to resolve from the instructions directory.
Closes#2530
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: auto-detect default branch for worktree creation when baseRef not configured
When creating git worktrees, if no explicit baseRef is configured in
the project workspace strategy and no repoRef is set, the system now
auto-detects the repository's default branch instead of blindly
falling back to "HEAD".
Detection strategy:
1. Check refs/remotes/origin/HEAD (set by git clone / remote set-head)
2. Fall back to probing refs/remotes/origin/main, then origin/master
3. Final fallback: HEAD (preserves existing behavior)
This prevents failures like "fatal: invalid reference: main" when a
project's workspace strategy has no baseRef and the repo uses a
non-standard default branch name.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix: address Greptile review - fix misleading comment and add symbolic-ref test
- Corrected comment to clarify that the existing test exercises the
heuristic fallback path (not symbolic-ref)
- Added new test case that explicitly sets refs/remotes/origin/HEAD
via `git remote set-head` to exercise the symbolic-ref code path
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
Added functionality to prevent deletion of skills that are still in use by agents. Updated the company skill service to throw an unprocessable error if a skill is attempted to be deleted while still referenced by agents. Enhanced the UI to include a delete button and confirmation dialog, displaying relevant messages based on agent usage. Updated tests to cover the new deletion logic and error handling.
When the adapter type changes via PATCH, the server only preserved
instruction bundle keys (instructionsBundleMode, etc.) from the
existing config. Adapter-agnostic keys like env, cwd, timeoutSec,
graceSec, promptTemplate, and bootstrapPromptTemplate were silently
dropped if the PATCH payload didn't explicitly include them.
This caused env var data loss when adapter type was changed via the
UI or API without sending the full existing adapterConfig.
The fix preserves these adapter-agnostic keys from the existing config
before applying the instruction bundle preservation, matching the
UI's behavior in AgentConfigForm.handleSave.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Timer wakes had no taskKey, so they couldn't use agentTaskSessions for
session resume. Adds a synthetic __heartbeat__ task key for timer wakes
so they participate in the full session system.
Includes 6 dedicated unit tests for deriveTaskKeyWithHeartbeatFallback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
j/k navigate up/down, a to archive, U to mark unread, r to mark read,
Enter to open. Includes server-side DELETE /issues/:id/read endpoint
for mark-unread support on issues.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Allow shared execution workspace sessions to be archived with warnings instead of hard-blocking on open linked issues, clear issue workspace links when those shared sessions are archived, and update the close dialog copy and coverage.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Expose project and execution workspace runtime defaults, control endpoints, startup recovery, and operator UI for start/stop/restart flows.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The dev-watch script was importing tsx via the internal path
'tsx/dist/cli.mjs', which is an undocumented implementation detail
that broke when tsx updated its internal structure.
Switched to the stable public export 'tsx/cli' which is the
officially supported entry point and won't break across versions.
Verifies the board mutation guard blocks requests when
X-Forwarded-Host is present but Origin does not match it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- probe host support on every platform instead of special-casing darwin
- re-export the db package helper from server and cli tests
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add @deprecated JSDoc and inline comments to bootstrapPromptTemplate
references in agent-instructions and company-portability services.
This field is superseded by the managed instructions bundle system.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
New CEO agents created during onboarding now include explicit delegation
rules: triage tasks, route to CTO/CMO/UXDesigner, never do IC work, and
follow up on delegated work.
When CURSOR_API_KEY is not set, check ~/.cursor/cli-config.json for
authInfo from `agent login` before emitting the missing key warning.
Users authenticated via native login no longer see a false warning.
The previous documentation parenthetical "(defaulting to ~/.codex/skills/)"
was misleading because Paperclip almost always sets CODEX_HOME to a
per-company managed home. Update index.ts docs, skills.ts detail string,
and execute.ts inline comment to make the runtime path unambiguous.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The previous commit incorrectly used resolveSharedCodexHomeDir() (~/.codex)
but Codex runs with CODEX_HOME set to a per-company managed home under
~/.paperclip/instances/. Skills injected into ~/.codex/skills/ would not
be discoverable by Codex. Now uses effectiveCodexHome directly.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The Codex adapter was the only one injecting skills into
<cwd>/.agents/skills/, polluting the project's git repo. All other
adapters (Gemini, Cursor, etc.) use a home-based directory. This
changes the Codex adapter to inject into ~/.codex/skills/ (resolved
via resolveSharedCodexHomeDir) to match the established pattern.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Behind a reverse proxy with a custom port (e.g. Caddy on :3443), the
browser sends an Origin header that includes the port, but the board
mutation guard only read the Host header which often omits the port.
This caused a 403 "Board mutation requires trusted browser origin"
for self-hosted deployments behind reverse proxies.
Read x-forwarded-host (first value, comma-split) with the same pattern
already used in private-hostname-guard.ts and routes/access.ts.
Fixes#1734
Consolidate the env stub into beforeEach so the pre-existing cwd
test is also isolated from host OPENAI_API_KEY, avoiding
non-deterministic filesystem side effects.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use vi.stubEnv to clear OPENAI_API_KEY in both new tests so they
don't silently pass the wrong branch when the key is set in the
test runner's environment.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add tests for codex_native_auth_present and codex_openai_api_key_missing
code paths. Also pass adapter-configured CODEX_HOME through to
readCodexAuthInfo so the probe respects per-adapter home directories.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The embedded-postgres library hardcodes --lc-messages=en_US.UTF-8 and
strips the parent process environment when spawning initdb/postgres.
In slim Docker images (e.g. node:20-bookworm-slim), the en_US.UTF-8
locale isn't installed, causing initdb to exit with code 1.
Two fixes applied:
1. Add --lc-messages=C to all initdbFlags arrays (overrides the
library's hardcoded locale since our flags come after in the spread)
2. pnpm patch on embedded-postgres to preserve process.env in spawn
calls, preventing loss of PATH, LD_LIBRARY_PATH, and other vars
Co-Authored-By: Paperclip <noreply@paperclip.ing>