Compare commits

...

1891 Commits

Author SHA1 Message Date
Dotta
a5aed931ab fix(dev-runner): tighten worktree env bootstrap 2026-04-11 08:35:53 -05:00
Dotta
b6115424b1 fix: isolate dev runner worktree env 2026-04-11 08:27:25 -05:00
Dotta
03a2cf5c8a Merge pull request #3303 from cryppadotta/PAP-438-review-openclaw-s-docs-on-networking-discovery-and-binding-what-could-we-learn-from-this
Introduce bind presets for deployment setup
2026-04-11 07:24:57 -05:00
Dotta
a77206812e Harden tailnet bind setup 2026-04-11 07:13:41 -05:00
dotta
6208899d0a Fix dev runner workspace import regression 2026-04-11 07:09:07 -05:00
dotta
2a84e53c1b Introduce bind presets for deployment setup
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-11 07:09:07 -05:00
Dotta
e1bf9d66a7 Merge pull request #3355 from cryppadotta/pap-1331-issue-thread-ux
feat: polish issue thread markdown and references
2026-04-11 06:55:26 -05:00
Dotta
b48be80d5d fix: address PR 3355 review regressions 2026-04-11 06:40:37 -05:00
Dotta
45ebecab5a Merge pull request #3356 from cryppadotta/pap-1331-inbox-ux
feat: polish inbox and issue list workflows
2026-04-11 06:35:59 -05:00
Dotta
dae888cc5d Merge pull request #3354 from cryppadotta/pap-1331-runtime-workflows
fix: harden heartbeat and adapter runtime workflows
2026-04-11 06:31:28 -05:00
Dotta
aaf42f3a7e Merge pull request #3353 from cryppadotta/pap-1331-dev-tools-docs
chore: improve worktree tooling and security docs
2026-04-11 06:28:49 -05:00
Dotta
62d05a7ae2 Merge pull request #3232 from officialasishkumar/fix/clear-empty-agent-env-bindings
fix(ui): persist cleared agent env bindings on save
2026-04-11 06:23:14 -05:00
Dotta
1cd0281b4d test(ui): fix heartbeat run fixture drift 2026-04-10 22:42:52 -05:00
Dotta
65480ffab1 fix: restore inbox optimistic run fixture 2026-04-10 22:40:49 -05:00
Dotta
dc94e3d1df fix: keep thread polish independent of quicklook routing 2026-04-10 22:36:45 -05:00
Dotta
0162bb332c fix: keep runtime UI changes self-contained 2026-04-10 22:36:45 -05:00
Dotta
7ec8716159 fix: keep inbox quicklook and tests standalone 2026-04-10 22:36:45 -05:00
Dotta
8cb70d897d fix: use CLI tsx entrypoint for workspace preflight 2026-04-10 22:32:55 -05:00
Dotta
8bdf4081ee chore: improve worktree tooling and security docs 2026-04-10 22:26:30 -05:00
Dotta
958c11699e feat: polish issue thread markdown and references 2026-04-10 22:26:21 -05:00
Dotta
c566a9236c fix: harden heartbeat and adapter runtime workflows 2026-04-10 22:26:21 -05:00
Dotta
dab95740be feat: polish inbox and issue list workflows 2026-04-10 22:26:21 -05:00
Devin Foley
548721248e fix(ui): keep latest issue document revision current (#3342)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - Board users and agents collaborate on issue-scoped documents such as
plans and revisions need to be trustworthy because they are the audit
trail for those artifacts.
> - The issue document UI now supports revision history and restore, so
the UI has to distinguish the current revision from historical revisions
correctly even while multiple queries are refreshing.
> - In `PAPA-72`, the newest content could appear under an older
revision label because the current document snapshot and the
revision-history query could temporarily disagree after an edit.
> - That made the UI treat the newest revision like a historical restore
target, which is the opposite of the intended behavior.
> - This pull request derives one authoritative revision view from both
sources, sorts revisions newest-first, and keeps the freshest revision
marked current.
> - The benefit is that revision history stays stable and trustworthy
immediately after edits instead of briefly presenting the newest content
as an older revision.

## What Changed

- Added a `document-revisions` helper that merges the current document
snapshot with fetched revision history into one normalized revision
state.
- Updated `IssueDocumentsSection` to render from that normalized state
instead of trusting either query in isolation.
- Added focused tests covering the current-revision selection and
ordering behavior.

## Verification

- `pnpm -r typecheck`
- `pnpm build`
- Targeted revision tests passed locally.
- Manual reviewer check:
  - Open an issue document with revision history.
  - Edit and save the document.
  - Immediately open the revision selector.
- Confirm the newest revision remains marked current and older revisions
remain the restore targets.

## Risks

- Low risk. The change is isolated to issue document revision
presentation in the UI.
- Main risk is merging the current snapshot with fetched history
incorrectly for edge cases, which is why the helper has focused unit
coverage.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-10 17:14:06 -07:00
Devin Foley
f4a05dc35c fix(cli): prepare plugin sdk before cli dev boot (#3343)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The company import/export e2e exercises the local CLI startup path
that boots the dev server inside a workspace
> - That startup path loads server and plugin code which depends on
built workspace package artifacts such as `@paperclipai/shared` and
`@paperclipai/plugin-sdk`
> - In a clean worktree those `dist/*` artifacts may not exist yet even
though `paperclipai run` can still attempt to import the local server
entry
> - That mismatch caused the import/export e2e to fail before the actual
company package flow ran
> - This pull request adds a CLI preflight step that prepares the needed
workspace build dependencies before the local server import and fails
closed if that preflight is interrupted or stalls
> - The benefit is that clean worktrees can boot `paperclipai run`
reliably without silently continuing after incomplete dependency
preparation

## What Changed

- Updated `cli/src/commands/run.ts` to execute
`scripts/ensure-plugin-build-deps.mjs` before importing
`server/src/index.ts` for local dev startup.
- Ensured `paperclipai run` can materialize missing workspace artifacts
such as `packages/shared/dist` and `packages/plugins/sdk/dist`
automatically in clean worktrees.
- Made the preflight fail closed when the child process exits via signal
and bounded it with a 120-second timeout so the CLI does not hang
indefinitely.
- Kept the fix isolated to the CLI startup path; no API contract,
schema, or UI behavior changed.
- Reused the existing
`cli/src/__tests__/company-import-export-e2e.test.ts` coverage that
already exercises the failing boot path, so no additional test file was
needed.

## Verification

- `pnpm test:run cli/src/__tests__/company-import-export-e2e.test.ts`
- `pnpm --filter paperclipai typecheck`
- On the isolated branch, confirmed `packages/shared/dist/index.js` and
`packages/plugins/sdk/dist/index.js` were absent before the run, then
reran the targeted e2e and observed a passing result.

## Risks

- Low risk: the change only affects the local CLI dev startup path
before the server import.
- Residual risk: other entrypoints still rely on their own
preflight/build behavior, so this does not normalize every workspace
startup path.
- The 120-second timeout is intentionally generous, but unusually slow
machines could still hit it and surface a startup error instead of
waiting forever.

## Model Used

- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment,
with shell/tool execution enabled. The exact runtime revision and
context window are 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 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
2026-04-10 17:01:06 -07:00
Dotta
b00d52c5b6 Merge pull request #3015 from aronprins/feature/backups-configuration
feat(backups): gzip compression and tiered retention with UI controls
2026-04-10 11:56:12 -05:00
Dotta
ac664df8e4 fix(authz): scope import, approvals, activity, and heartbeat routes (#3315)
## Thinking Path

> - Paperclip orchestrates AI agents and company-scoped control-plane
actions for zero-human companies.
> - This change touches the server authz boundary around company
portability, approvals, activity, and heartbeat-run operations.
> - The vulnerability was that board-authenticated callers could cross
company boundaries or create new companies through import paths without
the same authorization checks enforced elsewhere.
> - Once that gap existed, an attacker could chain it into higher-impact
behavior through agent execution paths.
> - The fix needed to harden every confirmed authorization gap in the
reported chain, not just the first route that exposed it.
> - This pull request adds the missing instance-admin and company-access
checks and adds regression tests for each affected route.
> - The benefit is that cross-company actions and new-company import
flows now follow the same control-plane authorization rules as the rest
of the product.

## What Changed

- Required instance-admin access for `new_company` import preview/apply
flows in `server/src/routes/companies.ts`.
- Required company access before approval decision routes in
`server/src/routes/approvals.ts`.
- Required company access for activity creation and heartbeat-run issue
listing in `server/src/routes/activity.ts`.
- Required company access before heartbeat cancellation in
`server/src/routes/agents.ts`.
- Added regression coverage in the corresponding server route tests.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/company-portability-routes.test.ts
src/__tests__/approval-routes-idempotency.test.ts
src/__tests__/activity-routes.test.ts
src/__tests__/agent-permissions-routes.test.ts`
- `pnpm --filter @paperclipai/server typecheck`
- Prior verification on the original security patch branch also included
`pnpm build`.

## Risks

- Low code risk: the change is narrow and only adds missing
authorization gates to existing routes.
- Operational risk: the advisory is already public, so this PR should be
merged quickly to minimize the public unpatched window.
- Residual product risk remains around open signup / bootstrap defaults,
which was intentionally left out of this patch because the current
first-user onboarding flow depends on it.

## Model Used

- OpenAI GPT-5 Codex coding agent with tool use and local code execution
in the Codex CLI 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Forgotten <forgottenrunes@protonmail.com>
2026-04-10 11:55:27 -05:00
Dotta
4477ca2a7e Merge pull request #3299 from aronprins/codex/fix-ceo-instruction-relative-paths
[codex] Clarify Claude instruction sibling file base path
2026-04-10 11:54:46 -05:00
Aron Prins
724893ad5b fix claude instruction sibling path hint 2026-04-10 14:22:48 +02:00
Aron Prins
7c42345177 chore: re-trigger CI to refresh PR base SHA 2026-04-10 12:16:25 +02:00
Dotta
0e87fdbe35 Merge pull request #3222 from paperclipai/pap-1266-issue-workflow
feat(issue-ui): refine issue workflow surfaces and live updates
2026-04-09 14:52:16 -05:00
dotta
4077ccd343 Fix signoff stage access and comment wake retries 2026-04-09 14:48:12 -05:00
Asish Kumar
44d94d0add fix(ui): persist cleared agent env bindings on save
Agent configuration edits already had an API path for replacing the full adapterConfig, but the edit form was still sending merge-style patches. That meant clearing the last environment variable serialized as undefined, the key disappeared from JSON, and the server merged the old env bindings back into the saved config.

Build adapter config save payloads as full replacement patches, strip undefined keys before send, and reuse the existing replaceAdapterConfig contract so explicit clears persist correctly. Add regression coverage for the cleared-env case and for adapter-type changes that still need to preserve adapter-agnostic fields.

Fixes #3179
2026-04-09 17:50:14 +00:00
Dotta
6d63a4df45 Merge pull request #3220 from paperclipai/pap-1266-routines
feat(routines): support draft routines and run-time overrides
2026-04-09 10:47:03 -05:00
dotta
3cee1f12da test(ui): wait for workspace selector in new issue dialog test 2026-04-09 10:38:21 -05:00
dotta
03dff1a29a Refine issue workflow surfaces and live updates 2026-04-09 10:26:17 -05:00
dotta
5d021583be Add draft routine defaults and run-time overrides 2026-04-09 10:19:52 -05:00
Dotta
b4a58ba8a6 Merge pull request #3206 from cryppadotta/pap-1239-server-test-isolation
test(server): isolate route modules in endpoint tests
2026-04-09 09:49:37 -05:00
dotta
da251e5eab Merge public/master into pap-1239-server-test-isolation 2026-04-09 09:40:44 -05:00
Dotta
264eb34f24 Merge pull request #3205 from cryppadotta/pap-1239-ui-ux
feat(ui): improve issue detail and inbox workflows
2026-04-09 09:13:51 -05:00
Dotta
0191fabdc6 Merge pull request #3203 from cryppadotta/pap-1239-tooling-docs
chore(dev): refresh worktree tooling and contributor docs
2026-04-09 09:11:52 -05:00
dotta
b578bf1f51 Merge public-gh/master into pap-1239-ui-ux 2026-04-09 09:04:22 -05:00
Dotta
781d9dcf74 Merge pull request #3204 from cryppadotta/pap-1239-runtime-backend
feat(runtime): add issue approvals and execution workflow fixes
2026-04-09 08:55:55 -05:00
dotta
c7bf2661c9 Remove workspace link package preflight hooks
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 08:35:41 -05:00
dotta
d607ca0089 Scope workspace link preflight to linked worktrees
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 08:19:19 -05:00
dotta
61ed4ef90c fix(server): reject non-participant stage mutations 2026-04-09 07:29:56 -05:00
dotta
ce3bc329fc test(ui): align inbox badge fixture with dismissal state 2026-04-09 07:07:16 -05:00
dotta
11c3eee66b test(server): align isolated route specs with current behavior 2026-04-09 07:07:08 -05:00
dotta
0ed3f56935 fix(ci): run workspace preflight through server toolchain 2026-04-09 07:07:08 -05:00
dotta
1ac1dbcb3e fix(ui): repair issue detail split regressions 2026-04-09 07:07:08 -05:00
dotta
fe21ab324b test(server): isolate route modules in endpoint tests 2026-04-09 06:25:41 -05:00
dotta
327eadb45c fix(ui): harden issue comment editor sync 2026-04-09 06:23:58 -05:00
dotta
996c7eb727 Disable inbox nesting on mobile 2026-04-09 06:23:58 -05:00
dotta
9e8cd28f81 Speed up issue detail comments and refreshes 2026-04-09 06:23:58 -05:00
dotta
a4b05d8831 Guard issue chat against assistant-ui crashes 2026-04-09 06:21:14 -05:00
dotta
de1cd5858d Add explicit review start action in issue sidebar 2026-04-09 06:21:14 -05:00
dotta
efc1e336b0 Improve issue detail load stability 2026-04-09 06:21:14 -05:00
dotta
d82468d6e5 Keep interrupted runs stable in issue chat 2026-04-09 06:18:27 -05:00
dotta
2ebbad6561 Add breathing room when focusing comment composer
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:27 -05:00
dotta
f44c951a22 Make issue chat composer inline again 2026-04-09 06:18:27 -05:00
dotta
e15b5412ec Prevent g c from leaking into global shortcuts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:27 -05:00
dotta
ba5cb34bed Tighten issue chat composer height cap 2026-04-09 06:18:27 -05:00
dotta
cbc237311f Fix interrupted issue chat rerender 2026-04-09 06:18:27 -05:00
dotta
1079f21ac4 Add issue detail shortcut for comment composer
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:27 -05:00
dotta
15b0f11275 Keep issue chat composer visible while typing 2026-04-09 06:18:27 -05:00
dotta
59d913d04b Fix issue detail main-pane focus on navigation 2026-04-09 06:18:27 -05:00
dotta
e21e442033 Fix issue detail inbox archive shortcut 2026-04-09 06:18:27 -05:00
dotta
296033620f Remove main-content focus outline
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:27 -05:00
dotta
fad5634b29 feat(ui): add keyboard shortcut cheatsheet dialog on ? keypress
Shows a beautiful categorized cheatsheet of all keyboard shortcuts
(inbox, issue detail, global) when the user presses ? with keyboard
shortcuts enabled. Respects text input focus detection — won't trigger
in text fields. Uses the existing Dialog component and Radix UI.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:27 -05:00
dotta
69ff793c6a Add issue-detail g i inbox shortcut
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:27 -05:00
dotta
ede3206423 fix(ui): always render transcript message for non-succeeded runs
Use createHistoricalTranscriptMessage for failed/cancelled/timed_out
runs even before transcript data loads. This prevents the flash where
a plain "run X failed" status line transforms into a foldable "failed
after X minutes" header when transcripts arrive asynchronously.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:27 -05:00
dotta
2cf2a44d68 fix(ui): fix inbox nesting column alignment
Move child indentation from wrapper paddingLeft into desktopMetaLeading
so the unread dot stays in the leftmost column regardless of nesting
depth. When nesting is enabled, all issue rows get a fixed-width folding
column (chevron or empty spacer) for consistent alignment. Children
indent after the folding column. When nesting is disabled, no folding
column is rendered.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:26 -05:00
dotta
58ae23aa2c fix(ui): make j/k keyboard shortcuts traverse nested child issues in inbox
Builds a flat navigation list that includes expanded child issues alongside
top-level items, so j/k moves through every visible row including children.
Also adds the NavEntry type and updates archive/read/enter actions to work
with both top-level work items and nested child issues.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:26 -05:00
dotta
d3e66c789e feat(ui): add toggle button for inbox parent-child nesting
Adds a ListTree icon button in the inbox top bar to toggle nesting
on/off. Preference is persisted in localStorage. When disabled, all
issues display as a flat list without grouping.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:26 -05:00
dotta
097f30b138 feat(ui): nest parent-child issues in inbox/mine view
Groups child issues under their parent in the inbox, matching the
nesting pattern used on the issues list page. Parent groups sort by
the most recent activity across all family members. Archived parents
don't hide their children — orphans show independently.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:26 -05:00
dotta
c5ccafbb80 fix(ui): show shimmer and icon on initial Working... state for new agent runs
Eliminates two visual glitches when a new agent run starts:
1. The initial "Working..." was rendered as plain text without the shimmer
   animation or agent icon — now matches the proper working state styling.
2. A brief blank flash occurred when transcript chunks arrived but hadn't
   produced parseable parts yet — fixed by deriving waitingText from parts
   availability instead of the hasOutput flag.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:26 -05:00
dotta
28a28d1cb6 fix(ui): eliminate flash when auto-folding work sections on page load
Replace useEffect with synchronous state derivation during render so
the browser never paints the unfolded intermediate state. This prevents
the visible "jump" when loading an issue page with already-completed
work sections like "worked for 4 minutes".

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:26 -05:00
dotta
7edd2f7946 fix(ui): add pause between shimmer animation repeats
The sweep now completes at 60% of the cycle and holds for the
remaining 40%, giving a ~1s pause before repeating. Total cycle
duration increased from 1.5s to 2.5s.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:26 -05:00
dotta
1a82646e9d fix(ui): fix shimmer animation loop jitter by keeping gradient in view
The shimmer keyframes animated from -100% to 200%, pushing the gradient
fully off-screen at both extremes. On loop restart the text flashed
invisible for a frame. Changed to 100%→0% so the gradient always
overlaps the visible area — both endpoints show solid base-color text
and the highlight sweeps smoothly through without discontinuity.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:26 -05:00
dotta
f75c0c317c fix(ui): move useCallback hook before early returns in IssueDetail
The handleChatImageClick useCallback (and its dependencies) was defined
after conditional early returns, violating React's rules of hooks and
causing "Rendered more hooks than during the previous render" crashes.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:26 -05:00
dotta
d0920da459 feat(ui): open gallery when clicking images in chat messages
Clicking an image in a chat message now opens the same ImageGalleryModal
used by the attachments gallery. Matches by contentPath or assetId.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:26 -05:00
dotta
c830c64727 fix(ui): fix shimmer text using invalid hsl() wrapper on oklch colors
--foreground is defined as oklch(), not HSL channels. Wrapping it in
hsl() produced an invalid color, making the shimmer text invisible.
Use var(--foreground) directly and color-mix() for the highlight band.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:26 -05:00
dotta
2ec2b1f1eb fix(ui): improve shimmer gradient and add Working/Worked tokens to Chat UX Lab
Rework the shimmer-text CSS to use a Cursor-style sweep: full-opacity base
with a narrow transparent highlight band that slides across, linear timing,
tighter background-size. Add a "Status tokens" section to the Issue Chat UX
Lab showing both the active shimmer "Working" state and the static "Worked"
state side-by-side for quick visual review.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:26 -05:00
dotta
9159b44fcc fix(ui): update inbox badge test inputs 2026-04-09 06:18:26 -05:00
dotta
f5a87ab14e fix(ui): avoid issue detail ref update loops 2026-04-09 06:18:05 -05:00
dotta
185195201a Adjust execution workspace header layout 2026-04-09 06:18:05 -05:00
dotta
1e4ccb2b1f Improve mobile comment copy button feedback 2026-04-09 06:18:05 -05:00
dotta
038dd2bb82 Improve issue assignee update responsiveness 2026-04-09 06:18:05 -05:00
dotta
bac5afa647 Remove "None" text from empty Blocking and Sub-issues property rows
When there are no blocking issues or sub-issues, show nothing (or just the
"+ Add sub-issue" button) instead of displaying "None" text.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:05 -05:00
dotta
db1279dc12 Make worktree banner name clickable to copy to clipboard
Clicking the worktree name in the banner now copies it to clipboard
and shows "Copied\!" feedback for 1.5 seconds.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:05 -05:00
dotta
30dd2b78e5 Polish shared issue columns trigger styling 2026-04-09 06:18:05 -05:00
dotta
ee82a4f243 Reuse inbox issue column controls in issues lists 2026-04-09 06:18:05 -05:00
dotta
1cbb0a5e34 Add execution workspace issues tab 2026-04-09 06:18:05 -05:00
dotta
93355bae6b Debounce issues search input 2026-04-09 06:18:05 -05:00
dotta
c6779b570f feat(ui): add workspace and parent issue grouping to issues list
Adds two new groupBy options on the issues page: "Workspace" groups
issues by their projectWorkspaceId, and "Parent Issue" groups by
parentId. Groups with items sort first; sentinel groups (No Workspace /
No Parent) appear last. Creating a new issue from a parent group
pre-fills parentId.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:18:05 -05:00
dotta
27ec1e0c8b Fix execution policy edits on in-review issues 2026-04-09 06:16:41 -05:00
dotta
26d4cabb2e Persist heartbeat child pid before stdin handoff 2026-04-09 06:16:41 -05:00
dotta
3baebee2df Track blocker and review activity events 2026-04-09 06:16:41 -05:00
dotta
8894520ed0 comment wake batching test 2026-04-09 06:16:05 -05:00
dotta
ec75cabcd8 Enforce execution-policy stage handoffs 2026-04-09 06:16:05 -05:00
dotta
9eaf72ab31 Fix Codex tool-use transcript completion 2026-04-09 06:16:05 -05:00
dotta
844b061267 Disable timer heartbeats by default for new agents 2026-04-09 06:16:05 -05:00
dotta
5640d29ab0 Persist non-issue inbox dismissals 2026-04-09 06:16:05 -05:00
dotta
1de5fb9316 Support routine variables in titles 2026-04-09 06:16:05 -05:00
dotta
372421ef0b Add generic issue-linked board approvals
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:16:05 -05:00
dotta
8e88577371 chore(dev): preflight workspace links and simplify worktree helpers 2026-04-09 06:14:12 -05:00
dotta
b1e9215375 docs: add browser process cleanup plan 2026-04-09 06:14:12 -05:00
dotta
0d270655ab Clarify repo plan docs vs issue plan documents 2026-04-09 06:14:12 -05:00
dotta
5758aba91e docs: add agent-os follow-up plan 2026-04-09 06:14:12 -05:00
dotta
482dac7097 docs: add agent-os technical report 2026-04-09 06:14:12 -05:00
dotta
0937f07c79 Remove standalone issue recovery plan doc 2026-04-09 06:14:12 -05:00
dotta
d00860b12a Add in-progress issue recovery plan 2026-04-09 06:14:12 -05:00
dotta
4e20279305 fix(skill): add scoped-wake fast path to skip full heartbeat on comment wakes
When an agent is woken by a comment (Resume Delta or Wake Payload), the
skill now explicitly instructs it to skip Steps 1-4 (identity, approvals,
inbox, pick work) and go directly to checkout. This prevents agents from
wastefully fetching their full assignment list and announcing "checking my
paperclip tasks" when they already know exactly which issue to work on.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:14:12 -05:00
dotta
4bd62471f7 kill chrome test servers too 2026-04-09 06:14:12 -05:00
dotta
56ee63bfd0 docs: add issue detail speed inventory plan 2026-04-09 06:14:12 -05:00
dotta
87db949d3f docs: survey pi and pi-mono hook surfaces 2026-04-09 06:14:12 -05:00
dotta
735c591bad docs: add manual mcp-server publish steps
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:14:12 -05:00
dotta
46892ded18 Add worktree reseed command
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 06:14:12 -05:00
dotta
9f9a8cfa25 skills: add prcheckloop CI remediation loop 2026-04-09 06:14:12 -05:00
Devin Foley
3264f9c1f6 Fix typing lag in long comment threads (PAPA-63) (#3163)
## 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>
2026-04-08 17:54:03 -07:00
Dotta
642188f900 Merge pull request #3124 from cleanunicorn/fix/better-auth-jwt-secret
fix: remove hardcoded JWT secret fallback from auth init
2026-04-08 11:12:31 -05:00
Daniel Luca
b7a7dacfa3 fix: remove hardcoded JWT secret fallback from createBetterAuthInstance 2026-04-08 17:51:21 +03:00
Aron Prins
b1e457365b fix: clean up orphaned .sql on compression failure and fix stale startup log
- 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
2026-04-08 14:40:05 +02:00
Aron Prins
fcbae62baf feat(backups): tiered daily/weekly/monthly retention with UI controls
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.
2026-04-08 14:40:05 +02:00
Aron Prins
cc44d309c0 feat(backups): gzip compress backups and add retention config to Instance Settings
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.
2026-04-08 14:40:05 +02:00
github-actions[bot]
316790ea0a chore(lockfile): refresh pnpm-lock.yaml (#3109)
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>
2026-04-08 07:07:49 -05:00
Dotta
8367c5f406 Merge pull request #2949 from Lempkey/fix/skip-system-prompt-on-resume
fix: skip --append-system-prompt-file on resumed claude sessions
2026-04-08 07:06:58 -05:00
dotta
0ff262ca0f fix: preserve claude instructions on resume fallback 2026-04-08 06:57:21 -05:00
lempkey
fa3cbc7fdb chore: trigger Greptile re-review 2026-04-08 06:57:21 -05:00
lempkey
e3804f792d fix: gate instructions file I/O and commandNotes on fresh sessions only
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.
2026-04-08 06:57:21 -05:00
lempkey
3cfbc350a0 fix: skip --append-system-prompt-file on resumed claude sessions
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
2026-04-08 06:57:21 -05:00
Dotta
667d5a7384 Merge pull request #3079 from paperclipai/PAP-1132-assistant-ui-pap-1131-make-issues-comments-be-like-a-chat
Polish issue chat UX and add worktree reseed
2026-04-08 06:10:04 -05:00
dotta
950ea065ae Reuse chat-style run feed on dashboard
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
b5e177df7e Address greptile review feedback 2026-04-08 06:02:34 -05:00
dotta
81b96c6021 Update transcript message expectations 2026-04-08 06:02:34 -05:00
dotta
fe96a2f976 Fix rebased issue detail chat props 2026-04-08 06:02:34 -05:00
dotta
92f142f7f8 Polish issue chat transcript presentation 2026-04-08 06:02:34 -05:00
dotta
34589ad457 Add worktree reseed command 2026-04-08 06:02:34 -05:00
dotta
7dd3661467 Tweak issue chat run action
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
e09dfb1a2c Reorder action bar icons and add relative date formatting
Action bar for agent messages is now: [copy] [thumbs up] [thumbs down] [date] [three dots].
Date shows relative time (e.g. "2h ago") if < 1 week old, otherwise short date (e.g. "Apr 6").
Hovering the date shows full timestamp tooltip. Date links to the comment anchor.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
a57f6f48b4 Move date/menu to action bar and fix activity label sizing
- Agent comment header now only shows name (+ running badge)
- Date, copy, thumbs up/down, and three-dots menu all in action bar
- Activity event STATUS/ASSIGNEE labels changed from text-[9px] to
  text-xs to match value font size

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
29514606bb Refactor user message avatar to flex layout
Replace absolute positioning (-right-8) with a flex row layout for the
"You" avatar. The avatar now sits naturally to the right of the bubble
via flex justify-end + gap-2.5, avoiding overflow clipping issues.
Max-width 85% is on the content column, not the bubble div, so the
bubble + avatar together fill the row naturally.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
627fbc80ac Polish issue chat chain-of-thought rendering
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
2a372fbe8a Refine issue chat chain-of-thought mapping 2026-04-08 06:02:34 -05:00
dotta
d8a7342686 Fix avatar positioning and activity line alignment in chat
- Move "You" avatar outside content column using -right-8 negative
  positioning instead of right-0 inside pr-8 padding
- Remove pr-8 padding from user message container so bubble touches
  the column edge
- Align activity event and run timeline avatars/gaps with chat messages
  (sm size + gap-2.5 instead of xs + gap-2)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
3574a3bf49 Move user avatar outside content column to right margin
Use relative positioning with pr-8 reserved space and absolute
positioning for the avatar, so it sits outside the content column
boundary while the bubble's right edge aligns with the column edge.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
f94fe57d10 Polish issue chat actions and overflow
- Scale activity components (events, runs) to ~80% font size with
  xs avatars for a quieter visual weight
- Hide succeeded runs from the timeline; only show failed/errored
- Always show three-dots menu on agent comments with "Copy message"
  option, plus optional "View run" when available
- User avatar repositioned to top-right (items-start) of message
- Change "Me" → "You" in assignee labels for natural chat phrasing
  ("You updated this task")

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
94652c6079 Fix chat comment alignment, avatars, and layout polish
- Agent messages: avatar outside left (matching feed items alignment),
  always shown, consistently uses icon avatar instead of initials
- User messages: avatar outside right, action bar moved below the
  gray bubble, gray darkened to bg-muted
- System events: right-aligned when actor is the current user
- Run messages: use agent icon avatar consistently
- Pass actorType/actorId in event metadata for current-user detection

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
9131cc0355 Restyle issue chat comments for chat-like UX
User messages: right-aligned bubbles (85% max-width) with gray
background, no border. Hover reveals short date + copy icon.

Agent messages: borderless with avatar, name, date and three-dots
in header. Left-aligned action bar with icon-only copy, thumbs up,
and thumbs down. Thumbs down opens a floating popover for reason.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
f7410673fe Fix needs-work feedback panel closing immediately
The IssueChatCtx.Provider was opened but never closed, causing the
context to not properly wrap the thread. This, combined with the
stable component references via context (already in place), ensures
OutputFeedbackButtons state is preserved across re-renders when
feedback votes update.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
4a75d05969 Remove border and padding from chat thread outer container
Strip the rounded border, padding, background gradient, and shadow
from ThreadPrimitive.Root so the chat thread flows naturally without
a bordered wrapper container.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
8ada49f31b Polish issue chat actions and overflow
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
f593e116c1 Refine issue chat activity and message chrome
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
3fea60c04c Polish issue chat layout and add UX lab
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
dotta
73abe4c76e Implement assistant-ui issue chat thread
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00
Dotta
9cfa37fce3 Merge pull request #1961 from antonio-mello-ai/fix/webhook-github-sentry-signing-modes
feat(server): add github_hmac and none webhook signing modes
2026-04-07 22:58:14 -05:00
Dotta
943b851a5e Merge pull request #2643 from chrisschwer/fix/stale-execution-lock-lifecycle
fix: stale execution lock lifecycle (PIP-002)
2026-04-07 22:55:53 -05:00
Dotta
f2a2049d17 Merge pull request #2442 from sparkeros/fix/capabilities-field-blank-screen
fix: prevent blank screen when clearing Capabilities field
2026-04-07 22:52:25 -05:00
Dotta
54f93c1f27 Merge pull request #2441 from DanielSousa/skill-removal-ui
feat(company-skills): implement skill deletion (UI) with agent usage check
2026-04-07 21:51:51 -05:00
Dotta
f55a5e557d Merge pull request #2866 from ergonaworks/fix/agent-auth-jwt-better-auth-secret-fallback
fix(agent-auth): fall back to BETTER_AUTH_SECRET when PAPERCLIP_AGENT_JWT_SECRET is absent
2026-04-07 21:49:28 -05:00
Dotta
50a36beec5 Merge pull request #3033 from kimnamu/feat/bedrock-model-selection
fix(claude-local): respect model selection for Bedrock users
2026-04-07 21:48:29 -05:00
Dotta
f559455d92 Merge pull request #2512 from AllenHyang/fix/inbox-badge-counts-all-mine-issues-not-unread
fix(ui): inbox badge should only count unread mine issues
2026-04-07 21:00:26 -05:00
Dotta
5ae335c42f Merge pull request #2148 from shoaib050326/codex/issue-2110-goal-show-properties
fix: restore goal view properties toggle
2026-04-07 20:58:48 -05:00
Dotta
a13ac0d56f Merge pull request #3039 from paperclipai/PAP-1139-consider-a-signoff-required-execution-policy
Add execution policy review and approval gates
2026-04-07 18:41:51 -05:00
dotta
b0b85e6ba3 Stabilize onboarding e2e cleanup paths 2026-04-07 18:20:35 -05:00
dotta
cb705c9856 Fix signoff PR follow-up tests 2026-04-07 17:56:39 -05:00
dotta
bce58d353d fix execution policy decision persistence 2026-04-07 17:43:10 -05:00
dotta
a0333f3e9d Stabilize heartbeat comment batching assertion 2026-04-07 17:43:10 -05:00
dotta
25d308186d Generate execution policy migration 2026-04-07 17:43:10 -05:00
dotta
0e80e60665 Document execution policy workflows 2026-04-07 17:43:10 -05:00
dotta
0a5ac9affd Clarify execution-policy reviewer guidance
Add explicit Paperclip skill guidance for reviewer/approver heartbeats and document that execution-policy decisions use PATCH /api/issues/:issueId rather than a separate endpoint.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-07 17:43:10 -05:00
dotta
ff333d6828 Align assignee/reviewer/approver pills vertically in new-issue dialog
Give the leading element of each row (the "For" text and the
Eye/ShieldCheck icons) a fixed w-6 width so all InlineEntitySelector
pills start at the same horizontal position.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-07 17:43:10 -05:00
dotta
e7fe02c02f Move reviewer/approver to rows under assignee with three-dot menu
- Comment out non-functional Labels chip in new-issue bottom bar
- Remove reviewer/approver mini pills from bottom chip bar
- Add three-dot menu (⋯) next to Project selector in the "For/in" row
- Clicking Reviewer or Approver in that menu toggles a full-sized
  participant selector row under Assignee, matching its styling
- Toggling off clears the selection

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-07 17:43:10 -05:00
dotta
71d93c79a5 Fix Upload button chip styling in new-issue dialog
The Upload button was missing the pill/chip styling (border, rounded-md,
padding) that all other buttons in the chip bar have. Apply the same
className pattern used by the Labels chip.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-07 17:43:10 -05:00
dotta
cb6e615186 Revert reviewer/approver pickers to sidebar, add to new-issue chip bar
Per feedback: reviewer/approver pickers were incorrectly placed in the
issue header row. This moves them back to the Properties sidebar at
regular size and adds them as small chip-style selectors in the
new-issue dialog's bottom bar (next to Upload), matching the existing
chip styling.

- Restored Reviewers/Approvers PropertyPicker rows in IssueProperties
- Removed ExecutionParticipantPicker pills from IssueDetail header
- Added Eye/ShieldCheck-icon reviewer/approver InlineEntitySelectors
  in NewIssueDialog chip bar after Upload button

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-07 17:43:10 -05:00
dotta
be518529b7 Move reviewer/approver pickers to inline header pills
Extract execution participant pickers from sidebar PropertyPicker rows into
compact pill-style Popover triggers in the issue header row, next to labels.
Creates a reusable ExecutionParticipantPicker component with matching
text-[10px] sizing. Removes the old sidebar rows and unused code.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-07 17:43:10 -05:00
dotta
2e31fb7c91 Add comprehensive e2e tests for signoff execution policy
Expands the execution policy test suite from 3 to 34 tests covering:
- Full happy path (executor → review → approval → done)
- Changes requested flow with re-submission
- Review-only and approval-only policy variants
- Access control (non-participant cannot advance stages)
- Comment requirements (empty, whitespace-only, null)
- Policy removal mid-flow with state cleanup
- Reopening done/cancelled issues clears execution state
- Multi-participant stage selection and exclusion
- User-type reviewer participants
- No-op transitions and edge cases

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-07 17:43:10 -05:00
dotta
b3e0c31239 Add issue review policy and comment retry
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-07 17:43:10 -05:00
Dotta
4b39b0cc14 Merge pull request #3063 from paperclipai/chore/refresh-lockfile
chore(lockfile): refresh pnpm-lock.yaml
2026-04-07 17:38:14 -05:00
lockfile-bot
e10baee84c chore(lockfile): refresh pnpm-lock.yaml 2026-04-07 22:35:57 +00:00
Dotta
3cd9a54d94 Merge pull request #2937 from Lempkey/fix/logger-respect-tz-env
fix: use SYS: prefix in pino-pretty so log timestamps honour TZ env var
2026-04-07 17:00:51 -05:00
Dotta
6e894f27a2 Merge pull request #2397 from HearthCore/fix/win11-opencode-cmd-shell
fix: use real cmd.exe for Windows .cmd/.bat adapter invocation
2026-04-07 16:56:36 -05:00
Dotta
93c7493054 Merge pull request #2936 from Lempkey/fix/express5-auth-wildcard-syntax
fix: use Express 5 wildcard syntax for better-auth handler route
2026-04-07 16:55:55 -05:00
Dotta
391afa627f Merge pull request #2143 from shoaib050326/codex/issue-2131-openclaw-session-key
fix(openclaw-gateway): prefix session keys with configured agent id
2026-04-07 16:53:18 -05:00
Dotta
47b025c146 Merge pull request #3009 from KhairulA/fix/keepalive-timeout
fix: increase Node keepAliveTimeout behind reverse proxies to prevent…
2026-04-07 16:52:48 -05:00
Dotta
8b7dafd218 Merge pull request #2435 from paperclipai/PAP-874-chat-speed-issues
Improve comment wake efficiency and worktree runtime isolation
2026-04-07 16:17:55 -05:00
Dotta
700b41f7e1 Merge pull request #2819 from mvanhorn/fix/2753-bump-multer-cve
fix(security): bump multer to 2.1.1 to fix HIGH CVEs
2026-04-07 16:03:38 -05:00
Dotta
7e78ce0d7e Merge pull request #2818 from mvanhorn/fix/2705-identifier-collision
fix(server): prevent identifier collision in issue creation
2026-04-07 15:41:27 -05:00
Dotta
aa18aeb1e9 Merge pull request #3062 from paperclipai/pap-1177-refresh-lockfile-pr
fix(ci): scope lockfile refresh PR lookup to repo owner
2026-04-07 15:14:21 -05:00
dotta
b6fe9ebcbc fix(ci): scope lockfile PR lookup to repo owner 2026-04-07 12:51:23 -05:00
dotta
53ffa50638 Clean up opencode rebase and stabilize runtime test
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-07 10:02:06 -05:00
dotta
ebd45b62cd Provision local node_modules in issue worktrees
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-07 10:02:05 -05:00
Dotta
26ebe3b002 Merge pull request #2662 from wbelt/fix/configurable-claimed-api-key-path
fix(openclaw-gateway): make claimedApiKeyPath configurable per agent
2026-04-07 09:31:14 -05:00
kimnamu
60744d8a91 fix: address Greptile P2 — reuse DIRECT_MODELS import, global region prefix match
- Import models from index.ts instead of duplicating the array
- Use regex ^\w+\.anthropic\. to match all Bedrock region prefixes
  (us, eu, ap, and any future regions)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:24:37 +09:00
kimnamu
3a0e71b080 Revert "chore: sync pnpm-lock.yaml with mcp-server package"
This reverts commit 1c1d006c5e.
2026-04-07 23:20:27 +09:00
kimnamu
1c1d006c5e chore: sync pnpm-lock.yaml with mcp-server package
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:19:13 +09:00
kimnamu
07987d75ad feat(claude-local): add Bedrock model selection support
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>
2026-04-07 23:16:57 +09:00
Dotta
aec88f10dd Merge pull request #2909 from marysomething99-prog/fix/KAB-158-codex-missing-rollout
fix: recognize missing-rollout Codex resume error as stale session
2026-04-07 08:41:25 -05:00
Dotta
45f18d1bee Merge pull request #3001 from paperclipai/pap-1167-mcp-server-package
Add a standalone Paperclip MCP server package
2026-04-07 07:36:46 -05:00
dotta
2329a33f32 Merge remote-tracking branch 'public-gh/master' into pap-1167-mcp-server-package
* public-gh/master: (51 commits)
  test(cli): align env input fixtures with project scope
  fix(export): strip project env values from company packages
  fix(ui): address review follow-ups
  fix(runtime): handle empty dev runner responses
  fix(ui): remove runtime-only preflight hook dependency
  test(ui): wait for async issue search results
  refactor(ui): inline document diff rendering
  test(cli): keep import preview fixtures aligned with manifest shape
  test(cli): cover project env in import preview fixtures
  fix(ui): restore attachment delete state hook order
  Speed up issue search
  Narrow parent issue and time-ago columns in inbox grid
  Add optional Parent Issue column to inbox show/hide columns
  Move sub-issues inline and remove sub-issues tab
  Display image attachments as square-cropped gallery grid
  Offset scroll-to-bottom button when properties panel is open
  Polish board approval card styling
  Default sub-issues to parent workspace
  Relax sub-issue dialog banner layout
  Improve issue approval visibility
  ...
2026-04-07 07:33:59 -05:00
dotta
74481b1d1e fix(ci): restore pr workflow from master 2026-04-07 07:33:32 -05:00
Dotta
cae7cda463 Merge pull request #3000 from paperclipai/pap-1167-app-ui-bundle
Improve issue detail workflows, approvals, and board UX
2026-04-07 07:31:16 -05:00
dotta
2c2e13eac2 merge master into pap-1167-app-ui-bundle 2026-04-07 07:10:14 -05:00
Dotta
502d60b2a8 Merge pull request #2999 from paperclipai/pap-1167-runtime-worktree-hardening
Harden worktree runtime setup and project env handling
2026-04-07 07:06:46 -05:00
dotta
f3e5c55f45 test(cli): align env input fixtures with project scope 2026-04-07 06:59:05 -05:00
dotta
448e9f2be3 revert(ci): drop pr workflow changes from mcp pr 2026-04-07 06:32:52 -05:00
dotta
48704c6586 fix(export): strip project env values from company packages 2026-04-07 06:32:52 -05:00
Khairul
e2962e6528 fix: increase Node keepAliveTimeout behind reverse proxies to prevent 502s
- 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
2026-04-07 12:56:10 +08:00
Dotta
3e0ab97b12 Merge pull request #2951 from Lempkey/fix/company-prefix-export-import-links
fix: use prefix-aware Link for export/import on Company Settings page
2026-04-06 22:45:40 -05:00
dotta
bb980bfb33 fix(ci): fetch base sha in pr jobs 2026-04-06 22:01:49 -05:00
dotta
1e4d252661 fix(ci): restore lockfile to pr base 2026-04-06 22:00:13 -05:00
dotta
ac473820a3 fix(ci): drop lockfile changes from mcp pr 2026-04-06 21:59:28 -05:00
dotta
2c8cb7f519 fix(ci): support manifest changes without lockfile 2026-04-06 21:58:29 -05:00
dotta
51414be269 fix(ui): address review follow-ups 2026-04-06 21:56:13 -05:00
dotta
1de1393413 fix(runtime): handle empty dev runner responses 2026-04-06 21:56:13 -05:00
dotta
669e5c87cc fix(mcp): tighten api request validation 2026-04-06 21:56:13 -05:00
dotta
9a150eee65 fix(ui): remove runtime-only preflight hook dependency
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:50:11 -05:00
dotta
a3ecc086d9 test(ui): wait for async issue search results
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:47:17 -05:00
dotta
85ca675311 fix(docker): include mcp server manifest in deps stage
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:43:19 -05:00
dotta
622a8e44bf refactor(ui): inline document diff rendering
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:43:19 -05:00
dotta
d71ff903e4 test(cli): keep import preview fixtures aligned with manifest shape
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:32:08 -05:00
dotta
492e49e1c0 test(cli): cover project env in import preview fixtures
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:31:58 -05:00
dotta
f1bb175584 feat(mcp): add approval creation tool
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:25:58 -05:00
dotta
4b654fc81e fix(ui): restore attachment delete state hook order
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:25:42 -05:00
dotta
5136381d8f Speed up issue search 2026-04-06 21:25:41 -05:00
dotta
0edac73a68 Narrow parent issue and time-ago columns in inbox grid
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:44 -05:00
dotta
b3b9d99519 Add optional Parent Issue column to inbox show/hide columns
Adds a "parent" column option to the inbox column toggle dropdown.
When enabled, sub-issues display the parent's identifier (e.g. PAP-123)
with the parent title as a tooltip. Uses the existing issueById lookup
map to resolve parent info without additional API calls.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:44 -05:00
dotta
c414790404 Move sub-issues inline and remove sub-issues tab
- When no sub-issues exist, show "Add sub-issue" button alongside
  "Upload attachment" and "New document" in the action row
- When sub-issues exist, show them in a dedicated section above
  Documents with "Sub-issues" header and "Add sub-issue" button
- Remove the sub-issues tab from the comments/activity tabs

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:44 -05:00
dotta
1b55474a9b Display image attachments as square-cropped gallery grid
Image attachments now render in a 4-column grid with square aspect ratio
and center-cropped thumbnails. Clicking opens the existing gallery modal.
Hover reveals a trash icon; clicking it shows an inline confirmation
overlay before deleting. Non-image attachments retain the original list
layout.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:44 -05:00
dotta
bf3fba36f2 Offset scroll-to-bottom button when properties panel is open
On desktop, the floating scroll-to-bottom button now shifts left
to stay clear of the properties panel when it's open (320px + margin).
Mobile positioning is unchanged.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:44 -05:00
dotta
dc842ff7ea Polish board approval card styling
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:44 -05:00
dotta
758219d53f Default sub-issues to parent workspace 2026-04-06 21:24:44 -05:00
dotta
2775a5652b Relax sub-issue dialog banner layout 2026-04-06 21:24:44 -05:00
dotta
bd0f56e523 Improve issue approval visibility
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:44 -05:00
dotta
977e9f3e9a Add sub-issue issue-page flows 2026-04-06 21:24:44 -05:00
dotta
365b6d9bd8 Add generic issue-linked board approvals
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:38 -05:00
dotta
6b4f3b56e4 docs: add sub-issue issue detail plan 2026-04-06 21:24:22 -05:00
dotta
c1d0c52985 fix(ui): force diff modal to 90% width past sm:max-w-lg default
The DialogContent base class applies sm:max-w-lg which was overriding
the wider max-w setting. Use \!important to ensure the modal takes up
90% of viewport width.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:22 -05:00
dotta
5d6217b70b Exclude self-comments from queued comment UI
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:22 -05:00
dotta
eda127a2b2 fix(ui): wrap workspace paths and always show copy icon in properties sidebar
Long branch/folder paths now wrap with break-all instead of truncating
and overflowing. Copy icon is always visible instead of hover-only,
since the sidebar is narrow and hover-reveal was hiding it behind overflow.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:22 -05:00
dotta
93e8e6447d fix(ui): improve diff modal layout and readability
- Make modal much wider (90vw) to show full document content
- Use monospace font in diff area for better readability
- Enable word-wrap with pre-wrap so long lines wrap cleanly
  without breaking line number gutters
- Move revision selectors into a single row with colored
  Old/New badges instead of stacked Left:/Right: labels

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:22 -05:00
dotta
13ada98e78 feat(ui): add document revision diff viewer
Add a "View diff" option to the document three-dot menu (visible when
revision > 1) that opens a modal showing side-by-side changes between
revisions using react-diff-viewer-continued. Defaults to comparing the
current revision with its predecessor, with dropdowns to select any two
revisions.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:17 -05:00
dotta
54ac2c6fe9 feat(ui): show workspace branch/folder in issue properties sidebar
Adds a new workspace section to the IssueProperties sidebar that
displays branch name and folder path (cwd) from the execution
workspace. Both values have copy-to-clipboard buttons and truncated
display with full path on hover.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:12 -05:00
dotta
962a882799 fix(ui): keep issue breadcrumb context out of the URL
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:12 -05:00
dotta
2ac1c62ab1 Fix mobile inbox layout: move search above tabs, hide column toggle
On mobile, the search input, tab selector, and "Show / hide columns" button
were all crammed into one row causing horizontal overflow. Now:
- Search appears as a full-width row above the tabs on mobile
- "Show / hide columns" button is hidden on mobile (columns are desktop-only)
- Desktop layout is unchanged

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:12 -05:00
dotta
2278d96d5a Fix execution workspace page overflow on mobile
- Add overflow-hidden to the outer container to prevent horizontal scroll
- Add min-w-0 to grid children so long monospace content in inputs
  respects container width instead of expanding it
- Truncate the workspace name heading for long names
- Add min-w-0 to the header name container

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:12 -05:00
dotta
aff56c2297 Copy inherited config as default when unchecking inherit checkbox
When unchecking the "Inherit project workspace runtime config" checkbox,
if the runtime config field is empty, automatically populate it with the
inherited config value so the user has a starting point to edit from.
Existing values are preserved and never overwritten.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:12 -05:00
dotta
612bab1eb6 Make execution workspace detail page responsive for mobile
- Reduce card padding on small screens (p-4 → p-4 sm:p-5)
- Reduce spacing between sections on mobile (space-y-4 sm:space-y-6)
- Scale heading text (text-xl sm:text-2xl)
- Truncate long description on mobile, show full on sm+
- Reduce textarea min-heights on mobile (sm: prefix for larger sizes)
- Stack linked issue cards vertically on mobile, horizontal scroll on sm+
- Remove min-width constraint on linked issue cards on mobile

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:12 -05:00
dotta
68499eb2f4 Support dropping non-image files onto markdown editor as attachments
When dragging files like .zip onto the issue description editor, non-image
files are now uploaded as attachments instead of being silently ignored.
Images continue to be handled inline by MDXEditor's image plugin.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:12 -05:00
dotta
e9c8bd4805 Allow arbitrary issue attachments
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:12 -05:00
dotta
517fe5093e Fix inbox archive flashing back after fade-out
The archive mutation was only using CSS opacity to hide items while the
network request was in flight. When the query refetch completed or the
archiving timer expired, the item could reappear. Now we optimistically
remove the item from React Query caches on mutate, snapshot previous
data for rollback on error, and sync with the server in onSettled.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:12 -05:00
dotta
bdc8e27bf4 Fix mention popup placement and spaced queries
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:24:11 -05:00
dotta
8cdba3ce18 Add standalone Paperclip MCP server package
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:23:46 -05:00
dotta
1a3aee9ee1 docs: add smart model routing plan
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:23:33 -05:00
dotta
9a8a169e95 Guard dev health JSON parsing
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:23:33 -05:00
dotta
bfa60338cc Cap dev-runner output buffering
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:23:33 -05:00
dotta
1e76bbe38c test(db): cover 0050 migration replay 2026-04-06 21:23:30 -05:00
dotta
42b326bcc6 fix(e2e): harden signoff policy tests for authenticated deployments
Address QA review feedback on the signoff e2e suite (86b24a5e):
- Use dedicated port 3199 with local_trusted mode to avoid reusing
  the dev server in authenticated mode (fixes 403 errors)
- Add proper agent authentication via API keys + heartbeat run IDs
- Fix non-participant test to actually verify access control rejection
- Add afterAll cleanup (dispose contexts, revoke keys, delete agents)
- Reviewers/approvers PATCH without checkout to preserve in_review state

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:23:30 -05:00
dotta
8f23270f35 Add project-level environment variables
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:23:30 -05:00
dotta
97d4ce41b3 test(e2e): add signoff execution policy end-to-end tests
Covers the full signoff lifecycle: executor → review → approval → done,
changes-requested bounce-back, comment-required validation, access control,
and review-only policy completion.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:23:30 -05:00
dotta
0a9a8b5a44 Limit isolated workspace memory spikes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:23:21 -05:00
dotta
37d2d5ef02 Handle empty moved symlink lists in worktree provisioning
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:23:21 -05:00
dotta
55d756f9a3 Use latest repo-managed worktree scripts on reuse
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:23:21 -05:00
dotta
7e34d6c66b Fix worktree provisioning and relinking
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:23:21 -05:00
dotta
8be6fe987b Repair stale worktree links before runtime start
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 21:23:21 -05:00
Brandon Woo
15bd2ef349 fix: recognize missing-rollout Codex resume error as stale session
The Codex CLI can return "no rollout found for thread id ..." when
resuming a heartbeat thread whose rollout has been garbage-collected.
Extend isCodexUnknownSessionError() to match this wording so the
existing single-retry path in execute.ts activates correctly.

Add parse.test.ts covering the new pattern, existing stale-session
wordings, parseCodexJsonl, and a negative case.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-07 10:45:38 +09:00
Dotta
08fea10ce1 Merge pull request #2772 from paperclipai/PAPA-46-why-did-this-issue-succeed-without-following-my-instructions
fix: enable agent re-checkout of in_review tasks on comment feedback
2026-04-06 18:57:33 -05:00
Dawid Piaskowski
b74d94ba1e Treat Pi quota exhaustion as a failed run (#2305)
## 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>
2026-04-06 14:29:41 -07:00
Nicola
8f722c5751 fix: allow to remove project description (#2338)
fixes https://github.com/paperclipai/paperclip/issues/2336

## Thinking Path

<!--
Required. Trace your reasoning from the top of the project down to this
  specific change. Start with what Paperclip is, then narrow through the
  subsystem, the problem, and why this PR exists. Use blockquote style.
  Aim for 5–8 steps. See CONTRIBUTING.md for full examples.
-->

- Paperclip allows to manage projects
- During the project creation you can optionally enter a description
- In the project overview or configuration you can edit the description
- However, you cannot remove the description
- The user should be able to remove the project description because it's
an optional property
- This pull request fixes the frontend bug that prevented the user to
remove/clear the project description

## What Changed

<!-- Bullet list of concrete changes. One bullet per logical unit. -->

- project description can be cleared in "project configuration" and
"project overview"

## Verification

<!--
  How can a reviewer confirm this works? Include test commands, manual
  steps, or both. For UI changes, include before/after screenshots.
-->

In project configuration or project overview:

- In the description field remove/clear the text

## Risks

<!--
  What could go wrong? Mention migration safety, breaking changes,
  behavioral shifts, or "Low risk" if genuinely minor.
-->

- none

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [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
2026-04-06 13:18:38 -07:00
Lucas Kim
b6e40fec54 feat: add AWS Bedrock auth support on "claude-local" (#2793)
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>
2026-04-06 13:15:18 -07:00
Antonio
a8d1c4b596 fix(server): use Buffer.length for timing-safe HMAC comparison and document header fallback
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>
2026-04-06 16:26:27 -03:00
Antonio
cd19834fab feat(server): add github_hmac and none webhook signing modes
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>
2026-04-06 16:26:27 -03:00
lempkey
9e2ccc24bb test: mock fs.mkdirSync and improve TZ test clarity, address Greptile review 2026-04-06 16:29:22 +01:00
lempkey
fc8e1d1153 test: add over-broad route guard test and address Greptile review 2026-04-06 16:28:42 +01:00
lempkey
f3ad1fc301 fix: use prefix-aware Link for export/import on Company Settings page
The Export and Import buttons in CompanySettings used plain <a href>
anchors which bypass the router's company-prefix wrapper. The links
resolved to /company/export and /company/import instead of
/:prefix/company/export, showing a 'Company not found' error.

Replace both <a href> elements with <Link to> from @/lib/router, which
calls applyCompanyPrefix under the hood and correctly resolves to
/:prefix/company/{export,import} regardless of which company is active.

Fixes: #2910
2026-04-06 16:19:41 +01:00
Dotta
eefe9f39f1 Merge pull request #2797 from paperclipai/PAP-1019-make-a-plan-for-first-class-blockers-wake-on-subtasks-done
Add first-class issue blockers and dependency wakeups
2026-04-06 09:15:22 -05:00
lempkey
8d20510b9a fix: use SYS: prefix in pino-pretty translateTime to honour TZ env var
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
2026-04-06 15:06:45 +01:00
dotta
5a252020d5 fix: drop stale child issue props after rebase
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 09:03:13 -05:00
dotta
4c01a45d2a fix: address greptile feedback for blocker dependencies
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 09:03:13 -05:00
dotta
467f3a749a Stabilize rebased route test expectations
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 09:03:13 -05:00
dotta
9499d0df97 Add blocker/dependency documentation to Paperclip skill
Document blockedByIssueIds field, issue_blockers_resolved and
issue_children_completed wake reasons, and blockedBy/blocks response
arrays in both SKILL.md and api-reference.md so agents know how to
set and use first-class issue dependencies.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 09:03:13 -05:00
dotta
dde4cc070e Add blocker relations and dependency wakeups
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 09:03:13 -05:00
lempkey
a8638619e5 fix: use Express 5 wildcard syntax for better-auth handler route
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.
2026-04-06 15:00:39 +01:00
Dotta
2f73346a64 Merge pull request #2659 from plind-dm/fix/redact-bearer-tokens-in-logs
fix(security): redact Bearer tokens from server log output
2026-04-06 08:58:34 -05:00
Dotta
785ce54e5e Merge pull request #2532 from plind-dm/fix/ceo-agents-md-relative-paths
fix(onboarding): use relative paths instead of $AGENT_HOME in CEO ins…
2026-04-06 08:57:51 -05:00
Dotta
73e7007e7c Merge pull request #2649 from plind-dm/fix/import-ceo-role-default
fix(import): read agent role from frontmatter before defaulting to "a…
2026-04-06 08:56:38 -05:00
Dotta
c5f3b8e40a Merge pull request #2542 from plind-dm/fix/heartbeat-context-attachments
fix(api): include attachment metadata in heartbeat-context response
2026-04-06 08:55:49 -05:00
Dotta
47299c511e Merge pull request #2594 from plind-dm/fix/checkout-null-assertion-crash
fix(issues): replace non-null assertions with null checks in checkout…
2026-04-06 08:55:15 -05:00
Dotta
ed97432fae Merge pull request #2654 from plind-dm/fix/kanban-collapse-empty-columns
fix(ui): collapse empty kanban columns to save horizontal space
2026-04-06 08:54:30 -05:00
Dotta
0593b9b0c5 Merge pull request #2655 from plind-dm/fix/goal-description-scroll
fix(ui): make goal description area scrollable in create dialog
2026-04-06 08:54:05 -05:00
Dotta
855d895a12 Merge pull request #2650 from plind-dm/fix/paused-agent-visual-indicator
fix(ui): dim paused agents in list and org chart views
2026-04-06 08:53:34 -05:00
Dotta
39d001c9b5 Merge pull request #2651 from plind-dm/fix/clear-extra-args-config
fix(ui): use null instead of undefined when clearing extra args
2026-04-06 08:51:43 -05:00
Dotta
89ad6767c7 Merge pull request #2733 from davison/feature/issue-management
Issue list and issue properties panel: improved UI
2026-04-06 08:10:42 -05:00
Wes Belt
c171ff901c Merge branch 'master' into fix/configurable-claimed-api-key-path 2026-04-06 06:17:42 -04:00
Allen Huang
2d129bfede fix(ui): inbox badge should only count unread mine issues
The sidebar inbox badge was counting all "mine" issues (issues created
by or assigned to the user) instead of only unread ones. This caused
the badge to show a count (e.g. 14) even when the Unread tab was empty.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 13:36:57 +08:00
Devin Foley
2e09570ce0 docs: enforce Model Used section in PR descriptions (#2891)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Agents create pull requests as part of their development workflow
> - The PR template already has a "Model Used" section (added in PR
#2552)
> - But agents were not filling it in because neither AGENTS.md nor
CONTRIBUTING.md referenced it
> - This PR updates both docs to explicitly require reading and filling
in the full PR template, including Model Used
> - The benefit is that every PR will now document which AI model
produced the change, improving traceability and auditability

## What Changed

- **CONTRIBUTING.md**: Added "Model Used (Required)" subsection under
"PR Requirements (all PRs)" and listed it in the required sections
enumeration
- **AGENTS.md**: Added new "Section 10: Pull Request Requirements"
instructing agents to read and fill in every section of the PR template
when creating PRs (including Model Used). Renumbered "Definition of
Done" to Section 11 and added PR template compliance as item 5.

## Verification

- Review `CONTRIBUTING.md` — confirm "Model Used (Required)" subsection
appears under PR Requirements
- Review `AGENTS.md` — confirm Section 10 (Pull Request Requirements)
lists all required PR template sections including Model Used
- Review `AGENTS.md` — confirm Definition of Done item 5 references PR
template compliance
- No code changes, no tests to run

## Risks

- Low risk — documentation-only changes. No code, schema, or behavioral
changes.

## Model Used

- **Provider**: Anthropic Claude
- **Model ID**: claude-opus-4-6 (1M context)
- **Capabilities**: Tool use, code execution, 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>
2026-04-05 19:04:49 -07:00
Matt Van Horn
866032eaaa fix(security): bump rollup to 4.59.0 to fix path-traversal CVE
Addresses GHSA-mw96-cpmx-2vgc (arbitrary file write via path
traversal in rollup <4.59.0). Bumps the direct dependency in the
plugin authoring example and adds a pnpm override for transitive
copies via Vite.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:33:05 -07:00
ErgonaWorks
81ff9fb311 fix(agent-auth): fall back to BETTER_AUTH_SECRET when PAPERCLIP_AGENT_JWT_SECRET is absent
`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>
2026-04-05 19:10:00 +00:00
plind
23eea392c8 Merge branch 'master' into fix/clear-extra-args-config 2026-04-05 22:23:50 +09:00
plind-dm
3513b60dbc test: assert attachments field in heartbeat-context response
Add missing assertion for the empty attachments array in the
heartbeat-context test to verify the field mapping is present.
2026-04-05 21:57:15 +09:00
Darren Davison
42989115a7 fix: re-open panel when childIssues changes to prevent stale sub-task list
Add childIssues to the useEffect dependency array so the Properties panel
is refreshed whenever the child issue list updates (e.g. an agent creates
or deletes a sub-task while the panel is open). Previously the panel kept
the snapshot from the initial render of the parent issue.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-05 12:02:49 +01:00
Darren Davison
7623f679cf fix: count all descendants in collapsed badge and prune stale localStorage IDs
Address two Greptile review comments:

1. Collapsed parent badge now shows total descendant count at all depths
   rather than direct-child count only. Add `countDescendants` utility to
   issue-tree.ts (recursive, uses existing childMap) and replace
   `children.length` with it in the titleSuffix badge.

2. Add a useEffect that prunes stale IDs from `collapsedParents` whenever
   the issues prop changes. Deleted or reassigned issues previously left
   orphan IDs in localStorage indefinitely; the effect filters to only IDs
   that appear as a parentId in the current issue list and persists the
   cleaned array via updateView.

Add four unit tests for countDescendants: leaf node, single-level,
multi-level, and unknown ID.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-05 12:02:49 +01:00
Darren Davison
9be1b3f8a9 test: extract buildIssueTree utility and add tests for hierarchy logic
Extract the inline tree-building logic from IssuesList into a pure
`buildIssueTree` function in lib/issue-tree.ts so it can be unit tested.
Add six tests covering: flat lists, parent-child grouping, multi-level
nesting, orphaned sub-tasks promoted to root, empty input, and list
order preservation.

Add two tests to IssueRow.test.tsx covering the new titleSuffix prop:
renders inline after the title when provided, and renders cleanly when
omitted.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-05 12:02:12 +01:00
Darren Davison
b380d6000f feat: show parent task and sub-tasks at bottom of issue properties panel
Move parent-task link out of the 2-column PropertyRow layout and into
a dedicated full-width section at the bottom of the panel, separated
by a Separator. Sub-tasks are listed in the same section when present.
Each item shows a StatusIcon aligned with the first line of wrapped
title text (items-start + mt-0.5 on the icon wrapper).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-05 12:02:12 +01:00
Darren Davison
e23d148be1 feat: persist collapse/expand state across navigation via localStorage
Move collapsedParents from ephemeral useState into IssueViewState,
which is already serialised to localStorage under the scoped key.
Navigating away and back now restores the exact collapsed/expanded
state the user left the list in.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-05 12:02:12 +01:00
Darren Davison
58a1a20f5b fix: indent nested sub-tasks at all depths using depth-based padding
Replace the boolean isChild flag with a numeric depth counter.
Each depth level adds 16px left padding via inline style on the
wrapper div, so sub-tasks of sub-tasks (and deeper) are indented
proportionally rather than all aligning at the same level.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-05 12:02:12 +01:00
Darren Davison
12011fa9de feat: show sub-task count in title when parent is collapsed
When a parent issue is collapsed, its title is suffixed with
"(N sub-tasks)" so the count remains visible at a glance.
The suffix disappears when the parent is expanded.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-05 12:02:12 +01:00
Darren Davison
11643941e6 fix: add sm:pl-7 to ensure child indentation is visible on desktop
The base IssueRow has sm:pl-1 which overrides pl-6 at sm+ breakpoints.
Adding sm:pl-7 ensures the indent is visible at all screen sizes.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-05 12:02:12 +01:00
Darren Davison
8cdb65febb feat: show sub-tasks indented under parent in issue list with collapse/expand
Sub-tasks are now grouped under their parent issue in the list view.
Parent issues with children show a chevron to collapse/expand their subtasks.
Child issues are visually indented to indicate hierarchy.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-05 12:02:12 +01:00
Matt Van Horn
2082bb61fe fix(security): bump multer to 2.1.1 to fix HIGH CVEs
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
2026-04-04 23:15:04 -07:00
Matt Van Horn
21a1e97a81 fix(server): prevent identifier collision in issue creation
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
2026-04-04 22:57:25 -07:00
Dotta
6c8569156c Merge pull request #2792 from paperclipai/pr/master-pre-rebind-recovery
Improve comment wake handling, issue search, and worktree dependency provisioning
2026-04-04 18:40:04 -05:00
dotta
c19208010a fix: harden worktree dependency hydration
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 18:37:19 -05:00
dotta
8ae4c0e765 Clean up opencode rebase and stabilize runtime test
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 18:15:28 -05:00
dotta
22af797ca3 Provision local node_modules in issue worktrees
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 18:15:10 -05:00
dotta
27accb1bdb Clarify issue-scoped comment wake prompts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 18:14:19 -05:00
dotta
b9b2bf3b5b Trim resumed comment wake prompts 2026-04-04 18:14:19 -05:00
dotta
4dea302791 Speed up issues-page search
Keep issue search local to the loaded list, defer heavy result updates, and memoize the rendered list body so typing stays responsive.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 18:14:19 -05:00
dotta
b825a121cb Prioritize comment wake prompts 2026-04-04 18:14:19 -05:00
dotta
91e040a696 Batch inline comment wake payloads
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 18:14:19 -05:00
Dotta
e75960f284 Merge pull request #2749 from paperclipai/fix/unified-toggle-mobile
Improve operator editing flows, mobile UI, and workspace runtime handling
2026-04-04 17:53:03 -05:00
dotta
94d4a01b76 Add skill slash-command autocomplete
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 17:48:54 -05:00
dotta
fe61e650c2 Avoid blur-save during mention selection
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 17:48:54 -05:00
dotta
c89349687f feat(ui): improve routines list and recent runs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 17:48:54 -05:00
dotta
f515f2aa12 Fix workspace runtime state reconciliation
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 17:48:54 -05:00
dotta
5a9a2a9112 Fix mobile mention menu placement
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 17:48:54 -05:00
dotta
65818c3447 Guard closed isolated workspaces on issues
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 17:48:54 -05:00
dotta
4993b5338c Fix horizontal scroll overflow in close workspace modal
Root cause: CSS Grid items default to min-width:auto, allowing content
to push the dialog wider than the viewport on mobile.

- Add [&>*]:min-w-0 on DialogContent to prevent grid children from
  expanding beyond the container width
- Keep overflow-x-hidden as safety net
- Remove negative-margin sticky footer that extended beyond bounds
- Revert to standard DialogFooter without negative margins

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 17:48:54 -05:00
dotta
d3401c0518 Fix horizontal scroll overflow in close workspace modal
- Add overflow-x-hidden on DialogContent to prevent horizontal scroll
- Truncate long monospace text (branch names, base refs) in git status grid
- Add min-w-0 on grid cells to allow truncation within CSS grid
- Add overflow-hidden on git status card and repo root section
- Add max-w-full + overflow-x-auto on pre blocks for long commands

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 17:48:54 -05:00
dotta
dbb5f0c4a9 Unify all toggle switches into a single responsive ToggleSwitch component
Replaces 12+ inline toggle button implementations across the app with a
shared ToggleSwitch component that scales up on mobile for better touch
targets. Default size is h-6/w-10 on mobile, h-5/w-9 on desktop; "lg"
variant is h-7/w-12 on mobile, h-6/w-11 on desktop.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 17:48:54 -05:00
dotta
3d685335eb Add sign out button to instance general settings
Adds a sign out section at the bottom of the general settings page.
Uses authApi.signOut() and invalidates the session query to redirect
to the login page.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 17:48:54 -05:00
dotta
2615450afc Make close workspace modal responsive for mobile
- Reduce padding and text sizes on small screens (p-4/text-xs -> sm:p-6/sm:text-sm)
- Tighter spacing between sections on mobile (space-y-3 -> sm:space-y-4)
- Sticky footer so action buttons stay visible while scrolling
- Grid layout stays 2-col on all sizes for git status
- Add shrink-0 to loading spinner

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 17:48:54 -05:00
Dotta
35f2fc7230 Merge pull request #2218 from HenkDz/feat/external-adapter-phase1
feat(adapters): external adapter plugin system with dynamic UI parser
2026-04-04 17:45:19 -05:00
dotta
d9476abecb fix(adapters): honor paused overrides and isolate UI parser state
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 14:04:33 -05:00
Devin Foley
d12650e5ac fix: update stale single-status checkout examples in worked docs
Greptile flagged that worked examples in task-workflow.md and
api-reference.md still used ["todo"] instead of the full
expectedStatuses array. Aligned them with the rest of the PR.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 11:43:29 -07:00
Devin Foley
d202631016 fix: autoformat pasted markdown in inline editor (#2673)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The inline markdown editor (MarkdownEditor / MDXEditor) is used to
edit agent instructions, issue descriptions, and other content
> - When users paste agent instructions copied from terminals or
consoles, extra leading whitespace is uniformly added to every line
> - PR #2572 fixed markdown structure preservation on paste but did not
address the leading whitespace (dedent) problem
> - This pull request adds a Lexical paste normalization plugin that
strips common leading whitespace and normalizes line endings before
MDXEditor processes pasted content
> - The benefit is that pasted content from terminals/consoles renders
correctly without manual cleanup

## What Changed

- **`ui/src/lib/normalize-markdown.ts`** — Pure utility that computes
minimum common indentation across non-empty lines and strips it
(dedent), plus CRLF → LF normalization
- **`ui/src/lib/paste-normalization.ts`** — Lexical `PASTE_COMMAND`
plugin at `CRITICAL` priority that intercepts plain-text pastes,
normalizes the markdown, and re-dispatches cleaned content for MDXEditor
to process. Skips HTML-rich pastes.
- **`ui/src/components/MarkdownEditor.tsx`** — Registers the new plugin;
updates PR #2572's `handlePasteCapture` to use `normalizeMarkdown()`
(dedent + CRLF) instead of `normalizePastedMarkdown()` (CRLF only) for
the markdown-routing path
- **`ui/src/lib/paste-normalization.test.ts`** — 9 unit tests covering
dedent, CRLF normalization, mixed indent, empty lines, single-line
passthrough, and edge cases

## Verification

- `pnpm --dir ui exec vitest run src/lib/paste-normalization.test.ts` —
9 tests pass
- Manual: paste indented agent instructions from a terminal into any
inline markdown editor and confirm leading whitespace is stripped

## Risks

- Low risk. The plugin only activates for plain-text pastes (no HTML
clipboard data). HTML/rich pastes pass through unchanged. Single-line
pastes are not modified. The dedent logic is conservative — it only
strips whitespace common to all non-empty lines.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-04 11:21:27 -07:00
Devin Foley
cd2be692e9 Fix in-review task recheckout guidance
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 11:20:29 -07:00
HenkDz
c6d2dc8b56 fix(ui): narrow overridePaused to boolean in AdapterManager spread 2026-04-04 17:08:53 +01:00
HenkDz
80b81459a7 Merge remote-tracking branch 'upstream/master' into feat/external-adapter-phase1 2026-04-04 14:33:48 +01:00
Dotta
a07237779b Merge pull request #2735 from paperclipai/chore/update-v2026-403-0-release-notes
Update v2026.403.0 release notes
2026-04-04 07:50:33 -05:00
dotta
21dd6acb81 updated release notes 2026-04-04 07:44:23 -05:00
HenkDz
b81d765d2e feat: server-side override pause/resume for builtin adapter types
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.
2026-04-04 13:17:21 +01:00
HenkDz
4efe018a8f fix(ui): external adapter UI parser can now override builtin parsers
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
2026-04-04 12:40:39 +01:00
HenkDz
0651f48f6c fix(ui): move reinstall button to end — power, reload, remove, reinstall 2026-04-03 23:09:30 +01:00
HenkDz
01c05b5f1b fix(ui): remove loaded badge, order badges — source, icon, version, override, disabled 2026-04-03 22:58:04 +01:00
HenkDz
c36ea1de6f fix(ui): reorder adapter badges — always-present first, conditional last 2026-04-03 22:51:49 +01:00
HenkDz
3c4b8711ec fix(ui): remove title prop from Lucide icons (not supported in this version) 2026-04-03 22:38:38 +01:00
HenkDz
ef2cbb838f chore: add trailing newline to server/package.json 2026-04-03 22:37:33 +01:00
HenkDz
fb3aabb743 feat(adapters): add overriddenBuiltin flag to API and Adapter Manager UI
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.
2026-04-03 22:25:58 +01:00
HenkDz
2a2fa31a03 feat(adapters): allow external plugins to override built-in 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.
2026-04-03 22:17:34 +01:00
Dotta
8adae848e4 Merge pull request #2675 from paperclipai/pap-feedback-trace-export-fixes
[codex] Restore feedback trace export fixes
2026-04-03 16:06:43 -05:00
dotta
00898e8194 Restore feedback trace export fixes 2026-04-03 15:59:42 -05:00
HenkDz
199a2178cf feat(ui): collapsible system_group block in transcript view
Batch consecutive system events into a single collapsible group
instead of rendering each as a separate warn-toned block. Shows
count in header, expands on click.
2026-04-03 21:52:36 +01:00
Dotta
ed95fc1dda Merge pull request #2674 from paperclipai/fix/feedback-test-uuid-redaction
fix: use deterministic UUID in feedback-service test to avoid phone redaction
2026-04-03 15:21:26 -05:00
HenkDz
c757a07708 fix(adapters): stable sort order, npm/local icons, reinstall dialog, HMR polling on WSL
- 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)
2026-04-03 21:11:24 +01:00
HenkDz
acfd7c260a feat: add hermes_local session management and show provider/model in run details 2026-04-03 21:11:23 +01:00
HenkDz
388650afc7 fix: update tests for SchemaConfigFields and comingSoon logic
- registry.test: fallback now uses SchemaConfigFields, not ProcessConfigFields
- metadata.test: isEnabledAdapterType checks comingSoon first so
  intentionally withheld built-in adapters (process/http) stay disabled
2026-04-03 21:11:23 +01:00
HenkDz
d7a7bda209 chore: restore pnpm-lock.yaml to upstream/master
CI blocks lockfile changes in PRs — restore to match base.
2026-04-03 21:11:23 +01:00
HenkDz
47f3cdc1bb fix(ui): external adapter selection, config field placement, and transcript parser freshness
- 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
2026-04-03 21:11:22 +01:00
HenkDz
69a1593ff8 feat(adapters): declarative config-schema API and UI for plugin adapters
Cherry-picked from feat/externalize-hermes-adapter.
Resolved conflicts: kept Hermes as built-in on phase1 branch.
2026-04-03 21:11:22 +01:00
HenkDz
f884cbab78 fix(adapters): restore built-in Hermes and sync lockfile with server
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
2026-04-03 21:11:21 +01:00
HenkDz
14d59da316 feat(adapters): external adapter plugin system with dynamic UI parser
- 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
2026-04-03 21:11:20 +01:00
Devin Foley
e13c3f7c6c fix: use deterministic UUID in feedback-service test to avoid phone redaction
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>
2026-04-03 13:04:56 -07:00
Dotta
f8452a4520 Merge pull request #2657 from paperclipai/fix/inbox-last-activity-ordering
Add versioned telemetry events
2026-04-03 14:19:05 -05:00
dotta
68b2fe20bb Address Greptile telemetry review comments
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-03 14:11:11 -05:00
Wes Belt
1ce800c158 docs: add claimedApiKeyPath to agentConfigurationDoc
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 14:15:36 -04:00
Devin Foley
aa256fee03 feat: add authenticated screenshot utility (#2622)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Agents sometimes need to capture UI screenshots for visual
verification of fixes
> - The Paperclip UI requires authentication, so headless browser
screenshots fail without auth
> - The CLI already stores a board token in `~/.paperclip/auth.json`
> - This pull request adds a Playwright-based screenshot script that
reads the board token and injects it as a Bearer header
> - The benefit is agents can now take authenticated screenshots of any
Paperclip UI page without storing email/password credentials

## What Changed

- Added `scripts/screenshot.cjs` — a Node.js script that:
  - Reads the board token from `~/.paperclip/auth.json`
- Launches Chromium via Playwright with the token as an `Authorization`
header
  - Navigates to the specified URL and saves a screenshot
  - Supports `--width`, `--height`, and `--wait` flags
- Accepts both full URLs and path-only shortcuts (e.g.,
`/PAPA/agents/cto/instructions`)

## Verification

```bash
node scripts/screenshot.cjs /PAPA/agents/cto/instructions /tmp/test.png --width 1920
```

Should produce an authenticated screenshot of the agent instructions
page.

## Risks

- Low risk — standalone utility script with no impact on the main
application. Requires Playwright (already a dev dependency) and a valid
board token in `~/.paperclip/auth.json`.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [ ] I have run tests locally and they pass
- [ ] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-03 10:51:26 -07:00
plind-dm
112eeafd62 fix: remove redundant bracket-notation redact path
Dot notation already covers the same key; bracket notation is a duplicate.
2026-04-04 01:33:08 +09:00
Devin Foley
258c7ccd21 fix: ensure agents respond to comments on in_review tasks
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>
2026-04-03 09:19:48 -07:00
馨冉
728fbdd199 Fix markdown paste handling in document editor (#2572)
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>
2026-04-03 08:50:48 -07:00
Wes Belt
8e42c6cdac fix(openclaw-gateway): make claimedApiKeyPath configurable per agent
The openclaw_gateway adapter hardcodes the Paperclip API key path to
~/.openclaw/workspace/paperclip-claimed-api-key.json in buildWakeText().
In multi-agent OpenClaw deployments, each agent has its own workspace
with its own key file. The hardcoded path forces all agents to share
one key, breaking agent identity isolation.

Add a claimedApiKeyPath field to the adapter config (with UI input)
that allows operators to set a per-agent path. Falls back to the
current default when unset — zero behavior change for existing
deployments.

Fixes #930
2026-04-03 11:25:58 -04:00
plind-dm
2af64b6068 fix(security): redact Bearer tokens from server log output
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
2026-04-03 23:50:45 +09:00
dotta
9b3ad6e616 Fix telemetry test mocking in agent skill routes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-03 09:43:58 -05:00
plind-dm
f749efd412 fix(ui): skip paused dimming on Paused filter tab
On the Paused tab every visible agent is paused, so applying
opacity-50 to all of them is redundant and makes the whole view
dim. Skip the dimming when tab === "paused" in both list and org
chart views. Pass tab prop through to OrgTreeNode for consistency.
2026-04-03 23:37:21 +09:00
plind
f2925ae0df Update ui/src/components/KanbanBoard.tsx
LGTM

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-03 23:37:12 +09:00
dotta
37b6ad42ea Add versioned telemetry events
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-03 09:25:00 -05:00
plind-dm
6d73a8a1cb test(import): verify frontmatter role fallback preserves CEO role
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".
2026-04-03 23:24:24 +09:00
plind
acb2bc6b3b Update ui/src/pages/Agents.tsx
Approved

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-03 23:22:02 +09:00
plind-dm
21ee44e29c fix(ui): make goal description area scrollable in create dialog
Long goal descriptions pushed the Create button below the viewport
with no way to scroll, making it impossible to submit the form. Add
overflow-y-auto and max-h-[50vh] to the description container so it
scrolls within the dialog while keeping the footer visible.

Closes #2631
2026-04-03 23:19:50 +09:00
plind-dm
58db67c318 fix(ui): collapse empty kanban columns to save horizontal space
Empty status columns took the same 260px width as populated ones,
wasting horizontal space and forcing unnecessary scrolling. Collapse
empty columns to 48px (showing only the status icon) and expand
them back when an issue is dragged over for drop targeting.

Closes #2279
2026-04-03 23:18:38 +09:00
plind-dm
87d46bba57 fix(ui): use null instead of undefined when clearing extra args
Clearing the extra args field set the overlay value to undefined,
which gets dropped during object spread when building the PATCH
payload. The existing extraArgs from the agent config survived the
merge, making it impossible to clear the field. Use null so the
value explicitly overwrites the existing config entry.

Closes #2350
2026-04-03 23:15:10 +09:00
plind-dm
045a3d54b9 fix(ui): dim paused agents in list and org chart views
Paused agents were visually identical to active agents in both the
list view and org chart, making it hard to distinguish them at a
glance. Add opacity-50 to agent rows when pausedAt is set.

Closes #2199
2026-04-03 23:14:05 +09:00
plind-dm
f467f3d826 fix(import): read agent role from frontmatter before 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
2026-04-03 23:04:44 +09:00
Dotta
2ac40aba56 Merge pull request #2645 from paperclipai/fix/feedback-row-run-link
fix(ui): tidy feedback actions and add v2026.403.0 changelog
2026-04-03 08:12:31 -05:00
chrisschwer
72408642b1 fix: add executionAgentNameKey to execution lock clears (Greptile)
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.
2026-04-03 15:11:42 +02:00
dotta
8db0c7fd2f docs: add v2026.403.0 release changelog
Covers 183 commits since v2026.325.0 including execution workspaces,
inbox overhaul, telemetry, feedback/evals, document revisions,
GitHub Enterprise support, and numerous fixes.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-03 08:01:23 -05:00
dotta
993a3262f6 fix(ui): place run link in same row as feedback buttons, right-aligned
When a comment has both helpful/needswork feedback buttons and a run link,
the run link now appears right-aligned in the same row instead of a separate
section below. Comments with only a run link (no feedback buttons) still
show the run link in its own bordered row.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-03 08:01:23 -05:00
dotta
a13a67de54 fix(ui): style Don't allow button as outline in feedback modal
The "Don't allow" button in the feedback sharing preference modal
should be visually distinct from "Always allow" by using an outline
variant instead of the default solid primary style.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-03 08:01:23 -05:00
Dotta
422dd51a87 Merge pull request #2638 from paperclipai/fix/inbox-last-activity-ordering
fix(inbox): prefer canonical last activity
2026-04-03 07:27:46 -05:00
dotta
a80edfd6d9 fix(inbox): prefer canonical last activity 2026-04-03 07:24:33 -05:00
chrisschwer
65e0d3d672 fix: stale execution lock lifecycle (PIP-002)
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.
2026-04-03 10:03:43 +02:00
Devin Foley
931678db83 fix: remove max-w-6xl from instructions tab (#2621)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The web UI includes an agent detail page with an Instructions tab
for editing agent prompts
> - The Instructions tab used `max-w-6xl` (1152px) to constrain its
two-panel layout (file tree + editor)
> - The floating Cancel/Save buttons used `float-right` at the full page
width, disconnecting them from the constrained content
> - This also left a large empty margin on the right side at wider
viewports
> - This pull request removes `max-w-6xl` so the flex layout fills
available width
> - The benefit is buttons now align with the content edge, and the
right margin is eliminated

## What Changed

- Removed `max-w-6xl` from the `PromptsTab` container in
`ui/src/pages/AgentDetail.tsx:1920`
- The file panel + editor flex layout now fills the available page width
naturally

## Verification

- Navigate to any agent's Instructions tab at a wide viewport (1920px+)
- Before: content stops at 1152px with a gap to the right; Cancel/Save
buttons float to the far edge
- After: content fills available width; Cancel/Save buttons sit flush
with the editor panel

## Risks

- Low risk — only removes a max-width constraint on a single tab's
container. Other tabs (Configuration, Skills, etc.) are unaffected.

## CI Note

The test failure in `feedback-service.test.ts:714` is **pre-existing**
and unrelated to this change. A PII redaction filter is incorrectly
treating a UUID segment (`5618-4783`) as a phone number, producing
`[REDACTED_PHONE]` in the expected UUID value.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [ ] I have run tests locally and they pass
- [ ] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-03 00:02:24 -07:00
Devin Foley
dda63a4324 Update CONTRIBUTING.md to require PR template, Greptile 5/5, and tests (#2618)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Contributors submit pull requests to improve the codebase
> - We have a PR template at `.github/PULL_REQUEST_TEMPLATE.md` that
standardizes PR descriptions
> - But PRs created via the API or other tooling sometimes bypass the
template
> - We also require Greptile automated review and passing tests, but
this wasn't clearly documented
> - This PR updates CONTRIBUTING.md to explicitly require use of the PR
template, a 5/5 Greptile score, and passing tests
> - The benefit is contributors have clear, upfront expectations for
what a mergeable PR looks like

## What Changed

- Added a new "PR Requirements (all PRs)" section to CONTRIBUTING.md
with three subsections:
- **Use the PR Template** — links to `.github/PULL_REQUEST_TEMPLATE.md`
and explains it must be used even when creating PRs outside the GitHub
UI
  - **Tests Must Pass** — requires local test runs and green CI
  - **Greptile Review** — requires 5/5 score with all comments addressed
- Updated Path 1 and Path 2 bullet points to reference the PR template,
Greptile 5/5, and CI requirements specifically
- Updated "Writing a Good PR message" section to link to the PR template
and clarify all sections are required

## Verification

- Read the updated CONTRIBUTING.md and verify it clearly references the
PR template, Greptile 5/5 requirement, and test requirements
- Verify all links to `.github/PULL_REQUEST_TEMPLATE.md` resolve
correctly

## Risks

- Low risk — documentation-only change, no code affected

## Model Used

- Provider: Anthropic Claude
- Model ID: claude-opus-4-6 (1M context)
- Capabilities: tool use, code editing

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-02 23:49:30 -07:00
Devin Foley
43fa9c3a9a fix(ui): make markdown editor monospace for agent instruction files (#2620)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The UI includes an inline markdown editor (MDXEditor) for editing
agent instruction files like AGENTS.md
> - The editor should render in monospace to match how markdown/code
files look in a text editor
> - The `AgentDetail.tsx` component already passes `font-mono` via
Tailwind's `contentClassName`, but it wasn't taking effect
> - Two CSS rules in `index.css` set `font-family: inherit`, which
overrode the Tailwind utility due to specificity/source order
> - This PR removes those overrides so `font-mono` applies correctly
> - The benefit is the markdown editor now renders in monospace
(Menlo/SF Mono), matching user expectations for code/config files

## What Changed

- Removed `font-family: inherit` from `.paperclip-mdxeditor
[class*="_placeholder_"]` in `ui/src/index.css`
- Removed `font-family: inherit` from `.paperclip-mdxeditor-content` in
`ui/src/index.css`

## Verification

- Navigate to any agent's Instructions tab in the Paperclip UI
- Confirm the markdown editor content renders in a monospace font
(Menlo/SF Mono)
- Visually verified by user on a live dev server

## Risks

- Low risk. Only removes two CSS declarations. Non-monospace editors are
unaffected since `font-mono` is only applied to agent instruction files
via `contentClassName` in `AgentDetail.tsx`.

## Screenshots
Before:
<img width="934" height="1228" alt="Screenshot 2026-04-02 at 10 46
06 PM"
src="https://github.com/user-attachments/assets/5d84f913-cbea-4206-9d41-3f283209c009"
/>

After:
<img width="1068" height="1324" alt="PNG image"
src="https://github.com/user-attachments/assets/2040e812-d9ca-4b37-b73b-ce05cf52168c"
/>

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [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>
2026-04-02 23:47:00 -07:00
plind-dm
c9ee8e7a7e fix(issues): replace non-null assertions with null checks in checkout re-read
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.
2026-04-03 09:56:23 +09:00
Octasoft Ltd
f843a45a84 fix: use sh instead of /bin/sh as shell fallback on Windows (#891)
## 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>
2026-04-02 17:34:26 -07:00
Dotta
36049beeea Merge pull request #2552 from paperclipai/PAPA-42-add-model-used-to-pr-template-and-checklist
feat: add Model Used section to PR template and checklist
2026-04-02 13:47:46 -05:00
Devin Foley
c041fee6fc feat: add Model Used section to PR template and checklist
Add a required "Model Used" section to the PR template so contributors
document which AI model (with version, context window, reasoning mode,
and other capability details) was used for each change. Also adds a
corresponding checklist item.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 11:32:22 -07:00
Dotta
82290451d4 Merge pull request #2541 from paperclipai/pap-1078-qol-fixes
fix(ui): polish issue detail timelines and attachments
2026-04-02 13:31:12 -05:00
dotta
fb3b57ab1f merge master into pap-1078-qol-fixes
Resolve the keyboard shortcut conflicts after [#2539](https://github.com/paperclipai/paperclip/pull/2539) and [#2540](https://github.com/paperclipai/paperclip/pull/2540), keep the release package rewrite working with cliVersion, and stabilize the provisioning timeout in the full suite.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 13:14:20 -05:00
Dotta
ca8d35fd99 Merge pull request #2540 from paperclipai/pap-1078-inbox-operator-polish
feat(inbox): add operator search and keyboard controls
2026-04-02 13:02:33 -05:00
Dotta
81a7f79dfd Merge pull request #2539 from paperclipai/pap-1078-workspaces-routines
feat(routines): add workspace-aware routine runs
2026-04-02 13:01:19 -05:00
dotta
ad1ef6a8c6 fix(ui): address final Greptile follow-up
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 12:21:35 -05:00
dotta
833842b391 fix(inbox): address Greptile review findings
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 12:16:34 -05:00
dotta
fd6cfc7149 fix(routines): address Greptile review findings
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 12:09:02 -05:00
plind
620a5395d7 Update server/src/routes/issues.ts
LGTM

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-03 02:01:46 +09:00
plind-dm
1350753f5f fix(api): include attachment metadata in heartbeat-context response
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
2026-04-03 01:53:57 +09:00
dotta
50e9f69010 fix(ui): surface skipped wakeup messages in agent detail
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 11:52:43 -05:00
dotta
38a0cd275e test(ui): cover routine run variables dialog
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 11:52:13 -05:00
dotta
bd6d07d0b4 fix(ui): polish issue detail timelines and attachments
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 11:51:40 -05:00
dotta
3ab7d52f00 feat(inbox): add operator search and keyboard controls
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 11:45:15 -05:00
dotta
909e8cd4c8 feat(routines): add workspace-aware routine runs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 11:38:57 -05:00
Dotta
36376968af Merge pull request #2527 from paperclipai/PAP-806-telemetry-implementation-in-paperclip-plan
Add app, server, and plugin telemetry plumbing
2026-04-02 11:10:16 -05:00
dotta
29d0e82dce fix: make feedback migration replay-safe after rebase
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 10:54:56 -05:00
dotta
1c1040e219 test: make cli telemetry test deterministic in CI
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 10:47:30 -05:00
dotta
0ec8257563 fix: include shared telemetry sources in cli typecheck
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 10:47:30 -05:00
dotta
38833304d4 fix: restore cli telemetry config handling in worktrees
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 10:47:30 -05:00
dotta
85e6371cb6 fix: use agent role for first heartbeat telemetry
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 10:47:30 -05:00
dotta
daea94a2ed test: align task-completed telemetry assertion with agent role
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 10:47:30 -05:00
dotta
c18b3cb414 fix: use agent role instead of adapter type in task_completed telemetry
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>
2026-04-02 10:47:30 -05:00
dotta
af844b778e Add plugin telemetry bridge capability
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>
2026-04-02 10:47:29 -05:00
dotta
53dbcd185e fix: align telemetry client payload and dimensions with backend schema
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>
2026-04-02 10:47:29 -05:00
dotta
f16de6026d fix: add periodic flush and graceful shutdown for server-side telemetry
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>
2026-04-02 10:47:29 -05:00
dotta
34044cdfce feat: implement app-side telemetry sender
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>
2026-04-02 10:47:29 -05:00
Dotta
ca5659f734 Merge pull request #2529 from paperclipai/PAP-880-thumbs-capture-for-evals-feature-pr
Add feedback voting and thumbs capture flow
2026-04-02 10:44:50 -05:00
plind-dm
77faf8c668 fix(onboarding): remove residual $AGENT_HOME reference in CEO AGENTS.md
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>
2026-04-03 00:08:31 +09:00
dotta
d12e3e3d1a Fix feedback review findings
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 10:03:07 -05:00
plind-dm
2fca400dd9 fix(onboarding): use relative paths instead of $AGENT_HOME in CEO instructions
$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>
2026-04-02 23:53:53 +09:00
dotta
c0d0d03bce Add feedback voting and thumbs capture flow
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 09:11:49 -05:00
Dotta
3db6bdfc3c Merge pull request #2414 from aronprins/skill/routines
feat(skills): add paperclip-routines skill
2026-04-02 06:37:44 -05:00
dotta
6524dbe08f fix(skills): move routines docs into paperclip references
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 06:28:04 -05:00
Dotta
2c1883fc77 Merge pull request #2449 from statxc/feat/github-enterprise-url-support
feat: GitHub enterprise url support
2026-04-02 06:07:44 -05:00
Aron Prins
4abd53c089 fix(skills): tighten api-reference table descriptions to match existing style
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 11:00:53 +02:00
Aron Prins
3c99ab8d01 chore: improve api documentation and implementing routines properly. 2026-04-02 10:52:52 +02:00
Devin Foley
9d6d159209 chore: add package files to CODEOWNERS for dependency review (#2476)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The GitHub repository uses CODEOWNERS to enforce review requirements on critical files
> - Currently only release scripts and CI config are protected — package manifests are not
> - Dependency changes (package.json, lockfile) can introduce supply-chain risk if merged without review
> - This PR adds all package files to CODEOWNERS
> - The benefit is that any dependency change now requires explicit approval from maintainers

## What Changed

- Added root package manifest files (`package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `.npmrc`) to CODEOWNERS
- Added all 19 workspace `package.json` files (`cli/`, `server/`, `ui/`, `packages/*`) to CODEOWNERS
- All entries owned by `@cryppadotta` and `@devinfoley`, consistent with existing release infrastructure ownership

## Verification

- `gh api repos/paperclipai/paperclip/contents/.github/CODEOWNERS?ref=PAPA-41-add-package-files-to-codeowners` to inspect the file
- Open a test PR touching any `package.json` and confirm GitHub requests review from the listed owners

## Risks

- Low risk. CODEOWNERS only adds review requirements — does not block merges unless branch protection enforces it. New packages added in the future will need a corresponding CODEOWNERS entry.

## Checklist

- [x] I have included a thinking path that traces from project context to this change
- [x] I have run tests locally and they pass
- [ ] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-01 20:32:39 -07:00
Devin Foley
26069682ee fix: copy button fallback for non-secure contexts (#2472)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The UI serves agent management pages including an instructions editor with copy-to-clipboard buttons
> - The Clipboard API (`navigator.clipboard.writeText`) requires a secure context (HTTPS or localhost)
> - Users accessing the UI over HTTP on a LAN IP get "Copy failed" when clicking the copy icon
> - This pull request adds an `execCommand("copy")` fallback in `CopyText` for non-secure contexts
> - The benefit is that copy buttons work reliably regardless of whether the page is served over HTTPS or plain HTTP

## What Changed

- `ui/src/components/CopyText.tsx`: Added `window.isSecureContext` check before using `navigator.clipboard`. When unavailable, falls back to creating a temporary `<textarea>`, selecting its content, and using `document.execCommand("copy")`. The return value is checked and the DOM element is cleaned up via `try/finally`.

## Verification

- Access the UI over HTTP on a non-localhost IP (e.g. `http://[local-ip]:3100`)
- Navigate to any agent's instructions page → Advanced → click the copy icon next to Root path
- Should show "Copied!" tooltip and the path should be on the clipboard

## Risks

- Low risk. `execCommand("copy")` is deprecated in the spec but universally supported by all major browsers. The fallback only activates in non-secure contexts where the modern API is unavailable. If/when HTTPS is enabled, the modern `navigator.clipboard` path is used automatically.

## Checklist

- [x] I have included a thinking path that traces from project context to this change
- [ ] I have run tests locally and they pass
- [ ] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after screenshots
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before requesting merge
2026-04-01 20:16:52 -07:00
Devin Foley
1e24e6e84c fix: auto-detect default branch for worktree creation when baseRef not configured (#2463)
* 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>
2026-04-01 18:00:49 -07:00
statxc
9d89d74d70 refactor: rename URL validators to looksLikeRepoUrl 2026-04-01 23:21:22 +00:00
statxc
056a5ee32a fix(ui): render agent capabilities field in org chart cards (#2349)
* fix(ui): render agent capabilities field in org chart cards

Closes #2209

* Update ui/src/pages/OrgChart.tsx

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-01 15:46:44 -07:00
Devin Foley
dedd972e3d Fix inbox ordering: self-touched issues no longer sink to bottom (#2144)
issueLastActivityTimestamp() returned 0 for issues where the user was
the last to touch them (myLastTouchAt >= updatedAt) and no external
comment existed. This pushed those items to the bottom of the inbox
list regardless of how recently they were updated.

Now falls back to updatedAt instead, so recently updated items sort
to the top of the Recent tab as expected.

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-01 14:52:53 -07:00
statxc
6a7830b07e fix: add HTTPS protocol check to server-side GitHub URL parsers 2026-04-01 21:27:10 +00:00
statxc
f9cebe9b73 fix: harden GHE URL detection and extract shared GitHub helpers 2026-04-01 21:05:48 +00:00
statxc
9e1ee925cd feat: support GitHub Enterprise URLs for skill and company imports 2026-04-01 20:53:41 +00:00
SparkEros
c424f06263 fix: prevent blank screen when clearing Capabilities field
The MarkdownEditor in the agent Configuration tab crashes when the
Capabilities field is fully cleared. The onChange handler converts empty
strings to null via (v || null), but the eff() overlay function then
returns that null to MDXEditor which expects a string, causing an
unhandled React crash and a blank screen.

Add a null-coalescing fallback (?? "") so MDXEditor always receives a
string.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:24:15 -05:00
Daniel Sousa
77f854c081 feat(company-skills): implement skill deletion with agent usage check
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.
2026-04-01 17:18:01 +01:00
Dotta
6c2c63e0f1 Merge pull request #2328 from bittoby/fix/project-slug-collision
Fix: project slug collisions for non-English names (#2318)
2026-04-01 09:34:23 -05:00
Dotta
461779a960 Merge pull request #2430 from bittoby/fix/add-gemini-local-to-adapter-types
fix: add gemini_local to AGENT_ADAPTER_TYPES validation enum
2026-04-01 09:18:39 -05:00
bittoby
6aa3ead238 fix: add gemini_local to AGENT_ADAPTER_TYPES validation enum 2026-04-01 14:07:47 +00:00
Dotta
e0f64c04e7 Merge pull request #2407 from radiusred/chore/docker-improvements
chore(docker): improve base image and organize docker files
2026-04-01 08:14:55 -05:00
TimoYi | HearthCore | ZenWise
9b238d9644 Update packages/adapter-utils/src/server-utils.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-01 14:37:46 +02:00
Aron Prins
e5b2e8b29b fix(skills): address greptile review on paperclip-routines skill
- Add missing `description` field to the Creating a Routine field table
- Document optional `label` field available on all trigger kinds
2026-04-01 13:56:10 +02:00
Aron Prins
62d8b39474 feat(skills): add paperclip-routines skill
Adds a new skill that documents how to create and manage Paperclip
routines — recurring tasks that fire on a schedule, webhook, or API
call and dispatch an execution issue to the assigned agent.
2026-04-01 13:49:11 +02:00
Cody (Radius Red)
420cd4fd8d chore(docker): improve base image and organize docker files
- Add wget, ripgrep, python3, and GitHub CLI (gh) to base image
- Add OPENCODE_ALLOW_ALL_MODELS=true to production ENV
- Move compose files, onboard-smoke Dockerfile to docker/
- Move entrypoint script to scripts/docker-entrypoint.sh
- Add Podman Quadlet unit files (pod, app, db containers)
- Add docker/README.md with build, compose, and quadlet docs
- Add scripts/docker-build-test.sh for local build validation
- Update all doc references for new file locations
- Keep main Dockerfile at project root (no .dockerignore changes needed)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-01 11:36:27 +00:00
Timo Götzken
b642d3e06b fix(adapter-utils): use cmd.exe for .cmd/.bat wrappers on Windows
Avoid relying on ComSpec for .cmd/.bat invocation in runChildProcess. Some Win11 environments set ComSpec to PowerShell, which breaks cmd-specific flags (/d /s /c) and causes adapter CLI discovery failures (e.g. opencode models).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-01 09:25:39 +02:00
Dotta
5b479652f2 Merge pull request #2327 from radiusred/fix/env-var-plain-to-secret-data-loss
fix(ui): preserve env var when switching type from Plain to Secret
2026-03-31 11:37:07 -05:00
bittoby
99296f95db fix: append short UUID suffix to project slugs when non-ASCII characters are stripped to prevent slug collisions 2026-03-31 16:35:30 +00:00
Cody (Radius Red)
92e03ac4e3 fix(ui): prevent dropdown snap-back when switching env var to Secret
Address Greptile review feedback: the plain-value fallback in emit()
caused the useEffect sync to re-run toRows(), which mapped the plain
binding back to source: "plain", snapping the dropdown back.

Fix: add an emittingRef that distinguishes local emit() calls from
external value changes (like overlay reset after save). When the
change originated from our own emit, skip the re-sync so the
transitioning row stays in "secret" mode while the user picks a secret.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-31 15:52:46 +00:00
Cody (Radius Red)
ce8d9eb323 fix(server): preserve adapter-agnostic keys when changing adapter type
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>
2026-03-31 15:42:03 +00:00
Cody (Radius Red)
06cf00129f fix(ui): preserve env var when switching type from Plain to Secret
When changing an env var's type from Plain to Secret in the agent
config form, the row was silently dropped because emit() skipped
secret rows without a secretId. This caused data loss — the variable
disappeared from both the UI and the saved config.

Fix: keep the row as a plain binding during the transition state
until the user selects an actual secret. This preserves the key and
value so nothing is lost.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-31 15:09:54 +00:00
Dotta
ebc6888e7d Merge pull request #1923 from radiusred/fix/docker-volumes
fix(docker): remap container UID/GID at runtime to avoid volume mount permission errors
2026-03-31 08:46:27 -05:00
Dotta
9f1bb350fe Merge pull request #2065 from edimuj/fix/heartbeat-session-reuse
fix: preserve session continuity for timer/heartbeat wakes
2026-03-31 08:29:45 -05:00
Dotta
46ce546174 Merge pull request #2317 from paperclipai/PAP-881-document-revisions-bulid-it
Add issue document revision restore flow
2026-03-31 08:25:07 -05:00
dotta
90889c12d8 fix(db): make document revision migration replay-safe 2026-03-31 08:09:00 -05:00
dotta
761dce559d test(worktree): avoid assuming a specific free port 2026-03-31 07:44:19 -05:00
dotta
41f261eaf5 Merge public-gh/master into PAP-881-document-revisions-bulid-it 2026-03-31 07:31:17 -05:00
Dotta
8427043431 Merge pull request #112 from kevmok/add-gpt-5-4-xhigh-effort
Add gpt-5.4 fallback and xhigh effort options
2026-03-31 06:19:38 -05:00
Dotta
19aaa54ae4 Merge branch 'master' into add-gpt-5-4-xhigh-effort 2026-03-31 06:19:26 -05:00
Cody (Radius Red)
d134d5f3a1 fix: support host UID/GID mapping for volume mounts
- Add USER_UID/USER_GID build args to Dockerfile
- Install gosu and remap node user/group at build time
- Set node home directory to /paperclip so agent credentials resolve correctly
- Add docker-entrypoint.sh for runtime UID/GID remapping via gosu

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 23:48:21 +00:00
Dotta
98337f5b03 Merge pull request #2203 from paperclipai/pap-1007-workspace-followups
fix: preserve workspace continuity across follow-up issues
2026-03-30 15:24:47 -05:00
dotta
477ef78fed Address Greptile feedback on workspace reuse
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 14:55:44 -05:00
Dotta
b0e0f8cd91 Merge pull request #2205 from paperclipai/pap-1007-publishing-docs
docs: add manual @paperclipai/ui publishing prerequisites
2026-03-30 14:48:52 -05:00
Dotta
ccb5cce4ac Merge pull request #2204 from paperclipai/pap-1007-operator-polish
fix: apply operator polish across comments, invites, routines, and health
2026-03-30 14:48:24 -05:00
Dotta
5575399af1 Merge pull request #2048 from remdev/fix/codex-rpc-client-spawn-error
fix(codex) rpc client spawn error
2026-03-30 14:24:33 -05:00
dotta
2c75c8a1ec docs: clarify npm prerequisites for first ui publish
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 14:15:30 -05:00
dotta
d8814e938c docs: add manual @paperclipai/ui publish steps
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 14:15:30 -05:00
dotta
a7cfbc98f3 Fix optimistic comment draft clearing 2026-03-30 14:14:36 -05:00
dotta
5e65bb2b92 Add company name to invite summaries
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 14:14:14 -05:00
dotta
d7d01e9819 test: add company settings selectors
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 14:14:14 -05:00
dotta
88e742a129 Fix health DB connectivity probe
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 14:14:14 -05:00
dotta
db4e146551 Fix routine modal scrolling
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 14:14:14 -05:00
dotta
9684e7bf30 Add dark mode inbox selection color
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 14:14:14 -05:00
dotta
a3e125f796 Clarify Claude transcript event categories
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 14:13:52 -05:00
dotta
2b18fc4007 Repair server workspace package links in worktrees
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 14:10:36 -05:00
dotta
ec1210caaa Preserve workspaces for follow-up issues
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 14:10:36 -05:00
dotta
3c66683169 Fix execution workspace reuse and slugify worktrees
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 14:10:36 -05:00
Dotta
c610192c53 Merge pull request #2074 from paperclipai/pap-979-runtime-workspaces
feat: expand execution workspace runtime controls
2026-03-30 08:35:50 -05:00
dotta
4d61dbfd34 Merge public-gh/master into pap-979-runtime-workspaces
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 08:35:30 -05:00
Dotta
26a974da17 Merge pull request #2072 from paperclipai/pap-979-board-ux
ui: improve board inbox and issue detail workflows
2026-03-30 08:31:29 -05:00
Dotta
8a368e8721 Merge pull request #2176 from paperclipai/fix/revert-paperclipai-script-path-clean
fix: restore root paperclipai script tsx path
2026-03-30 08:31:03 -05:00
dotta
c8ab70f2ce fix: restore paperclipai tsx script path 2026-03-30 08:20:00 -05:00
Dotta
29da357c5b Merge pull request #2071 from paperclipai/pap-979-cli-onboarding
cli: preserve config when onboarding existing installs
2026-03-30 07:45:19 -05:00
Dotta
4120016d30 Merge pull request #2070 from paperclipai/pap-979-commit-metrics
chore: add Paperclip commit metrics exporter
2026-03-30 07:44:10 -05:00
Dotta
fceefe7f09 Merge pull request #2171 from paperclipai/PAP-987-pr-1001-vite-hmr
fix: preserve PWA tags and StrictMode-safe live updates
2026-03-30 07:38:51 -05:00
Dotta
2d31c71fbe Merge pull request #1744 from mvanhorn/fix/board-mutation-forwarded-host
fix(server): include x-forwarded-host in board mutation origin check
2026-03-30 07:34:08 -05:00
dotta
b5efd8b435 Merge public-gh/master into fix/hmr-websocket-reverse-proxy
Reconcile the PR with current master, preserve both PWA capability meta tags, and add websocket lifecycle coverage for the StrictMode-safe live updates fix.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 07:17:23 -05:00
dotta
fc2be204e2 Fix CLI README Discord badge
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 06:49:15 -05:00
dotta
92ebad3d42 Address runtime workspace review feedback
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 06:48:45 -05:00
dotta
5310bbd4d8 Address board UX review feedback
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 06:46:21 -05:00
dotta
c54b985d9f Handle commit metrics search edge cases
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 06:44:46 -05:00
Shoaib Ansari
32fe1056e7 fix goal view properties toggle 2026-03-30 12:49:22 +05:30
Shoaib Ansari
8e2148e99d fix openclaw gateway session key routing 2026-03-30 12:13:39 +05:30
Edin Mujkanovic
70702ce74f fix: preserve session continuity for timer/heartbeat wakes
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>
2026-03-29 18:19:02 +02:00
dotta
b1b3408efa Restrict sidebar reordering to mouse input
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
57357991e4 Set inbox selection to fixed light gray
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
50577b8c63 Neutralize selected inbox accents
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
1871a602df Align inbox non-issue selection styling
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
facf994694 Align inbox click selection styling
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
403aeff7f6 Refine mine inbox shortcut behavior
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
7d81e4cb2a Fix mine inbox keyboard selection
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
44f052f4c5 Fix inbox selection highlight to show on individual items
Replace outline approach (blended with card border, invisible) with:
- 3px blue left-border bar (absolute positioned, like Gmail)
- Subtle tinted background with forced transparent children so the
  highlight shows through opaque child backgrounds

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
c33dcbd202 Fix keyboard shortcuts using refs to avoid stale closures
Refactored keyboard handler to use refs (kbStateRef, kbActionsRef) for
all mutable state and actions. This ensures the single stable event
listener always reads fresh values instead of relying on effect
dependency re-registration which could miss updates.

Also fixed selection highlight visibility: replaced bg-accent (too
subtle) with bg-primary/10 + outline-primary/30 which is clearly
visible in both light and dark modes.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
bc61eb84df Remove comment composer interrupt checkbox
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
74687553f3 Improve queued comment thread UX
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
4226e15128 Add issue comment interrupt support
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
cfb7dd4818 Harden optimistic comment IDs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
52bb4ea37a Add optimistic issue comment rendering
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
3986eb615c fix(ui): harden issue breadcrumb source routing
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
0f9faa297b Style markdown links with underline and pointer cursor
Links in both rendered markdown (.paperclip-markdown) and the MDXEditor
(.paperclip-mdxeditor-content) now display with underline text-decoration
and cursor:pointer by default. Mention chips are excluded from underline
styling to preserve their pill appearance.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:57:34 -05:00
dotta
d917375e35 Fix invisible keyboard selection highlight in inbox
Replace ring-2 outline (clipped by overflow-hidden container) with
bg-accent background color for the selected item. Visible in both
light and dark modes.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
ce4536d1fa Add agent Mine inbox API surface
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
4fd62a3d91 fix: prevent 'Mark all as read' from wrapping on mobile
Restructured the inbox header layout to always keep tabs and the
button on the same row using flex justify-between (no responsive
column stacking). Filter dropdowns for the All tab are now on a
separate row below.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
25066c967b fix: clamp mention dropdown position to viewport on mobile
The portal-rendered mention dropdown could appear off-screen on mobile
devices. Clamp top/left to keep it within the viewport and cap width
to 100vw - 16px.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
1534b39ee3 Move 'Mark all as read' button to top-right of inbox header
Moved the button out of the tabs wrapper and into the right-side flex
container so it aligns to the right instead of wrapping below the tabs.
The button now sits alongside the filter dropdowns (on the All tab) or
alone on the right (on other tabs).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
826da2973d Tighten mine-only inbox swipe archive
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
4426d96610 Restrict inbox keyboard shortcuts to mine tab only
All keyboard shortcuts (j/k/a/y/U/r/Enter) now only fire when the
user is on the "Mine" tab. Previously j/k and other navigation
shortcuts were active on all tabs.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
c8956094ad Add y as inbox archive shortcut alongside a
Both a and y now archive the selected item in the mine tab.
Archive requires selecting an item first with j/k navigation.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
2ec4ba629e Add mail-client keyboard shortcuts to inbox mine tab
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>
2026-03-29 10:57:34 -05:00
dotta
182b459235 Add "Today" divider line in inbox between recent and older items
Shows a dark gray horizontal line with "Today" label on the right,
vertically centered, between items from the last 24 hours and older
items. Applies to all inbox tabs (Mine, Recent, Unread, All).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
94d6ae4049 Fix inbox swipe-to-archive click-through
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:57:34 -05:00
dotta
b3d61a7561 Clarify manual workspace runtime behavior
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:45 -05:00
dotta
d9005405b9 Add linked issues row to execution workspace detail
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:26 -05:00
dotta
e3f07aad55 Fix execution workspace runtime control reuse
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:26 -05:00
dotta
2fea39b814 Reduce run lifecycle toast noise
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:26 -05:00
dotta
0356040a29 Improve workspace detail mobile layouts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:26 -05:00
dotta
caa7550e9f Fix shared workspace close semantics
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>
2026-03-29 10:55:26 -05:00
dotta
84d4c328f5 Harden runtime service env sanitization
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:26 -05:00
dotta
11f08ea5d5 Fix execution workspace close messaging
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:26 -05:00
dotta
1f1fe9c989 Add workspace runtime controls
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>
2026-03-29 10:55:26 -05:00
dotta
f1ad07616c Add execution workspace close readiness and UI
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:26 -05:00
dotta
868cfa8c50 Auto-apply dev:once migrations
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:26 -05:00
dotta
6793dde597 Add idempotent local dev service management
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:26 -05:00
dotta
cadfcd1bc6 Log resolved adapter command in run metadata
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:26 -05:00
dotta
c114ff4dc6 Improve execution workspace detail editing
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:21 -05:00
dotta
84e35b801c Fix execution workspace company routing
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:21 -05:00
dotta
cbeefbfa5a Fix project workspace detail route loading
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:21 -05:00
dotta
2de691f023 Link workspace titles from project tab
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:21 -05:00
dotta
41f2a80aa8 Fix issue workspace detail links
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:21 -05:00
dotta
bb1732dd11 Add project workspace detail page
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:21 -05:00
dotta
15e0e2ece9 Add workspace path copy control
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:21 -05:00
dotta
b7b5d8dae3 Polish workspace issue badges
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:21 -05:00
dotta
0ff778ec29 Exclude default shared workspaces from tab
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:21 -05:00
dotta
b69f0b7dc4 Adjust workspace row columns
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:21 -05:00
dotta
b75ac76b13 Add project workspaces tab
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:55:21 -05:00
dotta
19b6adc415 Use exported tsx CLI entrypoint
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:51:58 -05:00
dotta
54b05d6d68 Make onboarding reruns preserve existing config
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:51:58 -05:00
dotta
f83a77f41f Add cli/README.md with absolute image URLs for npm
The root README uses relative doc/assets/ paths which work on GitHub
but break on npmjs.com since those files aren't in the published
tarball. This adds a cli-specific README with absolute
raw.githubusercontent.com URLs so images render on npm.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:51:58 -05:00
dotta
a3537a86e3 Add filtered Paperclip commit exports
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:51:16 -05:00
dotta
5d538d4792 Add Paperclip commit metrics script
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:51:16 -05:00
Mikhail Batukhtin
dc3aa8f31f test(codex-local): isolate quota spawn test from host CODEX_HOME
After the mocked RPC spawn fails, getQuotaWindows() still calls
readCodexToken(). Use an empty mkdtemp directory for CODEX_HOME for the
duration of the test so we never read ~/.codex/auth.json or call WHAM.
2026-03-29 15:15:37 +03:00
Mikhail Batukhtin
c98af52590 test(codex-local): regression for CodexRpcClient spawn ENOENT
Add a Vitest case that mocks `node:child_process.spawn` so the child
emits `error` (ENOENT) after the constructor attaches listeners.
`getQuotaWindows()` must resolve with `ok: false` instead of leaving an
unhandled `error` event on the process.

Register `packages/adapters/codex-local` in the root Vitest workspace.

Document in DEVELOPING.md that a missing `codex` binary should not take
down the API server during quota polling.
2026-03-29 14:43:51 +03:00
Mikhail Batukhtin
01fb97e8da fix(codex-local): handle spawn error event in CodexRpcClient
When the `codex` binary is absent from PATH, Node.js emits an `error`
event on the ChildProcess. Because `CodexRpcClient` only subscribed to
`exit` and `data` events, the `error` event was unhandled — causing
Node to throw it as an uncaught exception and crash the server.

Add an `error` handler in the constructor that rejects all pending RPC
requests and clears the queue. This makes a missing `codex` binary a
recoverable condition: `fetchCodexRpcQuota()` rejects, `getQuotaWindows()`
catches the error and returns `{ ok: false }`, and the server stays up.

The fix mirrors the existing pattern in `runChildProcess`
(packages/adapter-utils/src/server-utils.ts) which already handles
`ENOENT` the same way for the main task execution path.
2026-03-29 14:20:55 +03:00
Dotta
6a72faf83b Merge pull request #1949 from vanductai/fix/dev-watch-tsx-cli-path
fix(server): use stable tsx/cli entry point in dev-watch
2026-03-28 16:45:04 -05:00
Dotta
1fd40920db Merge pull request #1974 from paperclipai/chore/refresh-lockfile
chore(lockfile): refresh pnpm-lock.yaml
2026-03-28 06:50:53 -05:00
lockfile-bot
caef115b95 chore(lockfile): refresh pnpm-lock.yaml 2026-03-28 11:46:21 +00:00
Dotta
17e5322e28 Merge pull request #1955 from HenkDz/feat/hermes-adapter-upgrade
feat(hermes): upgrade hermes-paperclip-adapter + UI adapter, skills, model detection
2026-03-28 06:46:01 -05:00
HenkDz
582f4ceaf4 fix: address Hermes adapter review feedback 2026-03-28 11:35:58 +01:00
HenkDz
1583a2d65a feat(hermes): upgrade hermes-paperclip-adapter + UI adapter + skills + detectModel
Upgrades hermes-paperclip-adapter from 0.1.1 to ^0.2.0 and wires in all new
capabilities introduced in v0.2.0:

Server
- Upgrade hermes-paperclip-adapter 0.1.1 -> ^0.2.0 (pending PR#10 merge)
- Wire listSkills + syncSkills from hermes-paperclip-adapter/server
- Add detectModel to hermesLocalAdapter (reads ~/.hermes/config.yaml)
- Add detectAdapterModel() function + /adapters/:type/detect-model route
- Export detectAdapterModel from server/src/adapters/index.ts

Types
- Add optional detectModel? to ServerAdapterModule in adapter-utils

UI
- Add hermes-paperclip-adapter ^0.2.0 to ui/package.json (for /ui exports)
- New ui/src/adapters/hermes-local/ — config fields + UI adapter module
- Register hermesLocalUIAdapter in UI adapter registry
- New HermesIcon (caduceus SVG) for adapter pickers
- AgentConfigForm: detect-model button, creatable model input, preserve
  adapter-agnostic fields (env, promptTemplate) when switching adapter type
- NewAgentDialog + OnboardingWizard: add Hermes to adapter picker
- Agents, OrgChart, InviteLanding, NewAgent, agent-config-primitives: add
  hermes_local label + enable in adapter sets
- AgentDetail: smarter run summary excerpt extraction
- RunTranscriptView: improved Hermes stdout rendering

NOTE: requires hermes-paperclip-adapter@0.2.0 on npm.
      Blocked on NousResearch/hermes-paperclip-adapter#10 merging.
2026-03-28 01:34:48 +01:00
vanductai
9a70a4edaa fix(server): use stable tsx/cli entry point in dev-watch
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.
2026-03-28 06:42:03 +07:00
Dotta
0ac01a04e5 Merge pull request #1891 from paperclipai/docs/maintenance-20260327-public
docs: documentation accuracy update 2026-03-27
2026-03-27 07:47:24 -05:00
dotta
11ff24cd22 docs: fix adapter type references and complete adapter table
- Fix openclaw → openclaw_gateway type key in adapters overview and managing-agents guide
- Add missing adapters to overview table: hermes_local, cursor, pi_local
- Mark gemini_local as experimental (adapter package exists but not in stable type enum)
- Update "Choosing an Adapter" recommendations to match stable adapter set

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 01:05:08 -05:00
Devin Foley
a5d47166e2 docs: add board-operator delegation guide (#1889)
* docs: add board-operator delegation guide

Create docs/guides/board-operator/delegation.md explaining the full
CEO-led delegation lifecycle from the board operator's perspective.
Covers what the board needs to do, what the CEO automates, common
delegation patterns (flat, 3-level, hire-on-demand), and a
troubleshooting section that directly answers the #1 new-user
confusion point: "Do I have to tell the CEO to delegate?"

Also adds a Delegation section to core-concepts.md and wires the
new guide into docs.json navigation after Managing Tasks.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* docs: add AGENTS.md troubleshooting note to delegation guide

Add a row to the troubleshooting table telling board operators to
verify the CEO's AGENTS.md instructions file contains delegation
directives. Without these instructions, the CEO won't delegate.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* docs: fix stale concept count and frontmatter summary

Update "five key concepts" to "six" and add "delegation" to the
frontmatter summary field, addressing Greptile review comments.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-03-26 23:01:58 -07:00
Matt Van Horn
eb8c5d93e7 test(server): add negative test for x-forwarded-host mismatch
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>
2026-03-26 16:39:46 -07:00
Dotta
af5b980362 Merge pull request #1857 from paperclipai/PAP-878-create-a-mine-tab-in-inbox
Add a Mine tab and archive flow to inbox
2026-03-26 16:21:47 -05:00
dotta
2e563ccd50 Move unread/archive column to the left for non-issue inbox items
Repositions the unread dot and archive X button to the leading
(left) side of approval, failed run, and join request rows,
matching the visual alignment of IssueRow where the unread slot
appears first due to CSS flex ordering.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 16:09:43 -05:00
dotta
2c406d3b8c Extend read/dismissed functionality to all inbox item types
Approvals, failed runs, and join requests now have the same
unread dot + archive X pattern as issues in the Mine tab:
- Click the blue dot to mark as read, then X appears on hover
- Desktop: animated dismiss with scale/slide transition
- Mobile: swipe-to-archive via SwipeToArchive wrapper
- Dismissed items are filtered out of Mine tab
- Badge count excludes dismissed approvals and join requests
- localStorage-backed read/dismiss state for non-issue items

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 16:09:43 -05:00
dotta
49c7fb7fbd Unify unread badge and archive X into single column on Mine tab
The unread dot and dismiss X now share the same rightmost column on
the Mine tab.  When an issue is unread the blue dot shows first;
clicking it marks the issue as read and reveals the X on hover for
archiving.  Read/unread state stays in sync across all inbox tabs.
Desktop dismiss animation polished with scale + slide.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 16:09:43 -05:00
dotta
995f5b0b66 Add the inbox mine tab and archive flow
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 16:09:43 -05:00
Dotta
b34fa3b273 Merge pull request #1834 from paperclipai/fix/project-description-mentions
fix: improve embedded Postgres bootstrap and worktree init
2026-03-26 12:43:22 -05:00
dotta
9ddf960312 Harden dev-watch excludes for nested UI outputs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 12:35:19 -05:00
dotta
a8894799e4 Align worktree provision with worktree init
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 12:35:19 -05:00
dotta
76a692c260 Improve embedded Postgres bootstrap errors
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 12:35:19 -05:00
Dotta
5913706329 Merge pull request #1830 from paperclipai/pr/pap-891-ui-polish
ui: polish mentions, issue workspace details, and issue search
2026-03-26 12:02:28 -05:00
Dotta
b944293eda Merge pull request #1829 from paperclipai/pr/pap-891-worktree-reliability
fix(worktree): harden provisioned worktree isolation and test fallback behavior
2026-03-26 12:01:47 -05:00
dotta
3c1ebed539 test(worktree): address embedded postgres helper review feedback
- 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>
2026-03-26 11:56:05 -05:00
dotta
ab0d04ff7a fix(ui): address workspace card review feedback
- restore pre-run workspace configuration visibility
- require explicit save/cancel for workspace edits
- stabilize debounced issue search callback

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 11:53:25 -05:00
Dotta
6073ac3145 Merge pull request #1827 from paperclipai/pr/pap-891-docs-refresh
docs: refresh adapter/runtime docs and deprecate bootstrapPromptTemplate
2026-03-26 11:46:44 -05:00
Dotta
3b329467eb Merge pull request #1828 from paperclipai/pr/pap-891-release-automation-followups
chore(release): publish @paperclipai/ui from release automation
2026-03-26 11:46:10 -05:00
Dotta
aa5b2be907 Merge pull request #1831 from paperclipai/pr/pap-891-opencode-headless-prompts
fix(opencode): support headless permission prompt configuration
2026-03-26 11:43:01 -05:00
Dotta
dcb66eeae7 Merge pull request #1812 from paperclipai/docs/maintenance-20260326-public
docs: documentation accuracy update 2026-03-26
2026-03-26 11:17:43 -05:00
dotta
874fe5ec7d Publish @paperclipai/ui from release automation
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 11:13:11 -05:00
dotta
c916626cef test: skip embedded postgres suites when initdb is unavailable
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 11:12:39 -05:00
dotta
555f026c24 Avoid sibling worktree port collisions
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 11:12:39 -05:00
dotta
e91da556ee updated reamde 2026-03-26 11:12:39 -05:00
dotta
ab82e3f022 Fix worktree runtime isolation recovery
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 11:12:39 -05:00
dotta
c74cda1851 Fix worktree provision isolation
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 11:12:39 -05:00
dotta
fcf3ba6974 Seed Paperclip env in provisioned worktrees
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 11:12:39 -05:00
dotta
ed62d58cb2 Fix headless OpenCode permission prompts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 11:12:35 -05:00
dotta
dd8c1ca3b2 Speed up issues page search responsiveness
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 11:12:29 -05:00
dotta
5ee4cd98e8 feat: move workspace info from properties panel to issue main pane
Display workspace branch, path, and status in a card on the issue main pane
instead of in the properties sidebar. Only shown for non-default (isolated)
workspaces. Edit controls are hidden behind an Edit toggle button.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 11:12:29 -05:00
dotta
a6ca3a9418 fix: enable @-mention autocomplete in new project description editor
The MarkdownEditor in NewProjectDialog was not receiving mention options,
so typing @ in the description field did nothing. Added agents query and
mentionOptions prop to match how NewIssueDialog handles mentions.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 11:12:29 -05:00
dotta
0fd75aa579 fix: render mention autocomplete via portal to prevent overflow clipping
The mention suggestion dropdown was getting clipped when typing at the
end of a long description inside modals/dialogs because parent containers
had overflow-y-auto. Render it via createPortal to document.body with
fixed positioning and z-index 9999 so it always appears above all UI.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 11:12:29 -05:00
dotta
eaa765118f chore: mark bootstrapPromptTemplate as deprecated
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>
2026-03-26 11:12:25 -05:00
dotta
ed73547fb6 docs: update SPEC work artifacts and deprecate bootstrapPromptTemplate
- SPEC: reflect that Paperclip now manages task-linked documents and
  attachments (issue documents, file attachments) instead of claiming
  it does not manage work artifacts
- agents-runtime: remove bootstrapPromptTemplate from recommended config,
  add deprecation notice, update minimal setup checklist

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 11:12:25 -05:00
dotta
692105e202 docs: update adapter list and repo map accuracy
- Add missing adapters (opencode_local, hermes_local, cursor, pi_local,
  openclaw_gateway) to agents-runtime.md
- Document bootstrapPromptTemplate in prompt templates section
- Update AGENTS.md repo map with packages/adapters, adapter-utils, plugins
- Fix troubleshooting section to reference all local CLI adapters

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 11:12:25 -05:00
dotta
01b550d61a docs: fix SPEC accuracy for adapters and backend
- align adapter list with current built-in adapters
- update backend framework references to Express
- remove outdated V1 not-supported template export claim
- clarify work artifact boundaries with issue documents

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 11:12:25 -05:00
Devin Foley
c6364149b1 Add delegation instructions to default CEO agent prompt (#1796)
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.
2026-03-26 08:11:22 -07:00
dotta
b0b9809732 Add issue document revision restore flow
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 08:24:57 -05:00
dotta
844b6dfd70 docs: update SPEC work artifacts and deprecate bootstrapPromptTemplate
- SPEC: reflect that Paperclip now manages task-linked documents and
  attachments (issue documents, file attachments) instead of claiming
  it does not manage work artifacts
- agents-runtime: remove bootstrapPromptTemplate from recommended config,
  add deprecation notice, update minimal setup checklist

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 07:23:09 -05:00
dotta
0a32e3838a fix: render mention autocomplete via portal to prevent overflow clipping
The mention suggestion dropdown was getting clipped when typing at the
end of a long description inside modals/dialogs because parent containers
had overflow-y-auto. Render it via createPortal to document.body with
fixed positioning and z-index 9999 so it always appears above all UI.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 07:22:24 -05:00
dotta
e186449f94 docs: update adapter list and repo map accuracy
- Add missing adapters (opencode_local, hermes_local, cursor, pi_local,
  openclaw_gateway) to agents-runtime.md
- Document bootstrapPromptTemplate in prompt templates section
- Update AGENTS.md repo map with packages/adapters, adapter-utils, plugins
- Fix troubleshooting section to reference all local CLI adapters

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 07:08:45 -05:00
dotta
4bb42005ea docs: fix SPEC accuracy for adapters and backend
- align adapter list with current built-in adapters
- update backend framework references to Express
- remove outdated V1 not-supported template export claim
- clarify work artifact boundaries with issue documents

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 07:08:45 -05:00
Dotta
66aa65f8f7 Merge pull request #1787 from HenkDz/fix/pi-adapter-models-stderr
fix(pi-local): parse models from stderr
2026-03-26 07:04:38 -05:00
HenkDz
15f6079c6b Fix Pi adapter execution and improve transcript parsing
- Changed from RPC mode to JSON print mode (--mode json -p)
- Added prompt as CLI argument instead of stdin RPC command
- Rewrote transcript parser to properly handle Pi's JSONL output
- Added toolUseId to tool_call entries for proper matching with tool_result
- Filter out RPC protocol messages from transcript
- Extract thinking blocks and usage statistics
2026-03-26 10:59:58 +01:00
Devin Foley
9e9eec9af6 ci: validate Dockerfile deps stage in PR policy (#1799)
* ci: add Dockerfile deps stage validation to PR policy

Checks that all workspace package.json files and the patches/
directory are copied into the Dockerfile deps stage. Prevents the
Docker build from breaking when new packages or patches are added
without updating the Dockerfile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: scope Dockerfile check to deps stage and derive workspace roots

Address Greptile review feedback:
- Use awk to extract only the deps stage before grepping, preventing
  false positives from COPY lines in other stages
- Derive workspace search roots from pnpm-workspace.yaml instead of
  hardcoding them, so new top-level workspaces are automatically covered

* ci: guard against empty workspace roots in Dockerfile check

Fail early if pnpm-workspace.yaml parsing yields no search roots,
preventing a silent false-pass from find defaulting to cwd.

* ci: guard against empty deps stage extraction

Fail early with a clear error if awk cannot find the deps stage in the
Dockerfile, instead of producing misleading "missing COPY" errors.

* ci: deduplicate find results from overlapping workspace roots

Use sort -u instead of sort to prevent duplicate error messages when
nested workspace globs (e.g. packages/* and packages/adapters/*) cause
the same package.json to be found twice.

* ci: anchor grep to ^COPY to ignore commented-out Dockerfile lines

Prevents false negatives when a COPY directive is commented out
(e.g. # COPY packages/foo/package.json).

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 22:42:16 -07:00
Devin Foley
1a4ed8c953 Merge pull request #1794 from paperclipai/fix/cursor-native-auth-check
fix(cursor): check native auth before warning about missing API key
2026-03-25 22:00:37 -07:00
Devin Foley
bd60ea4909 refactor: use async fs.readFile in readCursorAuthInfo for consistency
Match the async pattern used by readCodexAuthInfo in the Codex adapter.
2026-03-25 21:52:38 -07:00
Devin Foley
6ebfc0ff3d Merge pull request #1782 from paperclipai/fix/codex-skill-injection-location
fix(codex): inject skills into ~/.codex/skills/ instead of workspace
2026-03-25 21:44:58 -07:00
Devin Foley
083d7c9ac4 fix(cursor): check native auth before warning about missing API key
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.
2026-03-25 20:54:16 -07:00
Devin Foley
80766e589c Clarify docs: skills go to the effective CODEX_HOME, not ~/.codex
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>
2026-03-25 20:46:05 -07:00
Devin Foley
c5c6c62bd7 Merge pull request #1786 from paperclipai/fix/opencode-disable-project-config
fix(opencode): prevent opencode.json pollution in workspace
2026-03-25 20:38:11 -07:00
Devin Foley
1549799c1e Move OPENCODE_DISABLE_PROJECT_CONFIG after envConfig loop
Setting the env var before the user-config loop meant adapter env
overrides could disable the guard.  Move it after the loop so it
always wins, matching the pattern already used in test.ts and
models.ts.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-25 20:29:48 -07:00
HenkDz
af1b08fdf4 fix(pi-local): parse models from stderr
Pi outputs the model list to stderr instead of stdout. This fix checks
stderr first and falls back to stdout for compatibility with older
versions.

Fixes model discovery returning empty arrays and environment tests
failing with 'Pi returned no models' error.
2026-03-26 01:30:54 +01:00
Devin Foley
72bc4ab403 fix(opencode): prevent opencode.json config pollution in workspace
Set OPENCODE_DISABLE_PROJECT_CONFIG=true in all OpenCode invocations
(execute, model discovery, environment test) to stop the OpenCode CLI
from writing an opencode.json file into the project working directory.
Model selection is already passed via the --model CLI flag.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-25 17:22:49 -07:00
Devin Foley
4c6b9c190b Fix stale reference to resolveCodexSkillsHome in fallback path
The default fallback in ensureCodexSkillsInjected still referenced the
old function name. Updated to use resolveCodexSkillsDir with shared
home as fallback.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-25 16:09:09 -07:00
Devin Foley
f6ac6e47c4 Clarify docs: skills go to $CODEX_HOME/skills/, defaulting to ~/.codex
Addresses Greptile P2 review comment.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-25 16:05:57 -07:00
Devin Foley
623ab1c3ea Fix skill injection to use effective CODEX_HOME, not shared home
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>
2026-03-25 16:04:53 -07:00
Devin Foley
eeec52ad74 Fix Codex skill injection to use ~/.codex/skills/ instead of cwd
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>
2026-03-25 15:55:51 -07:00
Dotta
db3883d2e7 Merge pull request #1748 from paperclipai/pr/pap-849-release-changelog
docs(release): add v2026.325.0 changelog
2026-03-25 07:53:38 -05:00
dotta
9637351880 docs(release): add v2026.325.0 changelog
Summarizes the v2026.325.0 release with highlights, fixes, upgrade notes, and contributor credits.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-25 07:42:38 -05:00
Matt Van Horn
d0e01d2863 fix(server): include x-forwarded-host in board mutation origin check
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
2026-03-25 00:06:43 -07:00
Devin Foley
cbca599625 Merge pull request #1363 from amit221/fix/issue-1255
fix(issues): normalize HTML entities in @mention tokens before agent lookup
2026-03-24 20:19:47 -07:00
Devin Foley
b1d12d2f37 Merge pull request #1730 from paperclipai/fix/docker-patches
fix(docker): copy patches directory into deps stage
2026-03-24 16:13:24 -07:00
Devin Foley
0a952dc93d fix(docker): copy patches directory into deps stage
pnpm install needs the patches/ directory to resolve patched
dependencies (embedded-postgres). Without it, --frozen-lockfile
fails with ENOENT on the patch file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 15:59:36 -07:00
Devin Foley
ff8b839f42 Merge pull request #1658 from paperclipai/fix/migration-auto-apply-precedence
fix(server): check MIGRATION_AUTO_APPLY before MIGRATION_PROMPT
2026-03-24 15:56:00 -07:00
Devin Foley
fea892c8b3 Merge pull request #1702 from paperclipai/fix/codex-auth-check
fix(codex): check native auth before warning about missing API key
2026-03-24 15:40:20 -07:00
Devin Foley
1696ff0c3f fix(codex): use path.join for auth detail message path
Use path.join instead of string concatenation for the auth.json
fallback path in the detail message, ensuring correct path
separators on Windows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 15:27:32 -07:00
Devin Foley
4eecd23ea3 fix(codex): use codexHomeDir() fallback for accurate auth detail path
When adapter config has no CODEX_HOME but process.env.CODEX_HOME is
set, readCodexAuthInfo reads from the process env path. The detail
message now uses codexHomeDir() instead of hardcoded "~/.codex" so
the displayed path always matches where credentials were read from.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 12:46:16 -07:00
Devin Foley
4da83296a9 test(codex): move OPENAI_API_KEY stub to beforeEach for all tests
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>
2026-03-24 12:33:11 -07:00
Devin Foley
0ce4134ce1 fix(codex): use actual CODEX_HOME in auth detail message
Show the configured CODEX_HOME path instead of hardcoded ~/.codex
when the email fallback message is displayed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 10:54:05 -07:00
Dotta
03f44d0089 Merge pull request #1708 from paperclipai/pr/pap-817-onboarding-goal-context
Seed onboarding project and issue goal context
2026-03-24 12:38:19 -05:00
Dotta
d38d5e1a7b Merge pull request #1710 from paperclipai/pr/pap-817-agent-mention-pill-alignment
Fix agent mention pill vertical misalignment with project mention pill
2026-03-24 12:33:51 -05:00
Dotta
add6ca5648 Merge pull request #1709 from paperclipai/pr/pap-817-join-request-task-assignment-grants
Preserve task assignment grants for joined agents
2026-03-24 12:33:40 -05:00
github-actions[bot]
04a07080af chore(lockfile): refresh pnpm-lock.yaml (#1712)
Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
2026-03-24 17:33:24 +00:00
Dotta
8bebc9599a Merge pull request #1707 from paperclipai/pr/pap-817-embedded-postgres-docker-initdb
Fix embedded Postgres initdb failure in Docker slim containers
2026-03-24 12:32:57 -05:00
Dotta
6250d536a0 Merge pull request #1706 from paperclipai/pr/pap-817-inline-join-requests-inbox
Render join requests inline in inbox like approvals and other work items
2026-03-24 12:30:43 -05:00
Dotta
de5985bb75 Merge pull request #1705 from paperclipai/pr/pap-817-remove-instructions-log
Remove noisy "Loaded agent instructions file" log from all adapters
2026-03-24 12:30:15 -05:00
Dotta
331e1f0d06 Merge pull request #1704 from paperclipai/pr/pap-817-cli-api-connection-errors
Improve CLI API connection errors
2026-03-24 12:30:06 -05:00
Devin Foley
58c511af9a test(codex): isolate auth tests from host OPENAI_API_KEY
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>
2026-03-24 09:58:40 -07:00
dotta
4b668379bc Regenerate embedded-postgres vendor patch
Rebuild the patch file with valid unified-diff hunks so pnpm can
apply the locale and environment fixes during install.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-24 11:56:41 -05:00
dotta
f352f3f514 Force embedded-postgres messages locale to C
The vendor package still hardcoded LC_MESSAGES to en_US.UTF-8.
That locale is missing in slim containers, and initdb fails during
bootstrap even when --lc-messages=C is passed later.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-24 11:55:39 -05:00
dotta
4ff460de38 Fix embedded-postgres patch env lookup
Use globalThis.process.env in the vendor patch so the spawned child
process config does not trip over the local process binding inside
embedded-postgres.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-24 11:51:45 -05:00
Devin Foley
06b85d62b2 test(codex): add coverage for native auth detection in environment probe
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>
2026-03-24 09:51:44 -07:00
dotta
3447e2087a Fix agent mention pill vertical misalignment with project mention pill
Change vertical-align from baseline to middle on both editor and
read-only mention chip styles. The baseline alignment caused
inconsistent positioning because the agent ::before icon (0.75rem)
and project ::before dot (0.45rem) produced different synthesized
baselines in the inline-flex containers.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-24 11:49:00 -05:00
dotta
44fbf83106 Preserve task assignment grants for joined agents 2026-03-24 11:49:00 -05:00
dotta
eb73fc747a Seed onboarding project and issue goal context
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-24 11:48:59 -05:00
dotta
5602576ae1 Fix embedded Postgres initdb failure in Docker slim containers
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>
2026-03-24 11:48:59 -05:00
dotta
c4838cca6e Render join requests inline in inbox like approvals and other work items
Join requests were displayed in a separate card-style section below the main
inbox list. This moves them into the unified work items feed so they sort
chronologically alongside issues, approvals, and failed runs—matching the
inline treatment hiring requests already receive.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-24 11:48:43 -05:00
dotta
67841a0c6d Remove noisy "Loaded agent instructions file" log from all adapters
Loading an instructions file is normal, expected behavior — not worth
logging to stdout/stderr on every run. Warning logs for failed reads
are preserved.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-24 11:48:43 -05:00
dotta
5561a9c17f Improve CLI API connection errors
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-24 11:48:43 -05:00
Devin Foley
a9dcea023b fix(codex): check native auth before warning about missing API key
The environment test warned about OPENAI_API_KEY being unset even
when Codex was authenticated via `codex auth`. Now checks
~/.codex/auth.json before emitting the warning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 09:47:48 -07:00
amit221
14ffbe30a0 test(issues): shorten mid-token entity test comment
Made-with: Cursor
2026-03-24 15:39:59 +02:00
amit221
98a5e287ef test(issues): document Greptile mid-token case vs old strip behavior
Made-with: Cursor
2026-03-24 15:29:28 +02:00
amit221
2735ef1f4a fix(issues): decode @mention entities without lockfile or new deps
- Drop entities package (CI blocks pnpm-lock.yaml on PRs; reset lockfile to master)
- Restore numeric + allowlisted named entity decoding in issues.ts
- Split Greptile mid-token &amp; case into its own test with review comment

Made-with: Cursor
2026-03-24 15:22:21 +02:00
amit221
53f0988006 Merge origin/master into fix/issue-1255
- findMentionedAgents: keep normalizeAgentMentionToken + extractAgentMentionIds
- decode @mention tokens with entities.decodeHTMLStrict (full HTML entities)
- Add entities dependency; expand unit tests for Greptile follow-ups

Made-with: Cursor
2026-03-24 10:03:15 +02:00
amit221
730a67bb20 fix(issues): decode HTML entities in @mention tokens instead of stripping
Addresses Greptile review on PR #1363: numeric entities decode via
code points; named entities use a small allowlist (amp, nbsp, etc.)
so M&amp;M resolves correctly; unknown named entities are preserved.

Adds mid-token tests for &amp; in agent names.

Made-with: Cursor
2026-03-24 09:40:55 +02:00
Devin Foley
59e29afab5 Merge pull request #1672 from paperclipai/fix/docker-plugin-sdk
fix(docker): add plugin-sdk to Dockerfile build
2026-03-23 20:05:56 -07:00
Devin Foley
fd4df4db48 fix(docker): add plugin-sdk to Dockerfile build
The plugin framework landed without updating the Dockerfile. The
server now imports @paperclipai/plugin-sdk, so the deps stage needs
its package.json for install and the build stage needs to compile
it before building the server.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 19:58:59 -07:00
Devin Foley
8ae954bb8f Merge pull request #1666 from paperclipai/chore/pr-template
chore: add GitHub PR template
2026-03-23 19:53:45 -07:00
Dotta
32c76e0012 Merge pull request #1670 from paperclipai/pr/pap-803-mention-aware-link-node
Extract mention-aware link node helper and add tests
2026-03-23 21:33:49 -05:00
Dotta
70bd55a00f Merge pull request #1669 from paperclipai/pr/pap-803-agent-instructions-tab-reset
Fix instructions tab state on agent switch
2026-03-23 21:27:51 -05:00
Dotta
f92d2c3326 Merge pull request #1668 from paperclipai/pr/pap-803-imported-agent-frontmatter
Fix imported agent bundle frontmatter leakage
2026-03-23 21:27:42 -05:00
dotta
a3f4e6f56c Preserve prompts panel width on agent switch
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 20:58:19 -05:00
dotta
08bdc3d28e Handle nested imported AGENTS edge case
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 20:56:34 -05:00
dotta
7c54b6e9e3 Extract mention-aware link node helper and add tests
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 20:46:19 -05:00
dotta
a346ad2a73 Fix instructions tab state on agent switch
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 20:45:11 -05:00
dotta
e4e5b61596 Fix imported agent bundle frontmatter leakage
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 20:43:20 -05:00
Dotta
eeb7e1a91a Merge pull request #1655 from paperclipai/pr/pap-795-company-portability
feat(portability): improve company import and export flow
2026-03-23 19:45:05 -05:00
Dotta
f2637e6972 Merge pull request #1654 from paperclipai/pr/pap-795-agent-runtime
fix(runtime): improve agent recovery and heartbeat operations
2026-03-23 19:44:51 -05:00
dotta
c8f8f6752f fix: address latest Greptile runtime review 2026-03-23 19:43:50 -05:00
dotta
87b3cacc8f Address valid Greptile portability follow-ups 2026-03-23 19:42:58 -05:00
github-actions[bot]
4096db8053 chore(lockfile): refresh pnpm-lock.yaml (#1667)
Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
2026-03-24 00:29:19 +00:00
Dotta
fa084e1a16 Merge pull request #1653 from paperclipai/pr/pap-795-ui-polish
fix(ui): polish issue and agent surfaces
2026-03-23 19:28:50 -05:00
dotta
22067c7d1d revert: drop PR workflow lockfile refresh 2026-03-23 19:26:33 -05:00
dotta
85d2c54d53 fix(ci): refresh lockfile in PR jobs 2026-03-23 19:23:10 -05:00
Devin Foley
5222a49cc3 chore: expand thinking path placeholder for depth
Address Greptile feedback — the sparse 3-line placeholder could
lead to shallow thinking paths. Expanded to 6 lines with guiding
brackets and added "Aim for 5–8 steps" hint in the comment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 17:12:07 -07:00
Devin Foley
36574bd9c6 chore: add GitHub PR template
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 17:08:04 -07:00
dotta
2cc2d4420d Remove lockfile changes from UI polish PR
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 19:03:50 -05:00
Dotta
7576c5ecbc Update ui/src/pages/Auth.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-23 19:00:34 -05:00
Devin Foley
dd1d9bed80 fix(server): check MIGRATION_AUTO_APPLY before MIGRATION_PROMPT
PAPERCLIP_MIGRATION_PROMPT=never was checked before
PAPERCLIP_MIGRATION_AUTO_APPLY=true, causing auto-apply to never
trigger when both env vars are set (as in dev:watch). Swap the
check order so AUTO_APPLY takes precedence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:14:14 -07:00
dotta
92c29f27c3 Address Greptile review on portability PR
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 17:23:59 -05:00
dotta
55b26ed590 Address Greptile review on agent runtime PR
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 17:18:17 -05:00
dotta
6960ab1106 Address Greptile review on UI polish PR
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 17:16:10 -05:00
dotta
c3f4e18a5e Keep sidebar ordering with portability branch
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 17:02:43 -05:00
dotta
a3f568dec7 Improve generated company org chart assets
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:58:07 -05:00
dotta
6f1ce3bd60 Document imported heartbeat defaults
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:58:07 -05:00
dotta
159c5b4360 Preserve sidebar order in company portability
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:59 -05:00
dotta
b5fde733b0 Open imported company after import
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:39 -05:00
dotta
f9927bdaaa Disable imported timer heartbeats
Prevent company imports from re-enabling scheduler heartbeats on imported agents and cover both new-company and existing-company import flows in portability tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:39 -05:00
dotta
dcead97650 Fix company zip imports
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:39 -05:00
dotta
9786ebb7ba Revert "Add companies.sh import wrapper"
This reverts commit 17876ec1dc65a9150488874d79fc2fcc087c13ae.
2026-03-23 16:57:39 -05:00
dotta
66d84ccfa3 Add companies.sh import wrapper
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:38 -05:00
dotta
56a39fea3d Add importing & exporting company guide
Documents the `paperclipai company export` and `paperclipai company import`
CLI commands, covering package format, all options, target modes, collision
strategies, GitHub sources, interactive selection, and API endpoints.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:57:38 -05:00
dotta
2a6e1cf1fc Fix imported GitHub skill file paths
Normalize GitHub skill directories for blob/file imports and when reading legacy stored metadata so imported SKILL.md files resolve correctly.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:38 -05:00
dotta
c02dc73d3c Confirm company imports after preview
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:38 -05:00
dotta
06f5632d1a Polish import adapter defaults
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:38 -05:00
dotta
1246ccf250 Add nested import picker
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:38 -05:00
dotta
a339b488ae fix: dedupe company skill inventory refreshes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:38 -05:00
dotta
ac376d0e5e Add TUI import summaries
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:38 -05:00
dotta
220946b2a1 Default recurring task exports to checked
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:38 -05:00
dotta
c41dd2e393 Reduce portability warning fan-out
Infer portable repo metadata from local git workspaces when repoUrl is missing, and collapse repeated task workspace export warnings into a single summary per missing workspace.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:38 -05:00
dotta
2e76a2a554 Add routine support to recurring task portability
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:38 -05:00
dotta
8fa4b6a5fb added a script to generate company assets 2026-03-23 16:57:38 -05:00
dotta
d8b408625e fix providers 2026-03-23 16:57:38 -05:00
dotta
19154d0fec Clarify Codex instruction sources
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:33 -05:00
dotta
c0c1fd17cb Add "Disable All" button to heartbeats settings page
Adds a destructive-variant button at the top of the heartbeats page that
disables timer heartbeats for all agents at once. The button only appears
when at least one agent has heartbeats enabled, and shows a loading state
while processing.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:57:33 -05:00
dotta
2daae758b1 Include all agents on heartbeats page regardless of interval config
Agents without a heartbeat interval configured (intervalSec=0) were
filtered out, making them invisible on the instance heartbeats page.
This prevented managing heartbeats for agents that hadn't been
configured yet (e.g. donchitos company agents).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:33 -05:00
dotta
43b21c6033 Ignore .paperclip paths in restart tracking
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:33 -05:00
dotta
0bb1ee3caa Recover agent instructions from disk
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:33 -05:00
dotta
3b2cb3a699 Show all companies' agents on instance heartbeats page
The /instance/scheduler-heartbeats endpoint was filtering agents by the
requesting user's company memberships, which meant non-member companies
(like donchitos) were hidden. Since this is an instance-level admin page,
it should show all agents across all companies.

- Added assertInstanceAdmin to authz.ts for reuse
- Replaced assertBoard + company filter with assertInstanceAdmin
- Removed the companyIds-based WHERE clause since instance admins see all

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:33 -05:00
dotta
1adfd30b3b fix: recover managed agent instructions from disk
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:33 -05:00
dotta
a315838d43 fix: preserve agent instructions on adapter switch
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:33 -05:00
dotta
75c7eb3868 Ignore test-only paths in dev restart tracking
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:33 -05:00
dotta
eac3f3fa69 Honor explicit failed-run session resume
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:33 -05:00
dotta
02c779b41d Use issue participation for agent history
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:33 -05:00
dotta
5a1e17f27f Fix issue workspace reuse after isolation
Persist realized isolated/operator workspaces back onto the issue as reusable workspaces so later runs stay on the same workspace, and update the issue workspace picker to present realized isolated workspaces as existing workspaces.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:33 -05:00
dotta
e0d2c4bddf Ignore .paperclip in dev restart detection
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:33 -05:00
dotta
d73c8df895 fix: improve pill contrast by using WCAG contrast ratios on composited backgrounds
Pills with semi-transparent backgrounds were using raw color luminance to pick
text color, ignoring the page background showing through. This caused unreadable
text on dark themes for mid-luminance colors like orange. Now composites the
rgba background over the actual page bg (dark/light) before computing WCAG
contrast ratios, and centralizes the logic in a shared color-contrast utility.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:57:27 -05:00
dotta
e73bc81a73 fix: prevent documents row from causing horizontal scroll on mobile
- Shorten button labels ("New document" → "New", "Upload attachment" → "Upload") on small screens
- Add min-w-0 and shrink-0 to flex containers and items to prevent overflow
- Truncate document revision text on narrow viewports
- Mark chevron, key badge, and action buttons as shrink-0

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:27 -05:00
dotta
0b960b0739 Suppress same-page issue toasts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:27 -05:00
dotta
bdecb1bad2 Sort agents alphabetically by name in all views
Sort the flat list view, org tree view, and sidebar agents list
alphabetically by name using localeCompare.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:27 -05:00
dotta
e61f00d4c1 Add missing data-slot="toggle" to Routines toggle buttons
The initial mobile toggle fix (afc3d7ec) missed 2 toggle switches in
RoutineDetail.tsx and Routines.tsx. Without the attribute, these toggles
would still inflate to 44px on touch devices via the @media (pointer: coarse)
rule.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:27 -05:00
dotta
42c8d9b660 Fix oversized toggle switches on mobile
The global @media (pointer: coarse) rule was forcing min-height: 44px on
toggle button elements, inflating them from 20px to 44px. Added
data-slot="toggle" to all 10 toggle buttons and a CSS override to reset
their min-height, keeping the parent row as the touch target.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:27 -05:00
dotta
bd0b76072b Fix atomic markdown mention deletion
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:27 -05:00
dotta
db42adf1bf Make agent instructions tab responsive on mobile
On mobile, the two-panel file browser + editor layout now stacks
vertically with a toggleable file panel. The draggable separator is
hidden, and selecting a file auto-closes the panel to maximize
editor space.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:57:27 -05:00
dotta
0e8e162cd5 Fix mention pills by allowing custom URL schemes in Lexical LinkNode
The previous fix (validateUrl on linkPlugin) only affected the link dialog,
not the markdown-to-Lexical import path. Lexical's LinkNode.sanitizeUrl()
converts agent:// and project:// URLs to about:blank because they aren't
in its allowlist. Override the prototype method to preserve these schemes
so mention chips render correctly.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:27 -05:00
dotta
49ace2faf9 Allow custom markdown mention links in editor
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:27 -05:00
dotta
8232456ce8 Fix markdown mention chips
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:27 -05:00
dotta
cd7c6ee751 Fix login form not being detected by 1Password
Add name, id, and htmlFor attributes to form inputs and a method/action
to the form element so password managers can properly identify the login
form fields.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:57:27 -05:00
dotta
f8dd4dcb30 Reduce monospace font size from 1.1em to 1em
1.1em was too large per feedback — settle on 1em for all markdown
monospace contexts.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:57:27 -05:00
dotta
0b9f00346b Increase monospace font size and add dark mode background for inline code
Bump monospace font-size from 0.78em to 1.1em across all markdown
contexts (editor, code blocks, inline code). Add subtle gray
background (#ffffff0f) for inline code in dark mode.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:57:27 -05:00
dotta
ef0846e723 Remove priority icon from issue rows across the app
Priority is still supported as a feature (editable in issue properties,
used in filters), but no longer shown prominently in every issue row.
Affects inbox, issues list, my issues, and dashboard pages.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 16:57:27 -05:00
Dotta
3a79d94050 Merge pull request #1380 from DanielSousa/feature/change-reports-to
feat(ui): edit and hire with Reports to picker
2026-03-23 15:08:54 -05:00
Dotta
b5610f66a6 Merge pull request #1382 from lucas-stellet/fix/pi-local-missing-from-isLocal
fix: add pi_local to remaining isLocal guards in UI
2026-03-23 15:06:20 -05:00
Dotta
119dd0eaa0 Merge pull request #542 from albttx/dockerize
chore(ci): deploy docker image
2026-03-23 15:03:36 -05:00
Dotta
080c9e415d Merge pull request #1635 from paperclipai/pr/pap-768-board-cli-auth
Add browser-based board CLI auth flow
2026-03-23 08:47:32 -05:00
dotta
7f9a76411a Address Greptile review on board CLI auth
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 08:46:05 -05:00
dotta
01b6b7e66a fix: make cli auth migration 0044 idempotent
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 08:46:05 -05:00
dotta
298713fae7 Fix duplicate auth login company flag
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 08:46:05 -05:00
dotta
37c2c4acc4 Add browser-based board CLI auth flow
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 08:46:05 -05:00
Dotta
1376fc8f44 Merge pull request #1631 from paperclipai/pr/pap-768-company-import-safe-imports
Improve company import CLI flows and safe existing-company routes
2026-03-23 08:25:33 -05:00
Dotta
e6801123ca Merge pull request #1632 from paperclipai/pr/pap-768-merge-history
Add merge-history project import option
2026-03-23 08:22:27 -05:00
dotta
f23d611d0c Route existing-company CLI imports through safe routes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 08:14:51 -05:00
dotta
5dfdbe91bb Add merge-history project import option
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 08:14:51 -05:00
dotta
e6df9fa078 Support GitHub shorthand refs for company import
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 08:14:51 -05:00
dotta
5a73556871 Use positional source arg for company import
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 08:14:51 -05:00
dotta
e204e03fa6 Add CLI company import export e2e test
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 08:14:51 -05:00
Dotta
8b4850aaea Merge pull request #1622 from aronprins/docs/api-routines-and-fixes
docs(api): add Routines reference and fix two documentation bugs
2026-03-23 05:59:37 -05:00
Aron Prins
f87db64ba9 docs(api/routines): address three review findings
**#1 — Missing `description` field in fields table**
The create body example included `description` and the schema confirms
`description: z.string().optional().nullable()`, but the reference table
omitted it. Added as an optional field.

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

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

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 10:21:34 +01:00
Aron Prins
f42aebdff8 docs(api): add Routines reference
Routines are recurring tasks that fire on a schedule, webhook, or API
call and create a heartbeat run for the assigned agent. Document the
full CRUD surface including:

- List / get routines
- Create with concurrency and catch-up policy options
- Add schedule, webhook, and api triggers
- Update / delete triggers, rotate webhook secrets
- Manual run and public trigger fire
- List run history
- Agent access rules (agents can only manage own routines)
- Routine lifecycle (active → paused → archived)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 09:58:27 +01:00
Aron Prins
4ebc12ab5a docs(api): fix goal status value and document checkout re-claim pattern
- Fix goals-and-projects.md: `completed` is not a valid status — correct to
  `achieved` and document all valid values (planned/active/achieved/cancelled)
- Fix issues.md: document that `expectedStatuses: ["in_progress"]` can be used
  to re-claim a stale lock after a crashed run; clarify that `runId` in the
  request body is not accepted (run ID comes from X-Paperclip-Run-Id header only)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 09:58:20 +01:00
Dotta
fdb20d5d08 Merge pull request #550 from mvanhorn/osc/529-fix-missing-agents-md-fallback
fix: graceful fallback when AGENTS.md is missing in claude-local adapter
2026-03-21 11:17:35 -05:00
Dotta
5bf6fd1270 Merge pull request #551 from mvanhorn/osc/272-fix-comment-image-attachments
fix: embed uploaded images inline in comments via paperclip button
2026-03-21 11:16:49 -05:00
Dotta
e3e7a92c77 Merge pull request #552 from mvanhorn/osc/129-feat-filter-issues-by-project
feat(ui): add project filter to issues list
2026-03-21 11:15:09 -05:00
Dotta
640f527f8c Merge pull request #832 from mvanhorn/feat/evals-promptfoo-bootstrap
feat(evals): bootstrap promptfoo eval framework (Phase 0)
2026-03-21 07:28:59 -05:00
Dotta
49c1b8c2d8 Merge branch 'master' into feat/evals-promptfoo-bootstrap 2026-03-21 07:28:51 -05:00
Devin Foley
93ba78362d Merge pull request #1331 from paperclipai/ci/consolidate-pr-workflows
ci: consolidate PR workflows into a single file
2026-03-20 18:09:19 -07:00
Devin Foley
2fdf953229 ci: consolidate PR workflows into a single file
Merge pr-verify.yml, pr-policy.yml, and pr-e2e.yml into a single
pr.yml with three parallel jobs (policy, verify, e2e). Benefits:

- Single concurrency group cancels all jobs on new push
- Consistent Node 24 across all jobs
- One file to maintain instead of three

The jobs still run independently (no artifact sharing) since pnpm
cache makes install fast and the upload/download overhead for
node_modules would negate the savings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 17:11:36 -07:00
Devin Foley
ebe00359d1 Merge pull request #1316 from paperclipai/fix/lockfile-refresh-automerge-guard
fix(ci): skip auto-merge step when lockfile is unchanged
2026-03-20 16:58:03 -07:00
Devin Foley
036e2b52db Merge pull request #1326 from paperclipai/ci/pr-e2e-tests
ci: run e2e tests on PRs
2026-03-20 15:47:21 -07:00
Dotta
f4803291b8 Merge pull request #1385 from paperclipai/fix/worktree-merge-history-migrations
fix: renumber worktree merge history migrations
2026-03-20 17:26:58 -05:00
dotta
d47ec56eca fix: renumber worktree merge history migrations 2026-03-20 17:23:45 -05:00
Dotta
ae6aac044d Merge pull request #1384 from paperclipai/fix/codex-managed-home-followups
fix: restore post-merge route verification
2026-03-20 17:12:52 -05:00
dotta
da2c15905a fix: restore post-merge route verification 2026-03-20 17:09:57 -05:00
Dotta
13ca33aa4e Merge pull request #1383 from paperclipai/fix/codex-managed-home-followups
Improve worktree merge/import followups
2026-03-20 17:08:44 -05:00
Lucas Stellet
e37e9df0d1 fix: address greptile review — add pi_local to effectiveAdapterCommand and adapterLabels
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 19:01:52 -03:00
dotta
54b99d5096 Search sibling storage roots for attachments
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 17:01:52 -05:00
dotta
fb63d61ae5 Skip missing worktree attachment objects
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 17:01:52 -05:00
dotta
73ada45037 Import worktree documents and attachments
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 17:01:52 -05:00
dotta
be911754c5 Default comment reopen to checked
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 17:01:52 -05:00
dotta
cff06c9a54 Add issue titles to worktree merge preview
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 17:01:52 -05:00
dotta
ad011fbf1e Clarify worktree import source and target flags
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 17:01:52 -05:00
dotta
28a5f858b7 Add worktree source discovery commands
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 17:01:52 -05:00
dotta
220a5ec5dd Add project mapping prompts for worktree imports
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 17:01:52 -05:00
dotta
0ec79d4295 Add worktree history merge command
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 17:01:52 -05:00
Lucas Stellet
5e414ff4df fix: add pi_local to isLocalAdapter and ENABLED_INVITE_ADAPTERS guards
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 18:57:17 -03:00
Dotta
a46dc4634b Merge pull request #1351 from paperclipai/paperclip-routines
WIP: routines management, triggers, and execution flow
2026-03-20 16:53:06 -05:00
dotta
df64530333 test: add routines api end-to-end coverage 2026-03-20 16:50:11 -05:00
dotta
8dc98db717 fix: close remaining routine merge blockers 2026-03-20 16:40:27 -05:00
dotta
9093cfbe4f fix: address greptile routine review 2026-03-20 16:26:29 -05:00
Devin Foley
da9b31e393 fix(ci): use --frozen-lockfile in e2e workflow
Align with e2e.yml and ensure CI tests exactly the committed
dependency tree. The pr-policy job already blocks lockfile changes
in PRs, so frozen-lockfile is safe here.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 14:20:46 -07:00
dotta
99eb317600 fix: harden routine dispatch and permissions 2026-03-20 16:15:32 -05:00
Devin Foley
652fa8223e fix: invert reuseExistingServer and remove CI="" workaround
The playwright.config.ts had `reuseExistingServer: !!process.env.CI`
which meant CI would reuse (expect) an existing server while local
dev would start one. This is backwards — in CI Playwright should
manage the server, and in local dev you likely already have one
running.

Flip to `!process.env.CI` and remove the `CI: ""` env override
from the workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 13:49:03 -07:00
Daniel Sousa
4587627f3c refactor: improve layout and truncation in ReportsToPicker
- Enhanced button and text elements to ensure proper overflow handling and truncation for agent names and statuses.
- Adjusted class names for better responsiveness and visual consistency, particularly for unknown and terminated managers.
2026-03-20 20:33:36 +00:00
Daniel Sousa
17b6f6c8f7 fix: enhance ReportsToPicker to handle unknown and terminated managers
- Added handling for cases where the selected manager is terminated, displaying a distinct style and message.
- Introduced a new state for unknown managers, providing user feedback when the saved manager is missing.
- Improved layout for displaying current manager status, ensuring clarity in the UI.
2026-03-20 20:32:03 +00:00
Daniel Sousa
de10269d10 fix: update ReportsToPicker to display terminated status and improve layout
- Modified the display of the current agent's name to include a "(terminated)" suffix if the agent's status is terminated.
- Adjusted button layout to ensure proper text truncation and overflow handling for agent names and roles.
2026-03-20 20:30:07 +00:00
Daniel Sousa
dfb83295de Merge branch 'master' into feature/change-reports-to 2026-03-20 20:13:19 +00:00
Daniel Sousa
61f53b6471 feat: add ReportsToPicker for agent management
- Introduced ReportsToPicker component in AgentConfigForm and NewAgent pages to allow selection of an agent's manager.
- Updated organizational structure documentation to reflect the ability to change an agent's manager post-creation.
- Enhanced error handling in ConfigurationTab to provide user feedback on save failures.
2026-03-20 20:06:19 +00:00
dotta
e3c92a20f1 Merge remote-tracking branch 'public-gh/master' into paperclip-routines
* public-gh/master: (46 commits)
  chore(lockfile): refresh pnpm-lock.yaml (#1377)
  fix: manage codex home per company by default
  Ensure agent home directories exist before use
  Handle directory entries in imported zip archives
  Fix portability import and org chart test blockers
  Fix PR verify failures after merge
  fix: address greptile follow-up feedback
  Address remaining Greptile portability feedback
  docs: clarify quickstart npx usage
  Add guarded dev restart handling
  Fix PAP-576 settings toggles and transcript default
  Add username log censor setting
  fix: use standard toggle component for permission controls
  fix: add missing setPrincipalPermission mock in portability tests
  fix: use fixed 1280x640 dimensions for org chart export image
  Adjust default CEO onboarding task copy
  fix: link Agent Company to agentcompanies.io in export README
  fix: strip agents and projects sections from COMPANY.md export body
  fix: default company export page to README.md instead of first file
  Add default agent instructions bundle
  ...

# Conflicts:
#	packages/adapters/pi-local/src/server/execute.ts
#	packages/db/src/migrations/meta/0039_snapshot.json
#	packages/db/src/migrations/meta/_journal.json
#	server/src/__tests__/agent-permissions-routes.test.ts
#	server/src/__tests__/agent-skills-routes.test.ts
#	server/src/services/company-portability.ts
#	skills/paperclip/references/company-skills.md
#	ui/src/api/agents.ts
2026-03-20 15:04:55 -05:00
github-actions[bot]
a290d1d550 chore(lockfile): refresh pnpm-lock.yaml (#1377)
Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
2026-03-20 14:46:31 -05:00
Dotta
abf48cbbf9 Merge pull request #1379 from paperclipai/fix/codex-managed-home-followups
Default codex-local to a managed per-company CODEX_HOME
2026-03-20 14:45:55 -05:00
dotta
d53714a145 fix: manage codex home per company by default 2026-03-20 14:44:27 -05:00
dotta
07757a59e9 Ensure agent home directories exist before use
mkdir -p the CODEX_HOME directory in codex-local adapter and the
agentHome directory in the heartbeat service before passing them to
adapters. This prevents CLI tools from erroring when their home
directory hasn't been created yet. Covers all local adapters that
set AGENT_HOME.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 14:25:18 -05:00
Dotta
f0b5130b80 Merge pull request #840 from paperclipai/paperclip-company-import-export
WIP: markdown-first company import/export and adapter skill sync
2026-03-20 14:16:44 -05:00
dotta
0ca479de9c Handle directory entries in imported zip archives 2026-03-20 14:09:21 -05:00
dotta
553e7b6b30 Fix portability import and org chart test blockers 2026-03-20 14:06:37 -05:00
dotta
1830216078 Fix PR verify failures after merge 2026-03-20 13:40:53 -05:00
dotta
5140d7b0c4 Merge remote-tracking branch 'public-gh/master' into paperclip-company-import-export
* public-gh/master:
  fix: address greptile follow-up feedback
  docs: clarify quickstart npx usage
  Add guarded dev restart handling
  Fix PAP-576 settings toggles and transcript default
  Add username log censor setting
  fix: use standard toggle component for permission controls

# Conflicts:
#	server/src/routes/agents.ts
#	ui/src/pages/AgentDetail.tsx
2026-03-20 13:28:05 -05:00
dotta
a62c264ddf fix: harden public routine trigger auth 2026-03-20 13:23:31 -05:00
Dotta
3db2d33e4c Merge pull request #1356 from paperclipai/feature/dev-restart-log-censor-followups
Improve dev restart handling and instance settings behavior
2026-03-20 13:19:41 -05:00
dotta
360a7fc17b fix: address greptile follow-up feedback 2026-03-20 13:18:29 -05:00
dotta
13fd656e2b Add Beta badge to Routines page and sidebar nav
Adds an amber "Beta" tag next to "Routines" in both the page heading
and the sidebar navigation item. Extends SidebarNavItem with textBadge
and textBadgeTone props for reusable text badges.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 13:10:45 -05:00
Paperclip Dev
47449152ac fix(issues): normalize HTML entities in @mention tokens before agent lookup (#1255)
Rich-text comments store entities like &#x20; after @names; strip them before matching agents so issue_comment_mentioned and wake injection work.

Made-with: Cursor
2026-03-20 16:38:55 +00:00
dotta
9ee440b8e4 Add Routine badge to issue detail for routine-generated issues
Issues created from routine executions now display a clickable "Routine"
badge in the header bar, linking back to the originating routine.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 09:13:30 -05:00
dotta
5b1e1239fd Fix routine run assignment wakeups
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 08:58:24 -05:00
dotta
79652da520 Address remaining Greptile portability feedback 2026-03-20 08:55:10 -05:00
dotta
0f4a5716ea docs: clarify quickstart npx usage 2026-03-20 08:50:00 -05:00
dotta
8fc399f511 Add guarded dev restart handling
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 08:50:00 -05:00
dotta
dd44f69e2b Fix PAP-576 settings toggles and transcript default
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 08:50:00 -05:00
dotta
39878fcdfe Add username log censor setting
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 08:50:00 -05:00
dotta
3de7d63ea9 fix: use standard toggle component for permission controls
Replace the button ("Enabled"/"Disabled") for "Can create new agents" and
the custom oversized switch for "Can assign tasks" with the same toggle
style (h-5 w-9, bg-green-600/bg-muted) used throughout Run Policy.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 08:50:00 -05:00
dotta
581a654748 fix: add missing setPrincipalPermission mock in portability tests
The access service mock was missing the setPrincipalPermission function,
causing 5 test failures in the import flow.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 08:30:07 -05:00
dotta
888179f7f0 fix: use fixed 1280x640 dimensions for org chart export image
GitHub recommends 1280x640 for repository social media previews.
The org chart SVG/PNG now always outputs at these dimensions,
scaling and centering the content to fit any org size.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 08:27:27 -05:00
dotta
0bb6336eaf Adjust default CEO onboarding task copy
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 08:22:45 -05:00
dotta
2d8c8abbfb Fix routine assignment wakeups
Share issue-assignment wakeup logic between direct issue creation and routine-created execution issues, and add regression coverage for the routine path.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 08:11:19 -05:00
dotta
6f7609daac fix: link Agent Company to agentcompanies.io in export README
Update the "What's Inside" section to use a blockquote linking
"Agent Company" to https://agentcompanies.io and "Paperclip" to
https://paperclip.ing.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 08:06:04 -05:00
dotta
b26b9cda7b fix: strip agents and projects sections from COMPANY.md export body
COMPANY.md now contains only the company description in frontmatter,
without the agents list and projects list that were previously rendered
in the markdown body. The README.md already contains this info.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 08:05:55 -05:00
dotta
fb760a63ab Fix routine toggle dirty tracking
Remove routine status from the editable detail draft so the active/paused switch remains an immediate save path instead of surfacing unsaved form state.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 08:02:34 -05:00
dotta
971513d3ae fix: default company export page to README.md instead of first file
When navigating to the company export page without a specific file in
the URL, select README.md by default instead of whichever file happens
to be first in the export result (previously COMPANY.md).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 07:59:32 -05:00
dotta
d6bb71f324 Add default agent instructions bundle
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 07:42:36 -05:00
dotta
0f45999df9 Bundle default CEO onboarding instructions
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 07:38:05 -05:00
dotta
bee814787a fix: raise Paperclip wordmark to align with logo icon
Text y position 13 → 11.5 so the wordmark vertically centers
with the scaled paperclip icon.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 07:34:04 -05:00
dotta
d22131ad0a fix: nudge avatar down for better centering, scale down logo icon
- Avatar circle moved from y+24 to y+27 across all three card renderers
  for balanced whitespace between card top and text baseline
- Paperclip logo icon scaled to 0.72x with adjusted text position
  to better match the wordmark size

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 07:31:39 -05:00
dotta
7930e725af fix: apply text centering to default card renderer (warmth/mono/nebula)
The previous text centering fix (y+66/y+82) only updated the circuit
and schematic custom renderers. The defaultRenderCard used by warmth,
monochrome, and nebula still had the old y+52/y+68 positions.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 07:24:59 -05:00
dotta
5fee484e85 Fix routine coalescing for idle execution issues
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 07:21:38 -05:00
dotta
d7a08c1db2 Remove process from CEO onboarding adapters
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 07:18:58 -05:00
dotta
401b241570 fix: vertically center name/role text between avatar and card bottom
Moved name baseline from y+58 to y+66 and role from y+74 to y+82,
centering the text block in the 55px gap between the avatar circle
bottom (y+41) and the card bottom (y+96).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 07:04:59 -05:00
dotta
bf5cfaaeab Hide deprecated agent working directory by default
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 07:04:41 -05:00
dotta
616a2bc8f9 test: fix post-sync verification regressions 2026-03-20 07:01:42 -05:00
dotta
4ab3e4f7ab fix: org chart layout refinements — retina, text spacing, logo alignment
- Increase card height from 88 to 96px for better text spacing
- Move name/role text down (y+58/y+74) so text sits properly below emoji
- Fix Paperclip logo watermark — vertically center text with icon
- Render PNG at 2x density (144 DPI) for retina-quality output

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 06:59:10 -05:00
dotta
2a33acce3a Remove api trigger kind and mark webhook as coming soon
Drop "api" from the trigger kind dropdown and disable the "webhook"
option with a "COMING SOON" label until it's ready.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 06:54:03 -05:00
dotta
b2c2bbd96f feat: add Twemoji colorful emoji rendering to org chart SVG (no browser)
Embeds Twemoji SVG paths directly into the org chart cards, replacing
monochrome icon paths with full-color emoji graphics (crown, laptop,
globe, keyboard, microscope, wand, chart, gear). Works in pure SVG
without any browser, emoji font, or Satori dependency.

Twemoji graphics are CC-BY 4.0 licensed (Twitter/X).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 06:54:02 -05:00
dotta
b72279afe4 Clean up routine activity tab: remove pill badges, use inline text
Replace the cluttered rounded-full bordered pills around each activity
detail with clean inline text separated by dot separators. Wrap in a
bordered card container matching the runs tab style.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 06:39:04 -05:00
dotta
4c6e8e6053 Move routine title into header row, remove Automation label
The editable title now sits in the header alongside Run Now and
the active/paused toggle, replacing the old "Automation" subheading.
This removes the redundant label and gives the title top-level
prominence in the routine detail view.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 06:38:45 -05:00
dotta
f2c42aad12 feat: multi-style pure SVG org chart renderer (no Playwright needed)
Rewrote org-chart-svg.ts to support all 5 styles (monochrome, nebula,
circuit, warmth, schematic) as pure SVG — no browser or Satori needed.
Routes now accept ?style= query param. Added standalone comparison script.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 06:33:29 -05:00
dotta
6a568662b8 fix: remove duplicate company branding import 2026-03-20 06:29:08 -05:00
dotta
d07d86f778 Merge public-gh/master into paperclip-company-import-export 2026-03-20 06:25:24 -05:00
dotta
8cc8540597 Fix live run indicator: only show blue dot when a run is actually active
Previously used routine run status ("received"/"issue_created") which
are not the right signal. Now queries heartbeatsApi.liveRunsForIssue()
on the active issue to check if an agent is actually running — the same
source of truth the LiveRunWidget uses.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 06:22:51 -05:00
dotta
5f2b1b63c2 Add explicit skill selection to company portability 2026-03-20 06:20:30 -05:00
dotta
4fc80bdc16 Fix live run indicator: only show blue dot when a run is actually active
The blue dot and LiveRunWidget were driven by `routine.activeIssue`,
which returns any open execution issue — even after the heartbeat run
finishes. Now checks routineRuns for status "received" or "issue_created"
to determine if a run is actually in progress.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 06:20:01 -05:00
Dotta
dfdd3784b9 Merge pull request #1346 from paperclipai/feature/inbox-heartbeat-company-skills
Improve inbox workflows, runtime recovery, and company controls
2026-03-20 06:19:41 -05:00
dotta
a0a28fce38 fix: address greptile feedback 2026-03-20 06:18:00 -05:00
dotta
22b38b1956 Use toggle for task assignment permission control
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 06:09:04 -05:00
dotta
4ffa2b15dc fix: suggest comment reassignment from recent commenter 2026-03-20 06:05:15 -05:00
dotta
ee85028534 docs: expand paperclip company skills guidance 2026-03-20 06:05:15 -05:00
dotta
c844ca1a40 Improve orphaned local heartbeat recovery
Persist child-process metadata for local adapter runs, keep detached runs alive when their pid still exists, queue a single automatic retry when the pid is confirmed dead, and clear detached warnings when the original run reports activity again.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 06:05:15 -05:00
dotta
7f3fad64b8 Move cost summary from standalone collapsible to top of Activity tab
Moves the cost summary out of a collapsible section below the tabs and
into the Activity tab content, displayed as a static card at the top.
Removes the now-unused `cost` state from `secondaryOpen`.

Closes PAP-559

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 06:05:15 -05:00
dotta
d6c6aa5c49 test: cover agent permission cleanup routes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 06:05:15 -05:00
dotta
f9d685344d Expose agent task assignment permissions
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 06:05:15 -05:00
dotta
bcc1d9f3d6 Remove border containers from workspace fields in new project dialog
Strip the rounded-border card wrappers from repo URL and local folder
fields so they sit directly in the section. Add pt-3 to the section
so the first field doesn't touch the border-t above it.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 06:05:15 -05:00
dotta
25af0a1532 Interleave failed runs with issues and approvals in inbox
Failed runs are no longer shown in a separate section. They are now
mixed into the main work items feed sorted by timestamp, matching
how approvals are already interleaved with issues.

Replaced the large FailedRunCard with a compact FailedRunInboxRow
that matches the ApprovalInboxRow visual style.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 06:05:14 -05:00
dotta
72a0e256a8 Simplify new project dialog: always show repo and local folder fields
Remove the workspace setup toggle menu ("Where will work be done on this
project?") and instead always display both repo URL and local folder
inputs directly. Both fields are marked as optional with help tooltips
explaining their purpose. Repo is shown first, local folder second.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 06:05:14 -05:00
dotta
9e21ef879f Show agent name in inbox approval labels (e.g. "Hire Agent: Designer")
Instead of the generic "Hire Agent" label, display the agent's name from
the approval payload for hire_agent approvals across inbox, approval card,
and approval detail views.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 06:05:14 -05:00
dotta
58a3cbd654 Route non-fatal adapter notices to stdout
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 06:05:14 -05:00
dotta
915a3ff3ce fix: default comment reassign to last commenter who isn't me
When commenting on an issue, the reassign dropdown now defaults to the
last commenter who is not the current user, preventing accidental
self-reassignment. Falls back to the current issue assignee if no
other commenters exist.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 06:05:14 -05:00
dotta
9c5a31ed45 Allow CEO agents to update company branding (name, description, logo, color)
- Add updateCompanyBrandingSchema restricting agent-updatable fields to name,
  description, brandColor, and logoAssetId
- Update PATCH /api/companies/:companyId to allow CEO agents with branding-only
  fields while keeping admin fields (status, budget, etc.) board-only
- Allow agents to GET /api/companies/:companyId for reading company info
- issuePrefix (company slug) remains protected — not in any update schema
- Document branding APIs in SKILL.md quick reference and api-reference.md

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 06:05:14 -05:00
dotta
14ee364190 Add standalone Playwright-based org chart image generator
Replaces the sharp SVG→PNG approach with Playwright headless browser
rendering. This solves the emoji rendering issue - browser natively
renders emojis, full CSS (shadows, gradients, backdrop-filter), and
produces pixel-perfect output matching the HTML preview.

Generates 20 images: 5 styles (Mono, Nebula, Circuit, Warmth, Schematic)
× 3 org sizes (sm/med/lg) + OG cards (1200×630).

Usage: npx tsx scripts/generate-org-chart-images.ts

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-20 05:55:33 -05:00
dotta
2d7b9e95cb Merge public-gh/master into paperclip-company-import-export 2026-03-20 05:53:55 -05:00
dotta
b20675b7b5 Add org chart image export support 2026-03-20 05:51:33 -05:00
Devin Foley
df8cc8136f ci: add e2e tests to PR checks
Add a PR E2E workflow that runs the Playwright onboarding test on
every PR targeting master. Generates a minimal config file and lets
Playwright manage the server lifecycle. Runs in skip_llm mode so
no secrets are required.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 19:19:32 -07:00
Devin Foley
b05d0c560e fix(ci): skip auto-merge step when lockfile is unchanged
The "Enable auto-merge" step runs unconditionally, even when the
lockfile didn't change and no PR exists. This causes the workflow
to fail with "lockfile PR was not found."

Use a step output to gate the auto-merge step so it only runs
when a PR was actually created or updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 16:42:58 -07:00
dotta
c5f20a9891 Add live run support to routine detail page
- Blue dot indicator on Runs tab when there's an active run
- Run Now already switches to Runs tab (was done previously)
- LiveRunWidget shows streaming transcript in Runs tab for active runs
- Poll routine detail and runs list during active runs for real-time updates

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 17:51:07 -05:00
dotta
53249c00cf Upgrade org chart SVG to match Warmth style with icons and descriptive labels
- Larger cards (88px tall) with more breathing room (24px/56px gaps)
- Descriptive role labels (Chief Executive, Technology, etc.) instead of abbreviations
- SVG icon paths per role (star, terminal, globe, etc.) instead of text labels
- Keeps Pango-safe rendering (no emoji) while being visually closer to Warmth HTML

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 17:39:26 -05:00
dotta
339c05c2d4 Remove Issues tab from routine detail page
Keep only Triggers, Runs, and Activity tabs per board request.
Cleaned up unused executionIssues query, IssueRow and ListTree imports.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 17:38:05 -05:00
dotta
c7d05096ab Allow Run Now on paused routines
The server rejected manual runs for any non-active routine. Now only
archived routines are blocked — paused routines can still be triggered
manually via "Run now".

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 17:35:30 -05:00
dotta
21765f8118 Fix trigger delete: handle 204 No Content in API client
The DELETE /routine-triggers/:id endpoint returns 204 No Content, but
the API client unconditionally called res.json() on all responses,
causing a JSON parse error that surfaced as "API route not found".

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 17:31:27 -05:00
dotta
9998cc0683 Fix schedule time picker: cleaner hour labels, hide selectors for every-minute
- Change hour labels from "10:00 AM" to "10 AM" to avoid confusion with the separate minute selector
- Hide hour/minute selectors when "Every minute" preset is selected (no time config needed)
- Fix describeSchedule to work with updated hour label format

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 17:29:09 -05:00
dotta
c39758a169 Replace Mermaid org chart with PNG image in export preview
The frontend generateReadmeFromSelection() was building an inline Mermaid
diagram for the org chart. The server already generates a PNG at
images/org-chart.png, so the preview should reference it the same way.

Removed dead mermaidId/mermaidEscape/generateOrgChartMermaid helpers.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 17:23:27 -05:00
dotta
e341abb99c Improve routine configuration: delete triggers, fix pause, add feedback
- Remove per-trigger enabled/paused selector (routine-level only)
- Move save/rotate/delete buttons to the right in trigger editor
- Apply board feedback on UI cleanup

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 17:20:57 -05:00
dotta
5caf43349b Improve routine configuration: delete triggers, fix pause, add feedback
- Add trash icon button to delete triggers (full stack: service, route, API client, UI)
- Fix pause/unpause bug where saving routine could revert status by excluding
  status from the save payload (status is managed via dedicated pause/resume buttons)
- Add toast feedback for run, pause, and resume actions
- Auto-switch to Runs tab after triggering a manual run
- Add live update invalidation for routine/trigger/run activity events

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 16:45:08 -05:00
dotta
f7c766ff32 Fix markdown image rendering without resolver
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 16:41:28 -05:00
dotta
bdeaaeac9c Simplify routine configuration UI
- Add "Every minute" schedule preset as finest granularity
- Remove status and priority from advanced delivery settings
- Auto-generate trigger labels from kind instead of manual input

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 16:36:39 -05:00
dotta
a9802c1962 Resolve relative image paths in export/import markdown viewer
The MarkdownBody component now accepts an optional resolveImageSrc callback
that maps relative image paths (like images/org-chart.png) to base64 data URLs
from the portable file entries. This fixes the export README showing a broken
image instead of the org chart PNG.

Applied to both CompanyExport and CompanyImport preview panes.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 16:33:30 -05:00
dotta
531945cfe2 Add --skills flag to company export CLI and fix unsupported URL import path
- Add first-class --skills <list> option to `paperclipai company export`,
  passing through to the existing service support for skill selection
- Remove broken `type: "url"` source branch from import command — the shared
  schema and server only accept `inline | github`, so non-GitHub HTTP URLs
  now error clearly instead of failing at validation
- Export isHttpUrl/isGithubUrl helpers for testability
- Add server tests for skills-filtered export (selected + fallback)
- Add CLI tests for URL detection helpers

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 16:29:11 -05:00
dotta
6a7e2d3fce Redesign routines UI to match issue page design language
- Remove Card wrappers and gray backgrounds from routine detail
- Use max-w-2xl container layout like issue detail page
- Add icons to tabs (Clock, Play, ListTree, Activity) matching issue tabs
- Make activity tab compact (single-line items with space-y-1.5)
- Create shared RunButton and PauseResumeButton components
- Build user-friendly ScheduleEditor with presets (hourly, daily, weekdays, weekly, monthly)
- Auto-detect timezone via Intl API instead of manual timezone input
- Use shared action buttons in both AgentDetail and RoutineDetail
- Replace bordered run history cards with compact divided list

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 15:05:32 -05:00
Devin Foley
035cb8aec2 Merge pull request #737 from alaa-alghazouli/fix/embedded-postgres-initdbflags-types
fix: add initdbFlags to embedded postgres constructor typings
2026-03-19 12:21:27 -07:00
dotta
ca3fdb3957 Set sourceType to skills_sh for skills imported from skills.sh URLs
When skills are imported via skills.sh URLs or key-style imports
(org/repo/skill), the stored sourceType is now "skills_sh" with the
original skills.sh URL as sourceLocator, instead of "github" with the
resolved GitHub URL.

- Add "skills_sh" to CompanySkillSourceType and CompanySkillSourceBadge
- Track originalSkillsShUrl in parseSkillImportSourceInput
- Override sourceType/sourceLocator in importFromSource for skills.sh
- Handle skills_sh in key derivation, source info, update checks,
  file reads, portability export, and UI badge rendering

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 14:15:35 -05:00
dotta
301437e169 Make routine rows clickable and add Edit to context menu
- Clicking any routine row navigates to its detail/edit view
- Renamed "Open" to "Edit" in the three-dot context menu
- Added stopPropagation on toggle switch and dropdown cells
  so interactive elements don't trigger row navigation
- Removed redundant Link wrapper on routine title

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 14:08:36 -05:00
dotta
12c6584d30 Style routines table to match issues list
- Remove Card wrapper and header background (bg-muted/30)
- Remove uppercase tracking from header text
- Add hover state (hover:bg-accent/50) and border-b to rows
- Tighten cell padding to match issues list density

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 13:59:40 -05:00
dotta
efbcce27e4 Add Project and Agent columns to routines table
Add two new columns to the routines index table:
- Project column with a colored dot matching the project color
- Agent column with the agent icon and name

Moves the project info out of the Name cell subtitle into its own
dedicated column for better scannability.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 12:12:19 -05:00
dotta
54dd8f7ac8 Turn routines index into a table
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 12:07:49 -05:00
dotta
ce69ebd2ec Add DELETE endpoint for company skills and fix skills.sh URL resolution
- Add DELETE /api/companies/:companyId/skills/:skillId endpoint with same
  permission model as other skill mutations. Deleting a skill removes it
  from the DB, cleans up materialized runtime files, and automatically
  strips it from any agent desiredSkills that reference it.
- Fix parseSkillImportSourceInput to detect skills.sh URLs
  (e.g. https://skills.sh/org/repo/skill) and resolve them to the
  underlying GitHub repo + skill slug, instead of fetching the HTML page.
- Add tests for skills.sh URL resolution with and without skill slug.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 12:05:27 -05:00
dotta
500d926da7 Refine routines editor flow
Align the routines list and detail editors with the issue-composer interaction model, and fix the scheduler's typed date comparison.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 11:36:01 -05:00
Dotta
b1c4b2e420 Merge pull request #743 from Sigmabrogz/fix/openclaw-gateway-unhandled-rejection
fix(openclaw-gateway): handle challengePromise rejection to prevent crash
2026-03-19 09:12:20 -05:00
Dotta
1d1511e37c Merge pull request #1129 from AOrobator/ao/link-ticket-refs-skill
Clarify linked ticket references in Paperclip skill
2026-03-19 09:11:33 -05:00
dotta
8f5196f7d6 Add routines automation workflows
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 08:39:24 -05:00
dotta
8edff22c0b Add skills section to company export README
Lists all skills in a markdown table with name, description, and source.
GitHub and URL-sourced skills render as clickable links to their repository.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 08:27:39 -05:00
dotta
2f076f2add Default new agents to managed AGENTS.md
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 07:57:25 -05:00
Dotta
fff0600b1d Merge pull request #1250 from richardanaya/master
fix(pi-local): fix not using skills, fix poor parsing of pi output, fix pi-local not showing up in pi-config section
2026-03-19 07:46:02 -05:00
dotta
16e221d03c Update portability tests for binary file entries
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 07:28:26 -05:00
dotta
cace79631e Move AGENTCOMPANIES_SPEC_INVENTORY.md to doc/
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 07:27:20 -05:00
dotta
05c8a23a75 Add AGENTCOMPANIES_SPEC_INVENTORY.md indexing all spec-related code
Inventories every file that touches the agentcompanies/v1-draft spec:
spec docs, shared types/validators, server services and routes, CLI
commands, UI pages/components/libraries, tests, and skills. Includes
a cross-reference table mapping spec concepts to implementation files.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 07:26:02 -05:00
dotta
7a652b8998 Add company logo portability support 2026-03-19 07:24:04 -05:00
dotta
6d564e0539 Support binary portability files in UI and CLI 2026-03-19 07:23:36 -05:00
dotta
dbc9375256 Merge remote-tracking branch 'public-gh/master' into paperclip-company-import-export
* public-gh/master:
  fix: show validation error on incomplete login submit
  Fix Enter key not submitting login form
2026-03-19 07:15:41 -05:00
dotta
b4e06c63e2 Refine codex runtime skills and portability assets 2026-03-19 07:15:36 -05:00
dotta
01afa92424 Remove warning when archived projects are skipped from export
Archiving a project is normal, not something to warn about.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-19 07:06:46 -05:00
Richard Anaya
1cd61601f3 fix: handle direct array format for Pi tool results
Pi sometimes sends tool results as a direct array [{"type":"text","text":"..."}]
rather than wrapped in {"content": [...]}. Now handles both formats to
properly extract text content instead of showing raw JSON.
2026-03-18 21:11:03 -07:00
Richard Anaya
6eb9545a72 fix: extract text content from Pi's tool result content arrays
Pi returns tool results in format: {"content": [{"type": "text", "text": "..."}]}
Previously we were JSON.stringify-ing the whole object, showing as:
  {"content":[{"type":"text","text":"..."}]}
Now we extract the actual text content for cleaner display.
2026-03-18 21:02:53 -07:00
Richard Anaya
47a6d86174 fix: include toolName in tool_result entries from turn_end toolResults
The turn_end event includes toolResults array with toolName. Previously
the parsing only included toolCallId, now we also extract toolName so
the UI can display the correct tool name even when tool_result entries
arrive without a preceding tool_call.
2026-03-18 20:57:32 -07:00
Richard Anaya
aa854e7efe fix: include toolName in tool_result transcript entries for Pi adapter
When tool_result entries arrive without a matching tool_call, the transcript
was showing generic 'tool' as the name. Now pl-local parses toolName from
tool_execution_end events and passes it through, so the UI can display
the actual tool name (e.g., 'bash', 'Read', 'Ls') instead of 'tool'.
2026-03-18 20:51:59 -07:00
Richard Anaya
5536e6b91e fix(pi-local): ensure skills directory exists before passing --skill flag
- Hoist PI_AGENT_SKILLS_DIR to module-level constant to avoid duplicate path computation
- Always create ~/.pi/agent/skills directory before early return in ensurePiSkillsInjected, so the path is valid when --skill flag is passed
2026-03-18 20:40:55 -07:00
Richard Anaya
f37e0aa7b3 fix(pi-local): pass --skill flag for paperclip skills and enable pi_local in adapter dropdown
- Add --skill ~/.pi/agent/skills to pi CLI invocation so it loads the paperclip skill
- Enable pi_local in the AgentConfigForm adapter type dropdown (was showing as coming soon)
2026-03-18 20:34:08 -07:00
dotta
b75e00e05d Always include task files in company export, remove toggle button
Tasks are now loaded by default on the export page (unchecked and
folded as before). The "Load task files" / "Hide task files" button
is removed since it is no longer needed.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 21:59:02 -05:00
dotta
51ca713181 Add CEO-safe company portability flows
Expose CEO-scoped import/export preview and apply routes, keep safe imports non-destructive, add export preview-first UI behavior, and document the new portability workflows.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 21:54:10 -05:00
dotta
685c7549e1 Filter junk files from instructions bundles
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 21:47:53 -05:00
dotta
8be868f0ab Collapse tasks folder by default on company export page
The tasks directory is now excluded from auto-expanded top-level
directories when the export page loads, keeping the tree cleaner.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 21:31:38 -05:00
dotta
e28bcef4ad Exclude archived projects from company export
Filter out projects with archivedAt set when building the export bundle,
so archived projects never appear in the exported package. Adds a warning
when archived projects are skipped.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 21:29:42 -05:00
dotta
7b4a4f45ed Add CEO company branding endpoint
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 21:03:41 -05:00
dotta
87b17de0bd Preserve namespaced skill export paths
Keep readable namespaced skill export folders while replacing managed company UUID segments with the company issue prefix for export-only paths.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 16:54:25 -05:00
dotta
9ba47681c6 Use pretty export paths for skills
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 16:23:19 -05:00
dotta
ef60ea0446 Fix org chart canvas overflowing viewport by using h-full instead of calc
The previous h-[calc(100dvh-6rem)] underestimated the vertical overhead
(breadcrumb, padding, worktree banner, button bar). Using h-full lets the
flex layout propagate the correct available height from <main>.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 15:29:19 -05:00
dotta
cd01ebb417 Add click-to-copy workspace path on Paperclip workspace source label
When a skill's source is "Paperclip workspace", clicking the label now
copies the absolute path to the managed skills workspace to the clipboard
and shows a toast confirmation.

- Add sourcePath field to CompanySkillDetail and CompanySkillListItem types
- Return managedRoot path as sourcePath from deriveSkillSourceInfo for
  Paperclip workspace skills
- Make source label a clickable button in SkillPane detail view

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 15:24:22 -05:00
dotta
6000bb4ee2 Fix new file creation on Instructions tab silently failing
New files created via the "+" button were not appearing in the file tree
because visibleFilePaths was derived solely from bundle API data. The
selection reset effect would also immediately undo the file selection.

Add pendingFiles state to track newly created files until they are saved
to disk. Include pending files in visibleFilePaths, guard the selection
reset effect, and clean up pending state after successful save.

Fixes PAP-563

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 14:59:14 -05:00
dotta
e99fa66daf Fix sidebar scrollbar pushing content on hover
Reserve 8px scrollbar width at all times instead of expanding from 0 on
hover. The thumb stays transparent until hover so the scrollbar is
visually hidden but no longer causes a layout shift.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 14:40:50 -05:00
dotta
3b03ac1734 Scope Codex local skills home by company 2026-03-18 14:38:39 -05:00
dotta
6ba5758d30 Fix file selection being reset when clicking entry file in tree
The useEffect that syncs selectedFile was resetting to an existing file
whenever the selected path wasn't in the bundle's on-disk file list.
This prevented selecting the entry file (e.g. AGENTS.md) when it didn't
yet exist on disk, even though it was visible in the file tree.

Allow selecting the currentEntryFile even when it has no on-disk file.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 14:32:04 -05:00
dotta
cfc53bf96b Add unmanaged skill provenance to agent skills
Expose adapter-discovered user-installed skills with provenance metadata, share persistent skill snapshot classification across local adapters, and render unmanaged skills as a read-only section in the agent skills UI.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 14:21:50 -05:00
dotta
58d7f59477 Fix file tree clicks and redesign add-file UI on instructions tab
- Add onClick handler to file row div in PackageFileTree so clicks
  anywhere on the row select the file (not just the inner button)
- Replace "Add" button with compact "+" icon that reveals an inline
  input with Create/Cancel actions
- Hide file name input until "+" is clicked to reduce visual clutter
- Validate new file paths: reject ".." path traversal segments
- Change placeholder from "docs/TOOLS.md" to "TOOLS.md"

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 14:21:21 -05:00
dotta
b0524412c4 Default advanced toggle open when instructions mode is External
When the agent instructions tab is in "External" mode, the advanced
collapsible section now defaults to open so users don't have to manually
expand it to see the mode and path settings.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 14:20:06 -05:00
dotta
3689992965 Prune stale Codex skill symlinks
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 14:07:24 -05:00
dotta
55165f116d Prune stale deleted company skills
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 14:00:20 -05:00
dotta
480174367d Add company skill assignment to agent create and hire flows
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 13:18:48 -05:00
dotta
099c37c4b4 Add attachments API endpoints to Paperclip skill quick-reference
Add upload, list, get content, and delete attachment endpoints
to the Key Endpoints table so agents know about the attachments API.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 13:11:26 -05:00
dotta
d84399aebe Preserve any agent tab when switching agents in sidebar
Remove hardcoded validTabs set and pass through whatever tab
segment is in the current URL. This makes tab preservation
work for all current and future agent tabs.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 12:58:33 -05:00
dotta
4f49c8a2b9 Fix sidebar scrollbar well visible when not hovering
Set scrollbar width to 0 when not hovering so the track area
doesn't create a visible gutter. On hover, width expands to 8px
with the track/thumb colors.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 12:57:26 -05:00
dotta
10f26cfad9 Fix instructions page Advanced section layout: 3-column grid, truncate root path
- Use a 3-column grid (Mode | Root path | Entry file) instead of stacked layout
- Truncate long managed root path with hover tooltip instead of break-all wrapping
- Better spacing with grid gaps instead of space-y stacking

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 12:48:49 -05:00
dotta
1e393bedb2 Move Advanced settings above file browser, improve spacing
- Advanced collapsible now sits in its own row above the file browser/editor
- Increased spacing between form fields (gap-4 → gap-5, space-y-1 → space-y-1.5)
- Added more bottom padding (pb-6) to Advanced section for scroll room
- Increased inner spacing (space-y-4 → space-y-5) so mode/root path/entry file don't touch

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 12:20:18 -05:00
Dotta
1ac85d837a Merge pull request #1014 from paperclipai/fix/login-enter-submit
Fix Enter key not submitting login form
2026-03-18 10:25:20 -05:00
dotta
9e19f1d005 Merge public-gh/master into paperclip-company-import-export 2026-03-18 09:57:26 -05:00
Dotta
731c9544b3 Merge pull request #1222 from paperclipai/fix/github-release-origin-remote
fix: use origin for github release creation in actions
2026-03-18 09:51:53 -05:00
dotta
528f836e71 fix: use origin for github release creation in actions 2026-03-18 09:10:00 -05:00
Dotta
78c714c29a Merge pull request #1221 from paperclipai/fix/workspace-warnings-stdout
Log workspace warnings to stdout
2026-03-18 08:38:42 -05:00
dotta
88da68d8a2 Log workspace warnings to stdout
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 08:32:59 -05:00
Dotta
0d9fabb6ec Merge pull request #1220 from paperclipai/release-note-2026-318-0
Add 2026.318.0 release notes stub
2026-03-18 08:31:51 -05:00
dotta
ff16ff8d01 Add 2026.318.0 release notes stub 2026-03-18 08:30:59 -05:00
dotta
154a4a7ac1 Improve instructions bundle mode switching
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 08:10:36 -05:00
Dotta
493b0ca8d1 Merge pull request #1216 from paperclipai/split/release-smoke-calver
Release smoke workflow and CalVer patch-slot updates
2026-03-18 08:07:32 -05:00
Dotta
7730230aa9 Merge pull request #1217 from paperclipai/split/ui-onboarding-inbox-agent-details
Inbox, agent detail, and onboarding polish
2026-03-18 08:04:57 -05:00
dotta
2c05c2c0ac test: harden onboarding route coverage 2026-03-18 08:00:02 -05:00
dotta
cc1620e4fe chore: hide agents skills tab from UI
Comment out the Skills tab entry, view rendering, and breadcrumb
in AgentDetail so it's not visible. The code is preserved for
later re-enablement.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 07:59:50 -05:00
dotta
3e88afb64a fix: remove session compaction card from agent configuration
No adapters currently support configuring compaction, so the info box
adds complexity without utility. Will revisit at the adapter level.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 07:59:50 -05:00
dotta
3562cca743 feat: hide bootstrap prompt config unless agent already has one
Only show the bootstrap prompt field on the agent configuration page
when editing an existing agent that already has a bootstrapPromptTemplate
value set. Label it as "(legacy)" with an amber notice recommending
migration to prompt template or instructions file. Hidden entirely
for new agent creation.

Closes PAP-536

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 07:59:50 -05:00
dotta
9a4135c288 fix: use proper Tooltip component for sidebar version hover
The native title attribute tooltip was not working reliably. Switched
to the project's Radix-based Tooltip component which provides instant,
styled tooltips on hover.

Fixes PAP-533

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 07:59:50 -05:00
dotta
7140090d0b Align approval inbox icon with issue status column
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 07:59:50 -05:00
dotta
bfb1960703 fix: show only 'v' in sidebar with full version on hover tooltip
The full version string was pushing the sidebar too wide. Now displays
just "v" with the full version (e.g. "v1.2.3") shown on hover via
title attribute, for both mobile and desktop sidebar layouts.

Fixes PAP-533

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 07:59:50 -05:00
dotta
22ae70649b Mix approvals into inbox activity feed
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 07:59:50 -05:00
dotta
c121f4d4a7 Fix inbox recent visibility
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 07:59:50 -05:00
dotta
19f4a78f4a feat: add release smoke workflow 2026-03-18 07:59:32 -05:00
dotta
3e0e15394a chore: switch release calver to mdd patch 2026-03-18 07:57:36 -05:00
dotta
5252568825 Refine instructions tab: remove header/border, draggable panel, move Advanced below
- Remove "Instructions Bundle" header and border wrapper
- Move "Advanced" collapsible section below the file browser / editor grid
- Make the file browser column width draggable (180px–500px range)
- Advanced section now uses a wider 3-column grid layout

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 07:09:10 -05:00
dotta
c7d31346e0 Add instructions tab to sidebar tab preservation list
The agent instructions tab was renamed from "prompts" to "instructions"
in 6b355e1, but the sidebar valid tabs set was not updated to include
the new name, causing tab preservation to fall back to dashboard.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 07:08:19 -05:00
dotta
6b355e1acf Redesign agent instructions tab (formerly Prompts)
- Rename Prompts tab to Instructions (with backwards-compatible URL routing)
- Update header/subheader text to "Instructions Bundle" / "Configure your agent's behavior with instructions"
- Remove standalone legacy prompt warning banner; move warning to deprecated virtual file badge with tooltip
- Move mode, root path, and entry file controls into a collapsible "Advanced" section below the file browser
- Add help tooltips (?) for mode, root path, and entry file fields
- Show full root path in managed mode with copy-to-clipboard icon
- Reorder: root path now appears to the left of entry file
- Remove HEARTBEAT.md, SOUL.md, TOOLS.md shortcut buttons from file browser
- Add key prop to MarkdownEditor to ensure proper re-mount on file selection change

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 06:47:53 -05:00
dotta
f98d821213 Preserve active tab when switching agents in sidebar
When clicking a different agent in the sidebar, the current tab (prompts,
skills, configuration, budget, runs) is now preserved in the navigation
URL instead of always defaulting to dashboard. Falls back to default
agent URL for non-tab paths (e.g. specific run detail pages).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 06:31:42 -05:00
dotta
8954512dad Fix prompts page render loop
Stabilize prompt file tree expansion state so the prompts editor no longer loops into maximum update depth when loading the bundle. Also replace bundle and file loading placeholders with skeleton UI.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 16:44:07 -05:00
Dotta
f598a556dc Merge pull request #1166 from paperclipai/fix/canary-version-after-partial-publish
fix: advance canary after partial publish
2026-03-17 16:37:58 -05:00
dotta
21f7adbe45 fix: advance canary after partial publish 2026-03-17 16:31:38 -05:00
dotta
9d452eb120 Refine external instructions bundle handling
Keep existing instructionsFilePath agents in external-bundle mode during edits, expose legacy promptTemplate as a deprecated virtual file, and reuse the shared PackageFileTree component in the Prompts view.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 16:15:45 -05:00
dotta
4fdcfe5515 Refresh inbox recent after issue creation
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 16:14:43 -05:00
Dotta
9df7fd019f Merge pull request #1162 from paperclipai/fix/npm-provenance-package-metadata
fix: add npm provenance package metadata
2026-03-17 16:02:36 -05:00
dotta
0036f0e9a1 fix: add npm provenance package metadata 2026-03-17 16:01:48 -05:00
dotta
6ba9aea8ba Add publish metadata for npm provenance 2026-03-17 15:49:42 -05:00
Dotta
f499c9e222 Merge pull request #1145 from wesseljt/fix/opencode-home-env
fix(opencode-local): resolve HOME from os.userInfo() for model discovery
2026-03-17 15:40:13 -05:00
Dotta
b59bc9a6de Merge pull request #1159 from paperclipai/release-automation-followups
fix: validate canary release path in CI
2026-03-17 15:39:50 -05:00
dotta
5cf841283a fix: correct codeowners maintainer handle 2026-03-17 15:38:03 -05:00
repro
9176218d16 fix: validate canary release path in CI 2026-03-17 15:35:59 -05:00
github-actions[bot]
42c0ca669b chore(lockfile): refresh pnpm-lock.yaml (#900)
Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
2026-03-17 15:08:56 -05:00
Dotta
9acf70baab Merge pull request #1154 from paperclipai/release-automation-followups
Release automation follow-ups
2026-03-17 15:07:52 -05:00
Dotta
62e8fd494f chore: expand github codeowners coverage 2026-03-17 15:03:18 -05:00
Dotta
3921466aae chore: auto-merge lockfile refresh PRs 2026-03-17 15:02:16 -05:00
Dotta
f1a0460105 fix: reset lockfile changes before release publish 2026-03-17 14:53:23 -05:00
Dotta
773f8dcf6d Merge pull request #1151 from paperclipai/release-automation-calver
Automate canary/stable releases with CalVer
2026-03-17 14:46:26 -05:00
Dotta
824a297c73 fix: drop accidental agent prompt tab changes 2026-03-17 14:45:22 -05:00
Dotta
4d8c988dab fix: use one workflow for npm trusted publishing 2026-03-17 14:18:42 -05:00
Dotta
48326da83f fix: restore release script executable bit 2026-03-17 14:09:16 -05:00
Dotta
21c1235277 chore: automate canary and stable releases 2026-03-17 14:08:55 -05:00
Dotta
e980c2ef64 Add agent instructions bundle editing
Expose first-class instructions bundle APIs, preserve agent prompt bundles in portability flows, and replace the Agent Detail prompts tab with file-backed bundle editing while retiring bootstrap prompt UI.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 13:42:00 -05:00
Dotta
7b9718cbaa docs: plan memory service surface API
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 12:07:14 -05:00
John Wessel
5965266cb8 fix: guard os.userInfo() for UID-only containers, exclude HOME from cache key
Address Greptile review feedback:

1. Wrap os.userInfo() in try/catch — it throws SystemError when the
   current UID has no /etc/passwd entry (e.g. `docker run --user 1234`
   with a minimal image). Falls back to process.env.HOME gracefully.

2. Add HOME to VOLATILE_ENV_KEY_EXACT so the discovery cache key is
   not affected by the caller-supplied HOME vs the resolved HOME.
   os.userInfo().homedir is constant for the process lifetime, so
   HOME adds no useful cache differentiation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:05:23 -04:00
John Wessel
2aa607c828 fix(opencode-local): resolve HOME from os.userInfo() for model discovery
When Paperclip's server is started via `runuser -u node` (common in
Docker/Fly.io deployments), the HOME environment variable retains the
parent process's value (e.g. /root) instead of the target user's home
directory (/home/node). This causes `opencode models` to miss provider
auth credentials stored under the actual user's home, resulting in
"Configured OpenCode model is unavailable" errors for providers that
require API keys (e.g. zai/zhipuai).

Fix: use `os.userInfo().homedir` (reads from /etc/passwd, not env) to
ensure the child process always sees the correct HOME, regardless of
how the server was launched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 12:58:13 -04:00
Dotta
827b09d7a5 Speed up Claude agent skills loads
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 11:47:50 -05:00
Dotta
e2f26f039a feat: reorganize agent detail tabs and add Prompts tab
Rearrange tabs to: Dashboard, Prompts, Skills, Configuration, Budget.
Move Prompt Template out of Configuration into a dedicated Prompts tab
with its own save/cancel flow and dirty tracking.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 11:30:47 -05:00
Dotta
71de1c5877 Fix HTML entities appearing in copied issue text
Decode HTML entities (e.g. &#x20;) from title and description
before copying to clipboard, and trim trailing whitespace.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 11:18:55 -05:00
Dotta
cd67bf1d3d Add copy-to-clipboard button on issue detail header
Adds a copy icon button to the left of the properties panel toggle
on the issue detail page. Clicking it copies a markdown representation
of the issue (identifier, title, description) to the clipboard and
shows a success toast. The icon briefly switches to a checkmark for
visual feedback.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 11:12:56 -05:00
Dotta
2a15650341 feat: reorganize agent detail tabs and add Prompts tab
Rearrange tabs to: Dashboard, Prompts, Skills, Configuration, Budget.
Move Prompt Template out of Configuration into a dedicated Prompts tab
with its own save/cancel flow and dirty tracking.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 11:12:04 -05:00
Dotta
b5aeae7e22 Merge public-gh/master into paperclip-company-import-export 2026-03-17 10:45:14 -05:00
Dotta
eaa7d54cb4 Merge pull request #899 from paperclipai/paperclip-subissues
Advanced Workspace Support
2026-03-17 10:37:32 -05:00
Dotta
2a56f4134e Harden workspace cleanup and clone env handling 2026-03-17 10:29:44 -05:00
Dotta
b8a816ff8c Hide issue work product outputs 2026-03-17 10:21:11 -05:00
Dotta
2a7c44d314 Merge public-gh/master into paperclip-company-import-export 2026-03-17 10:19:31 -05:00
Dotta
108792ac51 Address remaining Greptile workspace review 2026-03-17 10:12:44 -05:00
Dotta
4a5f6ec00a Merge remote-tracking branch 'public-gh/master' into paperclip-subissues
* public-gh/master:
  hide version until loaded
  add app version label
2026-03-17 09:54:30 -05:00
Dotta
549e3b22e5 Merge pull request #1096 from saishankar404/feat/ui-version-label
add app version label
2026-03-17 09:50:04 -05:00
Dotta
b2f7252b27 Fix empty space in new issue pane when workspaces disabled
The execution workspace section wrapper div was rendered whenever a
project was selected, even when experimental workspaces were off.
The empty div's py-3 padding caused a visible layout bump. Now the
entire block only renders when currentProjectSupportsExecutionWorkspace
is true, and the redundant inner conditional is removed.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:49:12 -05:00
Dotta
6ebfd3ccf1 Merge public-gh/master into paperclip-subissues 2026-03-17 09:42:31 -05:00
Dotta
4da13984e2 Add workspace operation tracking and fix project properties JSX 2026-03-17 09:36:35 -05:00
Dotta
d974268e37 Merge pull request #1132 from paperclipai/dotta-march-17-updates
dottas-march-17-updates
2026-03-17 09:33:57 -05:00
Dotta
2c747402a8 docs: add PR thinking path guidance to contributing 2026-03-17 09:31:21 -05:00
Dotta
e39ae5a400 Add instance experimental setting for isolated workspaces
Introduce a singleton instance_settings store and experimental settings API, add the Experimental instance settings page, and gate execution workspace behavior behind the new enableIsolatedWorkspaces flag.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 09:24:28 -05:00
Dotta
4d9769c620 fix: address review feedback on skills and session compaction 2026-03-17 09:21:44 -05:00
Dotta
4323d4bbda feat: add agent skills tab and local dev helpers 2026-03-17 09:11:37 -05:00
Dotta
5a9a4170e8 Remove box border from execution workspace toggle in issue properties panel
Same styling fix as NewIssueDialog — removes the rounded-md border
from the workspace toggle in the issue detail view.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 09:10:46 -05:00
Dotta
cebd62cbb7 Remove box border and add vertical margin to execution workspace toggle in new issue dialog
Removes the rounded border around the execution workspace toggle section
and increases top/bottom padding for better visual spacing.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 09:10:46 -05:00
Dotta
bba36ab4c0 fix: convert archivedAt string to Date before Drizzle update
The zod schema validates archivedAt as a datetime string, but Drizzle's
timestamp column expects a Date object. The string was passed directly to
db.update(), causing a 500 error. Now we convert the string to a Date
in the route handler before calling the service.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:10:46 -05:00
Dotta
fee3df2e62 Make session compaction adapter-aware
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 09:10:46 -05:00
Dotta
2539950ad7 fix: add two newlines after image drop/paste in markdown editor
When dragging or pasting an image into a markdown editor field, the cursor
would end up right next to the image making it hard to continue typing.
Now inserts two newlines after the image so a new paragraph is ready.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 09:10:46 -05:00
Dotta
d06cbb84f4 fix: increase top margin on markdown headers for better visual separation
Headers were butting up against previous paragraphs too closely. Changed
rendered markdown header selectors from :where() to direct element selectors
to increase CSS specificity and beat Tailwind prose defaults. Bumped
margin-top from 1.15rem to 1.75rem. Also added top margins to MDXEditor
headers (h1: 1.4em, h2: 1.3em, h3: 1.2em) which previously had none.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:10:46 -05:00
Dotta
7c2f015f31 fix: replace window.confirm with inline confirmation for archive project
Swap the browser alert dialog for an in-page confirm/cancel button pair.
Shows a loading spinner while the archive request is in flight, then the
redirect and toast (from prior commit) handle the rest.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:10:46 -05:00
Dotta
b2072518e0 fix: hide sticky save bar on non-config tabs to prevent layout push
The sticky float-right save/cancel bar was rendering (invisible via
opacity-0) on all tabs including runs, causing it to push the runs
layout content. Now only rendered when showConfigActionBar is true.
Also reverts the negative margin workaround from the previous attempt.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:10:46 -05:00
Dotta
10e37dd7e5 fix: remove "None" text from empty goals, add padding to + goal button
When no goals are linked, hide the "None" label and just show the
"+ Goal" button. When goals exist, add left margin to the button so
it doesn't crowd the goal badges.

Closes PAP-522

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:10:46 -05:00
Dotta
0fd7ed84fb fix: archive project navigates to dashboard and shows toast
Previously archiving a project silently navigated to /projects with no
feedback. Now it navigates to /dashboard and shows a success toast for
both archive and unarchive actions.

Closes PAP-521

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:10:45 -05:00
Dotta
54a28cc5b9 fix: remove unnecessary right padding on runs page
Use negative right margin to counteract the Layout container padding,
giving the runs detail panel more horizontal space especially on
smaller screens.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:10:45 -05:00
Dotta
132e2bd0d9 feat: cache project tab per-project and rename/reorder tabs
- Rename "List" tab to "Issues" and reorder: Issues, Overview, Configuration
- Cache the last active tab per project in localStorage
- On revisit, restore the cached tab instead of always defaulting to Issues

Closes PAP-520

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:10:45 -05:00
Dotta
517e90c13a refactor: replace SVG org chart with Mermaid diagram in exports
- Org chart now uses a Mermaid flowchart (graph TD) instead of a
  standalone SVG file — GitHub and the preview both render it natively
- Removed SVG generation code, layout algorithm, and image resolution
- Removed images/org-chart.svg from export output
- Simplified ExportPreviewPane (no more SVG/data-URI handling)
- Both server and client README generators produce Mermaid diagrams

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 09:09:37 -05:00
Dotta
228277d361 feat: deep-linkable URLs for company export file preview
The export page now syncs the selected file with the URL path, e.g.
/PAP/company/export/files/agents/cmo/AGENTS.md. Navigating to such a
URL directly selects and reveals the file in the tree. Browser
back/forward navigation is supported without page refreshes.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 09:08:14 -05:00
Dotta
6c779fbd48 Improve workspace path wrapping with natural break points
- Replace break-all with <wbr> hints after / and - characters so paths
  break at directory and word boundaries instead of mid-word
- Use overflow-wrap: anywhere as fallback for very long segments
- Apply natural breaking to the workspace name link as well
- Rename CopyablePath to CopyableValue with optional mono prop for
  better semantic clarity

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 08:53:19 -05:00
Andrew Orobator
c539fcde8b Fix stale Paperclip issue link example
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 09:52:32 -04:00
Andrew Orobator
7a08fbd370 Reduce duplicate ticket-link guidance
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 09:43:47 -04:00
Andrew Orobator
71e1bc260d Clarify linked ticket references in Paperclip skill
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 09:35:57 -04:00
Dotta
78342e384d feat: dynamic README in export preview with SVG image resolution
- README.md now regenerates in real-time when files are checked/unchecked
  in the export file tree, so counts and tables reflect the actual selection
- SVG image references in markdown (e.g. images/org-chart.svg) resolve to
  inline data URIs so the org chart renders in the README preview
- Fixes issue where unchecked tasks/projects were still counted in README

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 08:27:42 -05:00
Dotta
e6269e5817 Add wrapping paths with copy-to-clipboard in workspace properties
- Replace truncated paths with wrapping text (break-all) so full paths
  are visible
- Add CopyablePath component with a copy icon that appears on hover and
  shows a green checkmark after copying
- Apply to all workspace paths: cwd, branch name, repo URL, and the
  project primary workspace fallback

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 08:08:14 -05:00
Dotta
be9dac6723 Show workspace path and details in issue properties pane
Display the absolute cwd path, branch name, and repo URL for the current
execution workspace in the issue properties panel. When no execution
workspace is active yet, show the project's primary workspace path as
a fallback.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 08:00:39 -05:00
Dotta
ce105d32c3 Simplify execution workspace chooser and deduplicate reusable workspaces
- Remove "Operator branch" and "Agent default" options from the workspace
  mode dropdown in both NewIssueDialog and IssueProperties, keeping only
  "Project default", "New isolated workspace", and "Reuse existing workspace"
- Deduplicate reusable workspaces by space identity (cwd path) so the
  "choose existing workspace" dropdown shows unique worktrees instead of
  duplicate entries from multiple issues sharing the same local folder

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 07:46:40 -05:00
Genie
59b1d1551a fix: Vite HMR WebSocket for reverse proxy + WS StrictMode guard
When running behind a reverse proxy (e.g. Caddy), the live-events
WebSocket would fail to connect because it constructed the URL from
window.location without accounting for proxy routing.

Also fixes React StrictMode double-invoke of WebSocket connections
by deferring the connect call via a cleanup guard.

- Replace deprecated apple-mobile-web-app-capable meta tag
- Guard WS connect with mounted flag to prevent StrictMode double-open
- Use protocol-relative WebSocket URL derivation for proxy compatibility
2026-03-17 07:09:00 -03:00
Sai Shankar
8abfe894e3 hide version until loaded 2026-03-17 09:47:19 +05:30
Sai Shankar
02bf0dd862 add app version label 2026-03-17 09:40:07 +05:30
Dotta
88bf1b23a3 Harden embedded postgres adoption on startup 2026-03-16 21:03:05 -05:00
Dotta
5d1e39b651 fix: SVG preview in export page and update getting-started command
- Add inline SVG rendering for .svg files in ExportPreviewPane
- Update Getting Started to use simpler `company import` syntax

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 20:48:32 -05:00
Dotta
ceb18c77db feat: generate README.md and org chart SVG in company exports
Adds auto-generated README.md with company summary, agent table, project
list, and getting-started instructions. Includes an SVG org chart image
in images/org-chart.svg using the same layout algorithm as the UI.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 20:37:05 -05:00
Dotta
8d5af56fc5 Fix Greptile workspace review issues 2026-03-16 20:12:22 -05:00
Dotta
6dd4cc2840 Verify embedded postgres adoption data dir 2026-03-16 20:01:31 -05:00
Dotta
794ba59bb6 Merge remote-tracking branch 'public-gh/master' into paperclip-subissues
* public-gh/master:
  fix(plugins): address Greptile feedback on testing.ts
  feat(plugins): add document CRUD methods to Plugin SDK
2026-03-16 19:51:09 -05:00
Dotta
6a1c198c04 fix: org chart canvas fits viewport with import/export buttons
Use flex layout so the canvas fills remaining space after the button bar,
instead of a fixed viewport calc that didn't account for button height.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 19:43:08 -05:00
Dotta
1309cc449d Fix trash button on repo when workspace has no local folder
clearRepoWorkspace was calling updateWorkspace to null out the repo
even when there was no local folder, leaving an empty workspace.
Now falls through to persistCodebase which correctly removes the
entire workspace when both cwd and repoUrl would be null.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 19:38:46 -05:00
Dotta
dd11e7aa7b Update pnpm lockfile 2026-03-16 19:36:47 -05:00
Dotta
81b4e4f826 Fix workspace form not refreshing when project accessed via URL key
The invalidateProject() in ProjectProperties only invalidated
queryKeys.projects.detail(project.id) (UUID), but the parent
ProjectDetail query uses routeProjectRef which is typically the
URL key (e.g. "openclaw-testing"). This meant mutations succeeded
but the parent query was never refetched — the UI only updated on
page refresh.

Now also invalidates via project.urlKey so both UUID and URL-key
based query caches are cleared.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 19:34:46 -05:00
Dotta
3456808e1c Fix workspace codebase form not allowing empty saves and not auto-updating
- Allow saving empty values to clear repo URL or local folder from an existing workspace
- submitLocalWorkspace/submitRepoWorkspace now handle empty input as a "clear" operation
- Save button is only disabled for empty input when there's no existing workspace to clear
- removeWorkspace.onSuccess now resets form state (matching create/update handlers)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 19:19:38 -05:00
Dotta
0cfbc58842 Normalize legacy Paperclip skill refs\n\nCo-Authored-By: Paperclip <noreply@paperclip.ing> 2026-03-16 19:13:00 -05:00
Dotta
79e0915a86 Remove namespace from skill list sidebars
Remove the org/repo namespace line from both the company skills sidebar
and agent skills tab — cleaner to show just the skill name with source
icon.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 19:11:01 -05:00
Dotta
56f7807732 feat: scan project workspaces for skills
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 19:09:33 -05:00
Dotta
52978e84ba Remove namespace from skill detail page header
Per feedback, the detail page looks cleaner without the org/repo
namespace line above the skill name. The icon remains on the name row.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 19:07:54 -05:00
Dotta
b339f923d6 Fix skill list namespace layout to stack vertically
The namespace was appearing side-by-side with the skill name because
they were in the same flex row. Restructured the layout so the
namespace appears on its own line above, with the icon and skill name
on the row below it.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 18:59:36 -05:00
Dotta
9e843c4dec Show namespace above skill name with icon on name row in detail view
On the skill detail page, the namespace (e.g. org/repo) now appears
above the skill name in small monospace text, and the source icon is
placed on the same row as the skill name rather than in the metadata.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 18:56:56 -05:00
Dotta
9a26974ba8 Show skill namespace above name, exclude skill name from namespace
In both the company skills list and agent skills tab, the skill key
(e.g. org/repo/skill) is now split so only the namespace portion
(org/repo) appears above the skill name, rather than the full key
below it. Non-namespaced skills show no namespace line.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 18:49:58 -05:00
Dotta
e079b8ebcf Remove "None" text from empty goals and add padding to + Goal button
- When no goals are linked, just show the "+ Goal" button without
  displaying "None" text
- Add left margin to the "+ Goal" button when goals exist above it
  for better visual spacing

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 18:47:29 -05:00
Dotta
9e6cc0851b Fix archive project appearing to do nothing
The archive mutation lacked user feedback:
- No toast notification on success/failure
- Navigated to /projects instead of /dashboard after archiving
- No error handling if the API call failed

Added success/error toasts using the existing ToastContext, navigate
to /dashboard after archiving (matching user expectations), and error
toast on failure.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 18:46:26 -05:00
Dotta
7e4aec9379 Remove the experimental workspace toggle
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 18:37:59 -05:00
Dotta
4220d6e057 Hide project execution workspace config for now
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 18:27:27 -05:00
Dotta
5890b318c4 Namespace company skill identities
Persist canonical namespaced skill keys, split adapter runtime names from skill keys, and update portability/import flows to carry the canonical identity end-to-end.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 18:27:20 -05:00
Dotta
bb788d8360 Treat Codex bootstrap logs as stdout
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 18:26:36 -05:00
Dotta
04ceb1f619 Fix codebase Save button not closing form after update
The updateWorkspace mutation's onSuccess handler only invalidated the
project query but didn't reset the form state (close the edit mode,
clear inputs). This made it look like Save did nothing when editing an
existing workspace. Now matches createWorkspace's onSuccess behavior.

Also added updateWorkspace.isPending to the Save button disabled state
for both local folder and repo inputs.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 18:26:06 -05:00
Dotta
bb46423969 Fix agent skills autosave hydration\n\nCo-Authored-By: Paperclip <noreply@paperclip.ing> 2026-03-16 17:46:07 -05:00
Dotta
8460fee380 Reduce company skill list payloads
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 17:45:28 -05:00
Dotta
bb7d1b2c71 Merge remote-tracking branch 'public-gh/master' into paperclip-subissues
* public-gh/master:
  Fix budget incident resolution edge cases
  Fix agent budget tab routing
  Fix budget auth and monthly spend rollups
  Harden budget enforcement and migration startup
  Add budget tabs and sidebar budget indicators
  feat(costs): add billing, quota, and budget control plane
  refactor(quota): move provider quota logic into adapter layer, add unit tests
  fix(costs): replace non-null map assertions with nullish coalescing, clarify weekData guard
  fix(costs): guard byProject against duplicate null keys, memoize ProviderQuotaCard row aggregations
  fix(costs): align byAgent run filter to startedAt, tighten providerTabItems memo deps, stabilize byProject row keys
  feat(costs): add agent model breakdown, harden date validation, sync CostByProject type, fix quota threshold and tab-gated queries
  fix(costs): harden company auth check, fix frozen date memo, hide empty quota rows
  fix(costs): guard routes, fix DST ranges, sync provider state, wire live updates
  feat(costs): consolidate /usage into /costs with Spend + Providers tabs
  feat(usage): add subscription quota windows per provider on /usage page
  address greptile review: per-provider deficit notch, startedAt filter, weekRange refresh, deduplicate providerDisplayName
  feat(ui): add resource and usage dashboard (/usage route)

# Conflicts:
#	packages/db/src/migration-runtime.ts
#	packages/db/src/migrations/meta/0031_snapshot.json
#	packages/db/src/migrations/meta/_journal.json
2026-03-16 17:19:55 -05:00
Dotta
eb113bff3d Merge pull request #1074 from residentagent/osc/940-plugin-sdk-document-crud
feat(plugins): add document CRUD methods to Plugin SDK
2026-03-16 17:19:24 -05:00
Dotta
3d01217aef Fix legacy migration reconciliation 2026-03-16 17:03:23 -05:00
Dotta
cca086b863 Merge public-gh/master into paperclip-company-import-export 2026-03-16 17:02:39 -05:00
Justin Miller
56985a320f fix(plugins): address Greptile feedback on testing.ts
Remove unnecessary `as any` casts on capability strings (now valid
PluginCapability members) and add company-membership guards to match
production behavior in plugin-host-services.ts.
2026-03-16 16:01:00 -06:00
Justin Miller
0d4dd50b35 feat(plugins): add document CRUD methods to Plugin SDK
Wire issue document list/get/upsert/delete operations through the
JSON-RPC protocol so plugins can manage issue documents with the same
capabilities available via the REST API.

Fixes #940
2026-03-16 15:53:50 -06:00
Dotta
c578fb1575 Merge pull request #949 from paperclipai/feature/upgraded-costs-and-budgeting
feat(costs): add billing, quota, and budget control plane
2026-03-16 16:52:57 -05:00
Dotta
8fbbc4ada6 Fix budget incident resolution edge cases 2026-03-16 16:48:13 -05:00
Dotta
d77630154a Fix required Paperclip skill rows on agent detail 2026-03-16 16:39:21 -05:00
Dotta
0c121b856f Merge remote-tracking branch 'public-gh/master' into paperclip-subissues
* public-gh/master: (51 commits)
  Use attachment-size limit for company logos
  Address Greptile company logo feedback
  Drop lockfile from PR branch
  Use asset-backed company logos
  fix: use appType "custom" for Vite dev server so worktree branding is applied
  docs: fix documentation drift — adapters, plugins, tech stack
  docs: update documentation for accuracy after plugin system launch
  chore: ignore superset artifacts
  Dark theme for CodeMirror code blocks in MDXEditor
  Remove duplicate @paperclipai/adapter-openclaw-gateway in server/package.json
  Fix code block styles with robust prose overrides
  Add Docker setup for untrusted PR review in isolated containers
  Fix org chart canvas height to fit viewport without scrolling
  Add doc-maintenance skill for periodic documentation accuracy audits
  Fix sidebar scrollbar: hide track background when not hovering
  Restyle markdown code blocks: dark background, smaller font, compact padding
  Add archive project button and filter archived projects from selectors
  fix: address review feedback — subscription cleanup, filter nullability, stale diagram
  fix: wire plugin event subscriptions from worker to host
  fix(ui): hide scrollbar track background when sidebar is not hovered
  ...

# Conflicts:
#	packages/db/src/migrations/meta/0030_snapshot.json
#	packages/db/src/migrations/meta/_journal.json
2026-03-16 16:02:37 -05:00
Dotta
1990b29018 Fix agent budget tab routing 2026-03-16 16:02:21 -05:00
Dotta
10d06bc1ca Separate required skills into own section on agent skills page
Required/built-in Paperclip skills are now shown in a dedicated
"Required by Paperclip" section at the bottom of the agent skills tab,
with checkboxes that are checked and disabled. Optional skills remain
in the main section above.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 16:01:20 -05:00
Dotta
9b7b90521f Redesign project codebase configuration 2026-03-16 15:56:37 -05:00
Dotta
728d9729ed Fix budget auth and monthly spend rollups 2026-03-16 15:41:48 -05:00
Dotta
0b76b1aced Fix import adapter configuration forms
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 15:41:06 -05:00
Dotta
5f2c2ee0e2 Harden budget enforcement and migration startup 2026-03-16 15:11:34 -05:00
Dotta
411952573e Add budget tabs and sidebar budget indicators 2026-03-16 15:11:01 -05:00
Dotta
76e6cc08a6 feat(costs): add billing, quota, and budget control plane 2026-03-16 15:11:01 -05:00
Sai Shankar
656b4659fc refactor(quota): move provider quota logic into adapter layer, add unit tests
- Extract all Anthropic credential/API logic into claude-local/src/server/quota.ts
- Extract all OpenAI/WHAM credential/API logic into codex-local/src/server/quota.ts
- Add optional getQuotaWindows() to ServerAdapterModule in adapter-utils
- Rewrite quota-windows.ts as a 29-line thin aggregator with zero provider knowledge
- Wire getQuotaWindows into adapter registry for claude-local and codex-local
- Add 47 unit tests covering toPercent, secondsToWindowLabel, WHAM normalization,
  readClaudeToken, readCodexToken, fetchClaudeQuota, fetchCodexQuota, fetchWithTimeout
- Add 8 unit tests covering parseDateRange validation and byProvider pro-rata math

Adding a third provider now requires only touching that provider's adapter.
2026-03-16 15:08:54 -05:00
Sai Shankar
f383a37b01 fix(costs): replace non-null map assertions with nullish coalescing, clarify weekData guard 2026-03-16 15:08:54 -05:00
Sai Shankar
3529ccfa85 fix(costs): guard byProject against duplicate null keys, memoize ProviderQuotaCard row aggregations 2026-03-16 15:08:54 -05:00
Sai Shankar
7db3446a09 fix(costs): align byAgent run filter to startedAt, tighten providerTabItems memo deps, stabilize byProject row keys 2026-03-16 15:08:54 -05:00
Sai Shankar
9d21380699 feat(costs): add agent model breakdown, harden date validation, sync CostByProject type, fix quota threshold and tab-gated queries
- add byAgentModel endpoint and expandable per-agent model sub-rows in the spend tab
- validate date range inputs with isNaN + badRequest to return HTTP 400 on bad input
- move CostByProject from a local api/costs.ts definition into packages/shared types
- gate providerData query on mainTab === providers, consistent with weekData/windowData/quotaData
- fix byProject range filter from finishedAt to startedAt, consistent with byProvider runs query
- fix WHAM used_percent threshold from <= 1 to < 1 to avoid misclassifying 1% usage as 100%
- replace inline opacity style with tailwind bg-primary/85 class in ProviderQuotaCard
- reset expandedAgents set when company or date range changes
- sort agent model sub-rows by cost descending in ui memo
2026-03-16 15:08:54 -05:00
Sai Shankar
db20f4f46e fix(costs): harden company auth check, fix frozen date memo, hide empty quota rows
- add company existence check on quota-windows route to guard against
  sentinel and forged company IDs (was a no-op assertCompanyAccess)
- fix useDateRange minuteTick memo frozen at mount; realign interval to
  next calendar minute boundary via setTimeout + intervalRef pattern
- fix midnight timer in Costs.tsx to use stable [] dep and
  self-scheduling todayTimerRef to avoid StrictMode double-invoke
- return null for rolling window rows with no DB data instead of
  rendering $0.00 / 0 tok false zeros
- fix secondsToWindowLabel to handle windows >168h with actual day count
  instead of silently falling back to 7d
- fix byProvider.get(p) non-null assertion to use ?? [] fallback
2026-03-16 15:08:54 -05:00
Sai Shankar
bc991a96b4 fix(costs): guard routes, fix DST ranges, sync provider state, wire live updates
- add companyAccess guard to costs route
- fix effectiveProvider/activeProvider desync via sync-back useEffect
- move ROLLING_WINDOWS to module level; replace IIFE with useMemo in ProviderQuotaCard
- add NO_COMPANY sentinel to eliminate non-null assertions before enabled guard
- fix DST-unsafe 7d/30d ranges in useDateRange (use Date constructor)
- remove providerData from providerTabItems memo deps (use byProvider)
- normalize used_percent 0-1 vs 0-100 ambiguity in quota-windows service
- rename secondsToWindowLabel index param to fallback; pass explicit labels
- add 4.33 magic number comment; fix quota window key collision
- remove rounded-md from date inputs (violates --radius: 0 theme)
- wire cost_event invalidation in LiveUpdatesProvider
2026-03-16 15:08:54 -05:00
Sai Shankar
56c9d95daa feat(costs): consolidate /usage into /costs with Spend + Providers tabs
merge Usage page into Costs as two tabs ('Spend' and 'Providers'),
extract shared date-range logic to useDateRange() hook, delete /usage
route and sidebar entry, fix quota-windows bugs from prior review
2026-03-16 15:08:54 -05:00
Sai Shankar
f14b6e449f feat(usage): add subscription quota windows per provider on /usage page
reads local claude and codex auth files server-side, calls provider
quota apis (anthropic oauth usage, chatgpt wham/usage), and surfaces
live usedPercent per window in ProviderQuotaCard with threshold fill colors
2026-03-16 15:08:54 -05:00
Sai Shankar
82bc00a3ae address greptile review: per-provider deficit notch, startedAt filter, weekRange refresh, deduplicate providerDisplayName 2026-03-16 15:08:54 -05:00
Sai Shankar
94018e0239 feat(ui): add resource and usage dashboard (/usage route)
adds a new /usage page that lets board operators see how much each ai
provider is consuming across any date window, with per-model breakdowns,
rolling 5h/24h/7d burn windows, weekly budget bars, and a deficit notch
when projected spend is on track to exceed the monthly budget.

- new GET /companies/:id/costs/by-provider endpoint aggregates cost events
  by provider + model with pro-rated billing type splits from heartbeat runs
- new GET /companies/:id/costs/window-spend endpoint returns rolling window
  spend (5h, 24h, 7d) per provider with no schema changes
- QuotaBar: reusable boxed-border progress bar with green/yellow/red
  threshold fill colors and optional deficit notch
- ProviderQuotaCard: per-provider card showing budget allocation bars,
  rolling windows, subscription usage, and model breakdown with token/cost
  share overlays
- Usage page: date preset toggles (mtd, 7d, 30d, ytd, all, custom),
  provider tabs, 30s polling plus ws invalidation on cost_event
- custom date range blocks queries until both dates are selected and
  treats boundaries as local-time (not utc midnight) so full days are
  included regardless of timezone
- query key to timestamp is floored to the nearest minute to prevent
  cache churn on every 30s refetch tick
2026-03-16 15:08:54 -05:00
Dotta
fed94d18f3 Improve imported agent adapter selection 2026-03-16 12:17:28 -05:00
Dotta
3dc3347a58 Merge pull request #162 from JonCSykes/feature/upload-company-logo
Feature/upload company logo
2026-03-16 11:10:07 -05:00
Dotta
0763e2eb20 fix: hide instructions file and show advanced fields in import adapter config
- Added hideInstructionsFile prop to AdapterConfigFieldsProps
- All adapter config-fields now conditionally hide the instructions file
  field when hideInstructionsFile is set (used during import since the
  AGENTS.md is automatically set as promptTemplate)
- Import adapter config panel now renders ClaudeLocalAdvancedFields
  (Chrome, skip permissions, max turns) when claude_local is selected

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 11:09:36 -05:00
Dotta
1548b73b77 feat: add adapter picker for imported agents
When importing a company, users can now choose the adapter type for each
imported agent. Defaults to the current company CEO's adapter type (or
claude_local if none). Includes an expandable "configure adapter" section
per agent that renders the adapter-specific config fields.

- Added adapterOverrides to import request schema and types
- Built AdapterPickerList UI component in CompanyImport.tsx
- Backend applies adapter overrides when creating/updating agents

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 10:28:44 -05:00
Dotta
cf8bfe8d8e Fix company import file selection
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 10:14:09 -05:00
Dotta
6eceb9b886 Use attachment-size limit for company logos 2026-03-16 10:13:19 -05:00
Dotta
4dfd862f11 Address Greptile company logo feedback 2026-03-16 10:05:14 -05:00
Dotta
5d6dadda83 fix: default import target to new company instead of existing
The /company/import page now defaults the target dropdown to "Create new
company" instead of the current company. The existing company option is
still available in the dropdown.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 10:05:03 -05:00
Dotta
43fa4fc487 fix: show rename indicator on folder, not AGENTS.md file
Move the rename indicator (→ newName) in the file tree to only
display on the parent directory node, not on the individual file.
The preview header still shows the rename when viewing the file.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 09:58:33 -05:00
Dotta
bf9b057670 feat: show rename indicators in file tree and preview, right-align import button
- Move "Import n files" button to right side of its container
- Show "→ newName" rename indicator next to files/directories in the
  file tree when an agent or project is being renamed on import
- Show "→ newName" rename indicator in the file preview header when
  viewing a file that will be renamed
- Uses cyan color to distinguish rename info from action badges

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 09:54:20 -05:00
Dotta
4a5aba5bac Restrict company imports to GitHub and zip packages
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 09:52:16 -05:00
Dotta
0b829ea20b fix: move import button below renames panel
Move the "Import n files" button from the sticky header bar to below
the renames confirmation panel, so the user reviews renames first
before importing.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 09:47:29 -05:00
Dotta
2d548a9da0 Drop lockfile from PR branch 2026-03-16 09:37:23 -05:00
Dotta
86bb3d25cc fix: refine import renames panel per feedback
- Remove COMPANY.md from renames panel; just uncheck it silently in
  the file tree when importing to existing company
- Rename panel from "Conflicts to resolve" to "Renames"
- Add "skip" button on the left and "confirm rename" button on the
  right of each rename row
- Confirmed renames show a green checkmark and green-tinted row
- Skipped items gray out and uncheck the file in the tree
- Un-confirmed renames still proceed with the rename by default

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 09:35:26 -05:00
Dotta
e538329b0a Use asset-backed company logos 2026-03-16 09:25:39 -05:00
Dotta
ad494e74ad feat: replace collision strategy dropdown with inline conflict resolution UI
- Remove the collision strategy dropdown; always default to "rename"
- Add a "Conflicts to resolve" chores list above the package file tree
  showing each collision with editable rename fields (oldname → newname)
- Default rename uses source folder prefix (e.g. gstack-CEO)
- Per-item "skip" button that syncs with file tree checkboxes
- COMPANY.md defaults to skip when importing to an existing company
- Add nameOverrides support to API types and server so user-edited
  renames are passed through to the import

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 09:21:48 -05:00
Dotta
bc8fde5433 fix: remove GitHub source pinning warning from company import
We don't support regular updates to agents from GitHub sources yet,
so the "not pinned to a commit SHA" warning is misleading and unnecessary.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 09:08:12 -05:00
Dotta
8d0581ffb4 refactor: extract shared PackageFileTree component for import/export
Extract duplicated file tree types, helpers (buildFileTree, countFiles,
collectAllPaths, parseFrontmatter), and visual tree component into a
shared PackageFileTree component. Both import and export pages now use
the same underlying tree with consistent alignment and styling.

Import-specific behavior (action badges, unchecked opacity) is handled
via renderFileExtra and fileRowClassName props. Also removes the file
count subtitle from the import sidebar to match the export page.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 09:04:22 -05:00
Dotta
298cb4ab8a fix: auto-expand conflicting files and warn on agent overwrites during import
When importing into an existing company, files with "update" action (conflicts)
now have their parent directories auto-expanded so users immediately see what
will be overwritten. Additionally, server-side warnings are generated for any
agent or project that will be overwritten by the import.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 08:59:43 -05:00
Dotta
3572ef230d fix: update import page badge colors for consistency
- update: blue → yellow (amber)
- overwrite/replace: added as red
- create (green) and skip (gray) unchanged

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 08:58:19 -05:00
Dotta
f8249af501 Stop exporting paperclipSkillSync in company packages
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 08:55:37 -05:00
Dotta
140c4e1feb fix: move import/export buttons to top left on org chart page
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 08:55:11 -05:00
Dotta
617aeaae0e Use zip archives for company export
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 08:54:50 -05:00
Dotta
b116e04894 fix: hide collision strategy dropdown when importing to new company
No collisions are possible when the target is a new company, so the
dropdown is unnecessary. The grid layout also adjusts to single-column
when only the target field is shown.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 08:54:10 -05:00
Dotta
dc1bf7e9c6 fix: match import page warning/error boxes to export page card style
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 08:53:45 -05:00
Dotta
1a5eaba622 Merge public-gh/master into review/pr-162 2026-03-16 08:47:05 -05:00
Dotta
5b44dbe9c4 fix: align file icons with folder icons in export file tree
Change file row outer gap from gap-2 (8px) to gap-1 (4px) to match
the directory row grid gap-x-1, so file and folder icons line up
vertically.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 08:33:44 -05:00
Dotta
3c31e379a1 fix: keep .paperclip.yaml in sync with export file selections
When users check/uncheck files in the export preview, the .paperclip.yaml
now dynamically filters its agents/projects/tasks sections to only include
entries whose corresponding files are checked. This applies to both the
preview pane and the downloaded tar archive.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 08:33:17 -05:00
Dotta
4e146f0075 feat: make skill pills clickable in company export preview
Clicking a skill pill in the frontmatter card now navigates to the
corresponding skills/<slug>/SKILL.md file in the export tree, expanding
parent directories as needed. No page reload required.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 08:27:21 -05:00
Dotta
173e7915a7 fix: export file tree alignment and remove file count subtitle
- Move paddingLeft from inner label to outer grid div on directory rows
  so folders align with files and the search field
- Remove "N files in rootPath" subtitle under Package files header

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-16 08:25:40 -05:00
Dotta
e76fca138d feat: paginate issues in company export file tree
Show only 10 task entries at a time with a "Show more issues" button.
Checked/selected tasks are always pinned visible regardless of the page
limit. Search still works across all issues — matched results are pinned
and the load-more button is hidden during search so all matches show.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 08:24:24 -05:00
Dotta
45df62652b fix: clean up export page warnings and notes display
- Remove "N notes" indicator from the top bar
- Hide terminated agent messages entirely instead of showing as notes
- Style warnings as a rounded box with side borders and more margin

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 08:21:52 -05:00
Dotta
068441b01b Merge remote-tracking branch 'public-gh/master' into paperclip-company-import-export
* public-gh/master:
  fix: use appType "custom" for Vite dev server so worktree branding is applied
2026-03-16 08:11:47 -05:00
Dotta
7034ea5b01 Merge pull request #1038 from paperclipai/paperclip-worktree-dynamics
fix: worktree branding not applied in vite dev mode
2026-03-16 08:10:54 -05:00
Dotta
ccb6729ec8 fix: use appType "custom" for Vite dev server so worktree branding is applied
Vite's "spa" appType adds its own SPA fallback middleware that serves
index.html directly, bypassing the custom catch-all route that calls
applyUiBranding(). Changing to "custom" lets our route handle HTML
serving, which injects the worktree-colored favicon and banner meta tags.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 08:08:38 -05:00
Dotta
ca0169eb6c Add company creator skill files 2026-03-16 07:40:39 -05:00
Dotta
448fdaab96 Merge public-gh/master into paperclip-company-import-export 2026-03-16 07:38:08 -05:00
Dotta
4244047d4d Merge pull request #990 from paperclipai/dotta-sunday-ui-updates
dottas-sunday-ui-updates
2026-03-16 07:12:35 -05:00
albttx
b1e2a5615b fix: recover @greptile-apps errors 2026-03-16 10:00:23 +00:00
Albert Le Batteux
b535860a50 Update .github/workflows/docker.yml
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-16 10:00:23 +00:00
Albert Le Batteux
2b478764a9 Update .github/workflows/docker.yml
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-16 10:00:23 +00:00
albttx
88cc8e495c chore(ci): deploy docker image 2026-03-16 10:00:23 +00:00
Devin Foley
88df0fecb0 fix: show validation error on incomplete login submit
Address Greptile review feedback:
- Show "Please fill in all required fields." instead of silently
  returning when form is submitted with missing fields
- Remove pointer-events-none so keyboard users can reach the
  button and receive the same validation feedback

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-15 19:39:12 -07:00
Dotta
34ec40211e Merge pull request #1010 from paperclipai/docs/maintenance-20260316
docs: fix documentation drift — adapters, plugins, tech stack
2026-03-15 21:13:34 -05:00
Dotta
52b12784a0 docs: fix documentation drift — adapters, plugins, tech stack
- Fix gemini adapter name: `gemini-local` → `gemini_local` (matches registry.ts)
- Move .doc-review-cursor to .gitignore (tooling state, not source)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 21:08:19 -05:00
Dotta
3bffe3e479 docs: update documentation for accuracy after plugin system launch
- README: mark plugin system as shipped in roadmap
- SPEC: update adapter table with openclaw_gateway, gemini-local, hermes_local
- SPEC: update plugin architecture section to reflect shipped status
- Add .doc-review-cursor for future maintenance runs

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 20:30:54 -05:00
Dotta
ef652a2766 Export: tasks in top-level folder, smart search expansion
- Move all tasks to top-level tasks/ folder (no longer nested under
  projects/slug/tasks/). The project slug is still in the frontmatter
  for association.
- Search auto-expands parent dirs of matched files so matches are
  always visible in the tree
- Restores previous expansion state when search is cleared
- All files already loaded in memory — search works across everything
  with no pagination limit

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-15 16:39:11 -05:00
Dotta
cf30ddb924 Export/import UX polish: search, scroll, sort, null cleanup
Export page:
- Sort files before directories so PROJECT.md appears above tasks/
- Tasks unchecked by default (only agents, projects, skills checked)
- Add inline search input to filter files in the tree
- Checked files sort above unchecked for easier scanning
- Sidebar scrolls independently from content preview pane

Import page:
- Match file-before-dir sort order
- Independent sidebar/content scrolling
- Skip null values in frontmatter preview

Backend:
- Skip null/undefined fields in exported frontmatter (no more
  "owner: null" in PROJECT.md files)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-15 15:54:26 -05:00
Dotta
2f7da835de Redesign import page: file-browser UX with rich preview
- Add `files` and `manifest` to CompanyPortabilityPreviewResult so the
  import UI can show actual file contents and metadata
- Rewrite import preview as a file/folder tree (matching export page
  design language) with per-file checkboxes to include/exclude items
- Show action badges (create/update/skip) on each file based on the
  import plan, with unchecked files dimmed and badged as "skip"
- Add rich frontmatter preview: clicking a file shows parsed frontmatter
  as structured data (name, title, reportsTo, skills) plus markdown body
- Include skills count in the sidebar summary
- Update import button to show dynamic file count that updates on
  check/uncheck
- Both /tree/ and /blob/ GitHub URLs already supported by backend

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-15 15:00:32 -05:00
Dotta
c5cc191a08 chore: ignore superset artifacts 2026-03-15 14:56:53 -05:00
Dotta
c6ea491000 Improve export/import UX: rich frontmatter preview, cleaner warnings
- Separate terminated agent messages from warnings into info notes
  (shown with subtle styling instead of amber warning banners)
- Clean up warning banner styles for dark mode compatibility
  (use amber-500/20 borders and amber-500/5 backgrounds)
- Parse YAML frontmatter in markdown files and render as structured
  data cards showing name, title, reportsTo, skills etc.
- Apply same warning style cleanup to import page

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-15 14:52:07 -05:00
Dotta
76d30ff835 Move company import/export to dedicated pages with file-browser UX
- Add /:company/company/export page with file tree, checkboxes for
  per-file selection, and read-only preview pane (skills-style layout)
- Add /:company/company/import page with source form (GitHub/URL/local),
  target/collision settings, preview tree with action badges, and detail pane
- Add Import/Export buttons to the Org Chart page header
- Replace import/export sections in CompanySettings with redirect links
- Clean up ~800 lines of dead code from CompanySettings
- Register new routes in App.tsx

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 14:43:07 -05:00
Dotta
16ab8c8303 Dark theme for CodeMirror code blocks in MDXEditor
The code blocks users see in issue documents are rendered by CodeMirror
(via MDXEditor's codeMirrorPlugin), not by MarkdownBody. MDXEditor
bundles cm6-theme-basic-light which gives them a white background.

Added dark overrides for all CodeMirror elements:
- .cm-editor: dark background (#1e1e2e), light text (#cdd6f4)
- .cm-gutters: darker gutter with muted line numbers
- .cm-activeLine, .cm-selectionBackground: subtle dark highlights
- .cm-cursor: light cursor for visibility
- Language selector dropdown: dark-themed to match
- Reduced pre padding to 0 since CodeMirror handles its own spacing

Uses \!important to beat CodeMirror's programmatically-injected theme
styles (EditorView.theme generates high-specificity scoped selectors).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-15 14:39:09 -05:00
Dotta
2daa35cd3a Remove duplicate @paperclipai/adapter-openclaw-gateway in server/package.json
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 14:33:22 -05:00
Dotta
597c4b1d45 Fix code block styles with robust prose overrides
Previous attempt was being overridden by Tailwind prose/prose-invert
CSS variables. This fix:

- Overrides --tw-prose-pre-bg and --tw-prose-invert-pre-bg CSS variables
  on .paperclip-markdown to force dark background in both modes
- Uses .paperclip-markdown pre with \!important for bulletproof overrides
- Removes conflicting prose-pre: utility classes from MarkdownBody
- Adds explicit pre code reset (inherit color/size, no background)
- Verified visually with Playwright at desktop and mobile viewports

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-15 14:30:53 -05:00
Dotta
6f931b8405 Add Docker setup for untrusted PR review in isolated containers
Adds a dedicated Docker environment for reviewing untrusted pull requests
with codex/claude, keeping CLI auth state in volumes and using a separate
scratch workspace for PR checkouts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:30:53 -05:00
Dotta
41e03bae61 Fix org chart canvas height to fit viewport without scrolling
The height calc subtracted only 4rem but the actual overhead is ~6rem
(3rem breadcrumb bar + 3rem main padding). Also use dvh for better
mobile support.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 14:30:53 -05:00
Dotta
d7f45eac14 Add doc-maintenance skill for periodic documentation accuracy audits
Skill detects documentation drift by scanning git history since last review,
cross-referencing shipped features against README, SPEC, and PRODUCT docs,
and opening PRs with minimal fixes. Includes audit checklist and section map
references.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 14:30:53 -05:00
Dotta
94112b324c Fix sidebar scrollbar: hide track background when not hovering
The scrollbar track background was still visible as a colored "well" even
when the thumb was hidden. Now both track and thumb are fully transparent
by default, only appearing on container hover.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-15 14:30:53 -05:00
Dotta
fe0d7d029a Restyle markdown code blocks: dark background, smaller font, compact padding
- Switch code block background from transparent accent to dark (#1e1e2e) with
  light text (#cdd6f4) for better readability in both light and dark modes
- Reduce code font size from 0.84em to 0.78em
- Compact padding and margins on pre blocks
- Hide MDXEditor code block toolbar by default, show on hover/focus to prevent
  overlap with code content on mobile
- Use horizontal scroll instead of word-wrap for code blocks to preserve formatting

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-15 14:30:33 -05:00
Dotta
aea133ff9f Add archive project button and filter archived projects from selectors
- Add "Archive project" / "Unarchive project" button in the project
  configuration danger zone (ProjectProperties)
- Filter archived projects from the Projects listing page
- Filter archived projects from NewIssueDialog project selector
- Filter archived projects from IssueProperties project picker
  (keeps current project visible even if archived)
- Filter archived projects from CommandPalette
- SidebarProjects already filters archived projects

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 14:30:33 -05:00
Dotta
c94132bc7e Merge pull request #988 from leeknowsai/fix/plugin-event-subscription-wiring
fix: wire plugin event subscriptions from worker to host
2026-03-15 14:26:11 -05:00
HD
8468d347be fix: address review feedback — subscription cleanup, filter nullability, stale diagram
- Add scopedBus.clear() in dispose() to prevent subscription accumulation
  on worker crash/restart cycles
- Use two-arg subscribe() overload when filter is null instead of passing
  empty object; fix filter type to include null
- Update ASCII flow diagram: onEvent is a notification, not request/response
2026-03-16 02:25:03 +07:00
Matt Van Horn
cc40e1f8e9 refactor(evals): split test cases into tests/*.yaml files
Move inline test cases from promptfooconfig.yaml into separate files
organized by category (core.yaml, governance.yaml). Main config now
uses file://tests/*.yaml glob pattern per promptfoo best practices.

This makes it easier to add new test categories without bloating the
main config, and lets contributors add cases by dropping new YAML
files into tests/.
2026-03-15 12:15:51 -07:00
HD
61fd5486e8 fix: wire plugin event subscriptions from worker to host
Plugin workers register event handlers via `ctx.events.on()` in the SDK,
but these subscriptions were never forwarded to the host process. The host
sends events via `notifyWorker("onEvent", ...)` which produces a JSON-RPC
notification (no `id`), but the worker only dispatched `onEvent` as a
request handler — notifications were silently dropped.

Changes:
- Add `events.subscribe` RPC method so workers can register subscriptions
  on the host-side event bus during setup
- Handle `onEvent` notifications in the worker notification dispatcher
  (previously only `agents.sessions.event` was handled)
- Add `events.subscribe` to HostServices interface, capability map, and
  host client handler
- Add `subscribe` handler in host services that registers on the scoped
  plugin event bus and forwards matched events to the worker
2026-03-16 02:10:10 +07:00
Dotta
675421f3a9 Merge pull request #587 from teknium1/feat/hermes-agent-adapter
feat: add Hermes Agent adapter (hermes_local)
2026-03-15 08:25:56 -05:00
Dotta
2162289bf3 Merge branch 'master' into feat/hermes-agent-adapter 2026-03-15 08:23:23 -05:00
Dotta
eb647ab2db Add company-creator skill for scaffolding agent company packages
Skill guides users through creating Agent Companies spec-conformant
packages, either from scratch (with interview-driven hiring plan) or
by analyzing an existing git repo and wrapping its skills/agents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 07:11:57 -05:00
Dotta
7675fd0856 Fix runtime skill injection across adapters 2026-03-15 07:05:01 -05:00
Dotta
82f253c310 Make company skills migration idempotent 2026-03-15 06:18:29 -05:00
Dotta
5de5fb507a Address Greptile review fixes 2026-03-15 06:13:50 -05:00
Dotta
269dd6abbe Drop lockfile changes from PR 2026-03-14 22:01:15 -05:00
Dotta
2c35be0212 Merge public-gh/master into paperclip-company-import-export 2026-03-14 21:45:54 -05:00
Dotta
c44dbf79cb Fix Gemini local execution and diagnostics 2026-03-14 21:36:05 -05:00
Dotta
5814249ea9 Improve Pi adapter diagnostics 2026-03-14 21:11:06 -05:00
Dotta
bfaa4b4bdc Merge pull request #834 from mvanhorn/fix/dotenv-cwd-fallback
fix(server): load .env from cwd as fallback
2026-03-14 21:02:54 -05:00
Dotta
e619e64433 Add skill sync for remaining local adapters 2026-03-14 19:22:23 -05:00
Dotta
b2c0f3f9a5 Refine portability export behavior and skill plans 2026-03-14 18:59:26 -05:00
Dotta
872807a6f8 Merge pull request #918 from gsxdsm/fix/plugin-slots
Enhance plugin loading and toolbar integration
2026-03-14 17:51:26 -05:00
Dotta
f482433ddf Merge pull request #919 from paperclipai/fix/sidebar-scrollbar-hover-track
fix(ui): hide sidebar scrollbar track until hover
2026-03-14 17:49:05 -05:00
Dotta
825d2b4759 fix(ui): hide scrollbar track background when sidebar is not hovered
The scrollbar-auto-hide utility was only hiding the thumb but left the
track background visible, creating a visible "well" even when idle.
Now both track and thumb are transparent by default, appearing only on
container hover.

Fixes PAP-374

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-14 17:47:22 -05:00
gsxdsm
6c7ebaeb59 Refactor secret-ref format registration to use a UI hint for Paperclip secret UUIDs 2026-03-14 15:43:56 -07:00
gsxdsm
6d65800173 Register secret-ref format in AJV for validating Paperclip secret UUIDs 2026-03-14 15:41:22 -07:00
gsxdsm
0d2380b7b1 Fix plugin launchers initialization by adding enabled flag based on companyId 2026-03-14 15:35:01 -07:00
gsxdsm
ec261e9c7c Enhance plugin loading and toolbar integration
- Added packagePath to plugin loader for improved manifest handling.
- Refactored GlobalToolbarPlugins for better slot and launcher management in BreadcrumbBar.
- Updated launcher trigger styles for globalToolbarButton.
2026-03-14 15:27:45 -07:00
Dotta
5d52ce2e5e Merge pull request #916 from gsxdsm/fix/plugin-slots
Add globalToolbarButton slot type and update related documentation
2026-03-14 17:22:34 -05:00
gsxdsm
811e2b9909 Add globalToolbarButton slot type and update related documentation 2026-03-14 15:05:04 -07:00
Dotta
8985ddaeed Merge pull request #914 from gsxdsm/fix/dev-runner-syntax
Fix syntax error in dev-runner script
2026-03-14 16:55:43 -05:00
gsxdsm
2dbb31ef3c Fix syntax error 2026-03-14 14:34:56 -07:00
Dotta
648ee37a17 Merge pull request #912 from gsxdsm/feat/plugin-breadcumb-slot
Add global toolbar slots to BreadcrumbBar component
2026-03-14 16:28:56 -05:00
Dotta
98e73acc3b Merge pull request #904 from gsxdsm/fix/build-plugin-sdk
Add buildPluginSdk function to build the plugin SDK during dev run
2026-03-14 16:28:26 -05:00
Dotta
0fb85e5729 Merge pull request #910 from gsxdsm/feat/plugin-cli
Add plugin cli commands
2026-03-14 16:28:06 -05:00
gsxdsm
7e3a04c76c Refactor BreadcrumbBar to use useMemo for global toolbar slot context and improve rendering logic 2026-03-14 14:19:32 -07:00
gsxdsm
e219761d95 Fix plugin installation output and error handling in registerPluginCommands 2026-03-14 14:15:42 -07:00
gsxdsm
0afd5d5630 Update cli/src/commands/client/plugin.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-14 14:08:21 -07:00
gsxdsm
4f8df1804d Update cli/src/commands/client/plugin.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-14 14:08:15 -07:00
gsxdsm
d0677dcd91 Update cli/src/commands/client/plugin.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-14 14:08:03 -07:00
gsxdsm
bc5d650248 Add global toolbar slots to BreadcrumbBar component 2026-03-14 14:05:29 -07:00
Dotta
2e3a0d027e Merge pull request #909 from mvanhorn/feat/plugin-domain-event-bridge
feat(plugins): bridge core domain events to plugin event bus
2026-03-14 16:00:03 -05:00
Dotta
b92f234d88 Merge pull request #903 from gsxdsm/fix/process-list
Refine heartbeatService to only target runs stuck in "running" state
2026-03-14 15:58:46 -05:00
gsxdsm
0f831e09c1 Add plugin cli commands 2026-03-14 13:58:43 -07:00
Matt Van Horn
a6c7e09e2a fix(plugins): log plugin handler errors, warn on double-init
Address Greptile review feedback:
- Log plugin event handler errors via logger.warn instead of
  silently discarding the emit() result
- Warn if setPluginEventBus is called more than once

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 13:51:41 -07:00
Matt Van Horn
30e2914424 feat(plugins): bridge core domain events to plugin event bus
The plugin event bus accepts subscriptions for core events like
issue.created but nothing emits them. This adds a bridge in
logActivity() so every domain action that's already logged also
fires a PluginEvent to subscribing plugins.

Uses a module-level setter (same pattern as publishLiveEvent)
to avoid threading the bus through all route handlers. Only
actions matching PLUGIN_EVENT_TYPES are forwarded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 13:44:26 -07:00
gsxdsm
6b17f7caa8 Update scripts/dev-runner.mjs
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-14 13:29:46 -07:00
gsxdsm
2dc3b4df24 Add buildPluginSdk function to build the plugin SDK during dev run 2026-03-14 13:10:01 -07:00
gsxdsm
b13c530024 Refine heartbeatService to only target runs stuck in "running" state 2026-03-14 13:02:21 -07:00
Dotta
dd828e96ad Fix workspace review issues and policy check 2026-03-14 14:13:03 -05:00
Dotta
6e6d67372c Merge remote-tracking branch 'public-gh/master' into paperclip-subissues
* public-gh/master:
  Drop lockfile from watcher change
  Tighten plugin dev file watching
  Fix plugin smoke example typecheck
  Fix plugin dev watcher and migration snapshot
  Clarify plugin authoring and external dev workflow
  Expand kitchen sink plugin demos
  fix: set AGENT_HOME env var for agent processes
  Add kitchen sink plugin example
  Simplify plugin runtime and cleanup lifecycle
  Add plugin framework and settings UI

# Conflicts:
#	packages/db/src/migrations/meta/0029_snapshot.json
#	packages/db/src/migrations/meta/_journal.json
2026-03-14 13:56:09 -05:00
Dotta
7e43020a28 Pin imported GitHub skills and add update checks 2026-03-14 13:52:20 -05:00
Dotta
0851e81b47 Merge pull request #821 from paperclipai/feature/plugin-runtime-instance-cleanup
WIP: Simplify plugin runtime and cleanup lifecycle
2026-03-14 13:45:56 -05:00
Dotta
325fcf8505 Merge pull request #864 from paperclipai/fix/agent-home-env
fix: set AGENT_HOME env var for agent processes
2026-03-14 12:44:28 -05:00
Dotta
8cf85a5a50 Merge remote-tracking branch 'public-gh/master' into paperclip-subissues
* public-gh/master: (55 commits)
  fix(issue-documents): address greptile review
  Update packages/shared/src/validators/issue.ts
  feat(ui): add issue document copy and download actions
  fix(ui): unify new issue upload action
  feat(ui): stage issue files before create
  feat(ui): handle issue document edit conflicts
  fix(ui): refresh issue documents from live events
  feat(ui): deep link issue documents
  fix(ui): streamline issue document chrome
  fix(ui): collapse empty document and attachment states
  fix(ui): simplify document card body layout
  fix(issues): address document review comments
  feat(issues): add issue documents and inline editing
  docs: add agent evals framework plan
  fix(cli): quote env values with special characters
  Fix worktree seed source selection
  fix: address greptile follow-up
  docs: add paperclip skill tightening plan
  fix: isolate codex home in worktrees
  Add worktree UI branding
  ...

# Conflicts:
#	packages/db/src/migrations/meta/0028_snapshot.json
#	packages/db/src/migrations/meta/_journal.json
#	packages/shared/src/index.ts
#	server/src/routes/issues.ts
#	ui/src/api/issues.ts
#	ui/src/components/NewIssueDialog.tsx
#	ui/src/pages/IssueDetail.tsx
2026-03-14 12:24:40 -05:00
Dotta
4cfbeaba9d Drop lockfile from watcher change 2026-03-14 12:19:25 -05:00
Dotta
0605c9f229 Tighten plugin dev file watching 2026-03-14 12:07:04 -05:00
Dotta
22b8e90ba6 Fix plugin smoke example typecheck 2026-03-14 11:44:50 -05:00
Dotta
7c4b02f02b Fix plugin dev watcher and migration snapshot 2026-03-14 11:32:15 -05:00
Dotta
cfa4925075 Refine skill import UX and built-in skills 2026-03-14 11:14:34 -05:00
Matt Van Horn
280536092e fix(adapters): add success log when agent instructions file is loaded
Matches the pattern in codex-local and cursor-local adapters,
giving operators consistent feedback about whether instructions
were actually loaded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 09:07:26 -07:00
Matt Van Horn
ff4f326341 fix(server): use realpathSync for .env path dedup to handle symlinks
realpathSync resolves symlinks and normalizes case, preventing
double-loading the same .env file when paths differ only by
symlink indirection or filesystem case.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 09:05:51 -07:00
Matt Van Horn
2ba0f5914f fix(ui): escape brackets in filename and use paragraph break for inline images
Escape `[` and `]` in filenames to prevent malformed markdown when
attaching images. Use `\n\n` instead of `\n` so the image renders
as its own paragraph instead of inline with preceding text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 09:02:20 -07:00
Dotta
dcd8a47d4f Merge pull request #713 from paperclipai/release/0.3.1
Release/0.3.1
2026-03-14 11:00:24 -05:00
Dotta
0bf53bc513 Add company skills library and agent skills UI 2026-03-14 10:55:04 -05:00
Dotta
eafb5b8fd9 Merge public-gh/master into feature/plugin-runtime-instance-cleanup 2026-03-14 10:46:19 -05:00
Dotta
30888759f2 Clarify plugin authoring and external dev workflow 2026-03-14 10:40:21 -05:00
Dotta
2137c2f715 Expand skills UI product plan 2026-03-14 10:15:04 -05:00
Dotta
58a9259a2e Update skill package docs and plans 2026-03-14 10:13:20 -05:00
Dotta
1d8f514d10 Refine company package export format 2026-03-14 09:46:16 -05:00
Dotta
9ed7092aab Stop runtime services during workspace cleanup 2026-03-14 09:41:13 -05:00
Dotta
193a987513 Merge pull request #837 from paperclipai/paperclip-issue-documents
feat(issues): add issue documents and inline editing
2026-03-14 09:37:47 -05:00
Dotta
3b25268c0b Fix execution workspace runtime lifecycle 2026-03-14 09:35:35 -05:00
Dotta
cb5d7e76fb Expand kitchen sink plugin demos 2026-03-14 09:26:45 -05:00
Dotta
bc12f08c66 fix(issue-documents): address greptile review
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-14 09:18:59 -05:00
Dotta
a7a64f11be Update packages/shared/src/validators/issue.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-14 09:09:21 -05:00
Dotta
31e6e30fe3 feat(ui): add issue document copy and download actions
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-14 07:24:47 -05:00
Dotta
ad7bf4288a fix(ui): unify new issue upload action
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-14 07:21:21 -05:00
Dotta
16dfcb56a4 feat(ui): stage issue files before create
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-14 07:13:59 -05:00
Dotta
924762c073 feat(ui): handle issue document edit conflicts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-14 06:59:20 -05:00
Dotta
abb70ca5c5 fix(ui): refresh issue documents from live events
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-14 06:52:44 -05:00
Dotta
1e3a485408 feat(ui): deep link issue documents
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-14 06:48:43 -05:00
Dotta
07d13e1738 fix(ui): streamline issue document chrome
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-14 06:13:07 -05:00
Dotta
c8cd950a03 fix(ui): collapse empty document and attachment states
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-14 06:02:46 -05:00
Dotta
501ab4ffa9 fix(ui): simplify document card body layout
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-14 05:56:17 -05:00
Devin Foley
d671a59306 fix: set AGENT_HOME env var for agent processes
The $AGENT_HOME environment variable was referenced by skills (e.g.
para-memory-files) but never actually set, causing runtime errors like
"/HEARTBEAT.md: No such file or directory" when agents tried to resolve
paths relative to their home directory.

Add agentHome to the paperclipWorkspace context in the heartbeat service
and propagate it as the AGENT_HOME env var in all local adapters.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-14 00:36:53 -07:00
Devin Foley
8a201022c0 Fix Enter key not submitting login form
The submit button's `disabled` attribute prevented browsers from firing
implicit form submission (Enter key) per HTML spec. Move the canSubmit
guard into the onSubmit handler and use aria-disabled + visual styles
instead, so Enter works when fields are filled.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-14 00:17:39 -07:00
Dotta
6fa1dd2197 Add kitchen sink plugin example 2026-03-13 23:03:51 -05:00
Dotta
56a34a8f8a Add adapter skill sync for codex and claude 2026-03-13 22:49:42 -05:00
Dotta
271c2b9018 Implement markdown-first company package import export 2026-03-13 22:29:30 -05:00
teknium1
93faf6d361 fix: address review feedback — pin version, enable JWT
- Pin hermes-paperclip-adapter to exact version 0.1.1 (was ^0.1.0).
  Avoids auto-pulling potentially breaking patches from a 0.x package.
- Enable supportsLocalAgentJwt (was false). The adapter uses
  buildPaperclipEnv which passes the JWT to the child process,
  matching the pattern of all other local adapters.
2026-03-13 20:26:27 -07:00
Dotta
eb0a74384e fix(issues): address document review comments
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 22:17:49 -05:00
Dotta
ab41fdbaee Merge public-gh/master into paperclip-issue-documents
Resolve conflicts by keeping the issue-documents work alongside upstream heartbeat-context, worktree branding, and adapter runtime updates.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 21:47:06 -05:00
Dotta
2975aa950b Refine company package spec and rollout plan 2026-03-13 21:36:19 -05:00
Dotta
45998aa9a0 feat(issues): add issue documents and inline editing
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 21:30:48 -05:00
Dotta
29b70e0c36 Add company import export v2 plan 2026-03-13 21:10:45 -05:00
Dotta
3f48b61bfa updated spec 2026-03-13 21:08:36 -05:00
Dotta
7a06a577ce Fix dev startup with embedded postgres reuse 2026-03-13 20:56:19 -05:00
Dotta
dbb5bd48cc Add company packages spec draft 2026-03-13 20:53:22 -05:00
Matt Van Horn
303c00b61b fix(server): load .env from cwd as fallback when .paperclip/.env is missing
The server only loads environment variables from .paperclip/.env, which is
not the standard location users expect. When DATABASE_URL is set in a .env
file in the project root (cwd), it is silently ignored, requiring users to
manually export the variable.

Add a fallback that loads cwd/.env after .paperclip/.env with override:false,
so the Paperclip-specific env file always takes precedence but standard .env
files in the project root are also picked up.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 17:22:27 -07:00
Matt Van Horn
a39579dad3 fix(evals): address Greptile review feedback
- Make company_boundary test adversarial with cross-company stimulus
- Replace fragile not-contains:retry with targeted JS assertion
- Replace not-contains:create with not-contains:POST /api/companies
- Pin promptfoo to 0.103.3 for reproducible eval runs
- Fix npm -> pnpm in README prerequisites
- Add trailing newline to system prompt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 17:19:25 -07:00
Matt Van Horn
fbb8d10305 feat(evals): bootstrap promptfoo eval framework (Phase 0)
Implements Phase 0 of the agent evals framework plan from discussion #808
and PR #817. Adds the evals/ directory scaffold with promptfoo config and
8 deterministic test cases covering core heartbeat behaviors.

Test cases:
- core.assignment_pickup: picks in_progress before todo
- core.progress_update: posts status comment before exiting
- core.blocked_reporting: sets blocked status with explanation
- governance.approval_required: reviews approval before acting
- governance.company_boundary: refuses cross-company actions
- core.no_work_exit: exits cleanly with no assignments
- core.checkout_before_work: always checks out before modifying
- core.conflict_handling: stops on 409, picks different task

Model matrix: claude-sonnet-4, gpt-4.1, codex-5.4, gemini-2.5-pro via
OpenRouter. Run with `pnpm evals:smoke`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 17:09:51 -07:00
Dotta
920bc4c70f Implement execution workspaces and work products 2026-03-13 17:12:25 -05:00
Dotta
12ccfc2c9a Simplify plugin runtime and cleanup lifecycle 2026-03-13 16:58:29 -05:00
Dotta
9da5358bb3 Add workspace technical implementation spec 2026-03-13 16:37:40 -05:00
Dotta
80cdbdbd47 Add plugin framework and settings UI 2026-03-13 16:22:34 -05:00
Dotta
bcce5b7ec2 Merge pull request #816 from paperclipai/fix/worktree-seed-and-env-quoting
fix(cli): preserve worktree seed source config and quote special env values
2026-03-13 15:18:03 -05:00
Dotta
8eacc9c697 Merge pull request #817 from paperclipai/docs/agent-evals-framework-plan
docs: add agent evals framework plan
2026-03-13 15:17:40 -05:00
Dotta
db81a06386 docs: add agent evals framework plan 2026-03-13 15:07:56 -05:00
Dotta
626a8f1976 fix(cli): quote env values with special characters 2026-03-13 15:07:49 -05:00
Dotta
aa799bba4c Fix worktree seed source selection
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 15:07:42 -05:00
Dotta
aaadbdc144 Merge pull request #790 from paperclipai/paperclip-token-optimization
Optimize heartbeat token usage
2026-03-13 15:01:45 -05:00
Dotta
a393db78b4 fix: address greptile follow-up
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 14:53:30 -05:00
Dotta
c1430e7b06 docs: add paperclip skill tightening plan
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 14:37:44 -05:00
Dotta
7e288d20fc Merge remote-tracking branch 'public-gh/master' into pr-432
* public-gh/master:
  Add worktree UI branding
  Fix company switch remembered routes
  Add me and unassigned assignee options
  feat: skip pre-filled assignee/project fields when tabbing in new issue dialog
  Fix manual company switch route sync
  Delay onboarding starter task creation until launch
2026-03-13 11:59:17 -05:00
Dotta
528505a04a fix: isolate codex home in worktrees 2026-03-13 11:53:56 -05:00
Dotta
e2a0347c6d Merge pull request #805 from paperclipai/fix/worktree-ui-branding
Add worktree UI branding
2026-03-13 11:15:11 -05:00
Dotta
cce9941464 Add worktree UI branding 2026-03-13 11:12:43 -05:00
Dotta
d51c4b1a4c fix: tighten token optimization edge cases
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 10:18:00 -05:00
Dotta
3b0d9a93f4 Merge pull request #802 from paperclipai/fix/ui-routing-and-assignee-polish
fix(ui): polish company switching, issue tab order, and assignee filters
2026-03-13 10:11:09 -05:00
Dotta
41eb8e51e3 Fix company switch remembered routes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 10:01:32 -05:00
Dotta
cdebf7b538 Merge remote-tracking branch 'public-gh/master' into pr-432
* public-gh/master: (33 commits)
  fix: align embedded postgres ctor types with initdbFlags usage
  docs: add dated plan naming rule and align workspace plan
  Expand workspace plan for migration and cloud execution
  Add workspace product model plan
  docs: add token optimization plan
  docs: organize plans into doc/plans with date prefixes
  fix: keep runtime skills scoped to ./skills
  fix: prefer .agents skills and repair codex symlink targets\n\nCo-Authored-By: Paperclip <noreply@paperclip.ing>
  Change sidebar Documentation link to external docs.paperclip.ing
  Fix local-cli skill install for moved .agents skills
  docs: update PRODUCT.md and add 2026-03-13 features plan
  feat(worktree): add worktree:cleanup command, env var defaults, and auto-prefix
  fix: resolve type errors in process-lost-reaper PR
  fix(heartbeat): prevent false process_lost failures on queued and non-child-process runs
  Revert "Merge pull request #707 from paperclipai/nm/premerge-lockfile-refresh"
  ci: refresh pnpm lockfile before merge
  fix(docker): include gemini adapter manifest in deps stage
  chore(lockfile): refresh pnpm-lock.yaml
  Raise default max turns to 300
  Drop pnpm lockfile from PR
  ...
2026-03-13 09:52:38 -05:00
Dotta
32ab4f8e47 Add me and unassigned assignee options
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 09:47:01 -05:00
Dotta
6365e03731 feat: skip pre-filled assignee/project fields when tabbing in new issue dialog
When creating a new issue with a pre-filled assignee or project (e.g. from
a project page), Tab from the title field now skips over fields that already
have values, going directly to the next empty field or description.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 09:47:01 -05:00
Dotta
2b9de934e3 Fix manual company switch route sync
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 09:47:01 -05:00
Dotta
4a368f54d5 Delay onboarding starter task creation until launch
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 09:46:36 -05:00
Dotta
25d3bf2c64 Incorporate Worktrunk patterns into workspace plan 2026-03-13 09:41:12 -05:00
Dotta
7d1748b3a7 feat: optimize heartbeat token usage
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 09:40:43 -05:00
Dotta
2246d5f1eb Add me and unassigned assignee options
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 09:40:43 -05:00
Dotta
575a2fd83f feat: skip pre-filled assignee/project fields when tabbing in new issue dialog
When creating a new issue with a pre-filled assignee or project (e.g. from
a project page), Tab from the title field now skips over fields that already
have values, going directly to the next empty field or description.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 09:40:43 -05:00
Dotta
c9259bbec0 Fix manual company switch route sync
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 09:40:43 -05:00
Dotta
f3c18db7dd Delay onboarding starter task creation until launch
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 09:40:43 -05:00
Dotta
43baf709dd Merge pull request #797 from paperclipai/fix/embedded-postgres-initdbflags
fix: align embedded postgres ctor types with initdbFlags usage
2026-03-13 09:39:27 -05:00
Dotta
24d6e3a543 fix: align embedded postgres ctor types with initdbFlags usage 2026-03-13 09:25:04 -05:00
Dotta
0b8223b8b9 Merge pull request #796 from paperclipai/docs/organize-and-date-plans
docs: consolidate dated plan docs and naming guidance
2026-03-13 09:20:25 -05:00
Dotta
e2f0241533 docs: add dated plan naming rule and align workspace plan 2026-03-13 09:16:28 -05:00
Dotta
89e247b410 Expand workspace plan for migration and cloud execution 2026-03-13 09:15:36 -05:00
Dotta
216cb3fb28 Add workspace product model plan 2026-03-13 09:15:36 -05:00
Dotta
84fc6d4a87 docs: add token optimization plan
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 09:15:36 -05:00
Dotta
9c7d9ded1e docs: organize plans into doc/plans with date prefixes
Move plans from doc/plan/ into doc/plans/ and add YYYY-MM-DD date
prefixes to all undated plan files based on document headers or
earliest git commit dates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:11:56 -05:00
Dotta
dfe40ffcca Merge pull request #793 from paperclipai/split/docs-sidebar-link
ui: point Documentation sidebar link to docs.paperclip.ing
2026-03-13 09:09:30 -05:00
Dotta
f477f23738 Merge pull request #794 from paperclipai/split/agent-skill-resolution
fix(adapters): resolve local agent skill lookup after .agents migration
2026-03-13 09:09:22 -05:00
Dotta
752a53e38e Expand workspace plan for migration and cloud execution 2026-03-13 09:06:49 -05:00
Dotta
29a743cb9e fix: keep runtime skills scoped to ./skills
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 08:49:25 -05:00
Dotta
4e759da070 fix: prefer .agents skills and repair codex symlink targets\n\nCo-Authored-By: Paperclip <noreply@paperclip.ing> 2026-03-13 08:49:25 -05:00
Dotta
69b9e45eaf Change sidebar Documentation link to external docs.paperclip.ing
The sidebar Documentation links were pointing to an internal /docs route.
Updated both mobile and desktop sidebar instances to link to
https://docs.paperclip.ing/ in a new tab instead.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 08:49:24 -05:00
Paperclip
5c7d2116e9 Fix local-cli skill install for moved .agents skills
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-13 08:49:24 -05:00
Dotta
284bd733b9 Add workspace product model plan 2026-03-13 08:41:01 -05:00
Dotta
0f3e9937f6 Merge branch 'master' of github.com-dotta:paperclipai/paperclip
* 'master' of github.com-dotta:paperclipai/paperclip:
  docs: update PRODUCT.md and add 2026-03-13 features plan
  feat(worktree): add worktree:cleanup command, env var defaults, and auto-prefix
2026-03-13 07:26:49 -05:00
Dotta
c32d19415b Merge pull request #783 from paperclipai/docs/product-and-features-plan
docs: update PRODUCT.md and add features plan
2026-03-13 07:25:52 -05:00
Dotta
0a0d74eb94 Merge pull request #782 from paperclipai/feat/worktree-cleanup-and-env
feat(worktree): cleanup command, env var defaults, auto-prefix
2026-03-13 07:25:31 -05:00
Dotta
2c5e48993d docs: update PRODUCT.md and add 2026-03-13 features plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 07:25:23 -05:00
Dotta
77af1ae544 feat(worktree): add worktree:cleanup command, env var defaults, and auto-prefix
- Add `worktree:cleanup <name>` command that safely removes a worktree,
  its branch, and instance data. Idempotent and safe by default — refuses
  to delete branches with unique unmerged commits or worktrees with
  uncommitted changes unless --force is passed.
- Support PAPERCLIP_WORKTREES_DIR env var as default for --home across
  all worktree commands.
- Support PAPERCLIP_WORKTREE_START_POINT env var as default for
  --start-point on worktree:make.
- Auto-prefix worktree names with "paperclip-" if not already present,
  so `worktree:make subissues` creates ~/paperclip-subissues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 07:24:39 -05:00
Dotta
e24a116943 Merge branch 'master' of github.com-dotta:paperclipai/paperclip
* 'master' of github.com-dotta:paperclipai/paperclip:
  fix: resolve type errors in process-lost-reaper PR
  fix(heartbeat): prevent false process_lost failures on queued and non-child-process runs
  Revert "Merge pull request #707 from paperclipai/nm/premerge-lockfile-refresh"
  fix: ensure embedded PostgreSQL databases use UTF-8 encoding
2026-03-13 07:07:34 -05:00
Dotta
872d2434a9 Merge pull request #251 from mjaverto/fix/process-lost-reaper
fix(heartbeat): prevent false process_lost failures on queued and non-child-process runs
2026-03-13 07:06:10 -05:00
Dotta
fe764cac75 fix: resolve type errors in process-lost-reaper PR
- Fix malformed try/catch/finally blocks in heartbeat executeRun
- Declare activeRunExecutions Set to track in-flight runs
- Add resumeQueuedRuns function and export from heartbeat service
- Add initdbFlags to EmbeddedPostgresCtor type

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 06:56:31 -05:00
Sigmabrogz
3d2abbde72 fix(openclaw-gateway): catch challengePromise rejection to prevent unhandled rejection process crash
Resolves #727

Signed-off-by: Sigmabrogz <bnb1000bnb@gmail.com>
2026-03-13 00:42:28 +00:00
teknium1
e84c0e8df2 fix: use npm package instead of GitHub URL dependency
- Published hermes-paperclip-adapter@0.1.0 to npm registry
- Replaced github:NousResearch/hermes-paperclip-adapter with
  hermes-paperclip-adapter ^0.1.0 (proper semver, reproducible builds)
- Updated imports from @nousresearch/paperclip-adapter-hermes to
  hermes-paperclip-adapter
- Wired in hermesSessionCodec for structured session validation

Addresses both review items from greptile-apps:
1. Unpinned GitHub dependency → now a proper npm package with semver
2. Missing sessionCodec → now imported and registered
2026-03-12 17:23:24 -07:00
teknium1
4e354ad00d fix: address review feedback — pin dependency and add sessionCodec
- Pin @nousresearch/paperclip-adapter-hermes to v0.1.0 tag for
  reproducible builds and supply-chain safety
- Import and wire hermesSessionCodec into the adapter registration
  for structured session parameter validation (matching claude_local,
  codex_local, and other adapters that support session persistence)
2026-03-12 17:03:49 -07:00
Alaa Alghazouli
ff02220890 fix: add initdbFlags to embedded postgres ctor types 2026-03-12 23:03:44 +01:00
Dotta
f81d37fbf7 fix(heartbeat): prevent false process_lost failures on queued and non-child-process runs
- reapOrphanedRuns() now only scans running runs; queued runs are
  legitimately absent from runningProcesses (waiting on concurrency
  limits or issue locks) so including them caused false process_lost
  failures (closes #90)
- Add module-level activeRunExecutions set so non-child-process adapters
  (http, openclaw) are protected from the reaper during execution
- Add resumeQueuedRuns() to restart persisted queued runs after a server
  restart, called at startup and each periodic tick
- Add outer catch in executeRun() so setup failures (ensureRuntimeState,
  resolveWorkspaceForRun, etc.) are recorded as failed runs instead of
  leaving them stuck in running state
- Guard resumeQueuedRuns() against paused/terminated/pending_approval agents
- Increase opencode models discovery timeout from 20s to 45s
2026-03-12 17:24:50 -04:00
Dotta
83381f9c12 Add me and unassigned assignee options
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 16:12:38 -05:00
Dotta
f7e1952a55 feat: skip pre-filled assignee/project fields when tabbing in new issue dialog
When creating a new issue with a pre-filled assignee or project (e.g. from
a project page), Tab from the title field now skips over fields that already
have values, going directly to the next empty field or description.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 16:11:37 -05:00
Dotta
cf77ff927f Fix manual company switch route sync
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 16:04:28 -05:00
Dotta
fc8b5e3956 fix: keep runtime skills scoped to ./skills
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 15:57:37 -05:00
Dotta
ed16d30afc fix: prefer .agents skills and repair codex symlink targets\n\nCo-Authored-By: Paperclip <noreply@paperclip.ing> 2026-03-12 15:44:44 -05:00
Dotta
402cef66e9 Change sidebar Documentation link to external docs.paperclip.ing
The sidebar Documentation links were pointing to an internal /docs route.
Updated both mobile and desktop sidebar instances to link to
https://docs.paperclip.ing/ in a new tab instead.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:39:50 -05:00
Dotta
13c2ecd1d0 Delay onboarding starter task creation until launch
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 14:37:30 -05:00
Paperclip
a2b7611d8d Fix local-cli skill install for moved .agents skills
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 14:33:15 -05:00
Dotta
d14e656ec1 Merge pull request #645 from vkartaviy/fix/embedded-postgres-utf8
fix: ensure embedded PostgreSQL databases use UTF-8 encoding
2026-03-12 14:15:58 -05:00
Dotta
63c62e3ada chore: release v0.3.1 2026-03-12 13:09:22 -05:00
Dotta
964e04369a fixes verification 2026-03-12 12:55:26 -05:00
Dotta
873535fbf0 verify the packages actually make it to npm 2026-03-12 12:42:00 -05:00
Dotta
5201222ce7 Merge pull request #711 from paperclipai/revert-pr-707
Revert PR #707
2026-03-12 12:17:43 -05:00
Dotta
b888f92718 Revert "Merge pull request #707 from paperclipai/nm/premerge-lockfile-refresh"
This reverts commit 56df8d3cf0, reversing
changes made to ac82cae39a.
2026-03-12 12:13:39 -05:00
Dotta
87c0bf9cdf added v0.3.1.md changelog 2026-03-12 11:05:31 -05:00
Dotta
56df8d3cf0 Merge pull request #707 from paperclipai/nm/premerge-lockfile-refresh
ci: refresh pnpm lockfile before merge
2026-03-12 10:55:25 -05:00
Dotta
8808a33fe1 ci: refresh pnpm lockfile before merge 2026-03-12 10:52:17 -05:00
Dotta
ac82cae39a Merge pull request #706 from zvictor/fix-gemini-build
fix(docker): include gemini adapter manifest in deps stage
2026-03-12 10:38:13 -05:00
zvictor
9c6a913ef1 fix(docker): include gemini adapter manifest in deps stage 2026-03-12 12:28:45 -03:00
Dotta
18f7092b71 Merge pull request #430 from paperclipai/chore/refresh-lockfile
chore(lockfile): refresh pnpm-lock.yaml
2026-03-12 09:39:22 -05:00
Dotta
c8b08e64d6 Merge remote-tracking branch 'public-gh/master'
* public-gh/master:
  Raise default max turns to 300
2026-03-12 09:37:15 -05:00
lockfile-bot
e6ff4eb8b2 chore(lockfile): refresh pnpm-lock.yaml 2026-03-12 14:36:52 +00:00
Dotta
7adc14ab50 Merge pull request #701 from paperclipai/nm/raise-default-max-turns-300
Raise default max turns to 300
2026-03-12 09:36:28 -05:00
Dotta
aeafeba12b Raise default max turns to 300
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 09:34:05 -05:00
Dotta
890ff39bdb Merge remote-tracking branch 'public-gh/master'
* public-gh/master:
  Drop pnpm lockfile from PR
  Update ui/src/components/OnboardingWizard.tsx
  Update ui/src/components/OnboardingWizard.tsx
  Fix onboarding manual debug JSX
  Improve onboarding defaults and issue goal fallback
  Simplify adapter environment check: animate pass, show debug only on fail
  Show Claude Code and Codex as recommended, collapse other adapter types
  Animate onboarding layout when switching between Company and Agent steps
  Make onboarding wizard steps clickable tabs for easier dev navigation
  Add agent chat architecture plan
  Style tweaks for onboarding wizard step 1
  Add direct onboarding routes
2026-03-12 09:24:57 -05:00
Dotta
55c145bff2 Merge pull request #700 from paperclipai/paperclip-better-onboarding
Improve onboarding flow and issue goal fallback
2026-03-12 09:24:11 -05:00
Dotta
7809405e8f Drop pnpm lockfile from PR 2026-03-12 09:19:28 -05:00
Dotta
88916fd11b Update ui/src/components/OnboardingWizard.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-12 09:15:40 -05:00
Dotta
06b50ba161 Update ui/src/components/OnboardingWizard.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-12 09:15:20 -05:00
Dotta
f76a7ef408 Fix onboarding manual debug JSX 2026-03-12 08:54:27 -05:00
Dotta
448e9c192b Improve onboarding defaults and issue goal fallback 2026-03-12 08:50:31 -05:00
Dotta
1d5e5247e8 Raise default max turns to 300
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 08:42:12 -05:00
Dotta
5f3f354b3a Simplify adapter environment check: animate pass, show debug only on fail
- When the adapter probe passes, show a clean animated "Passed" badge with
  a checkmark icon instead of verbose check details
- Only show the "Manual debug" section when the test fails, reducing noise
  for successful flows

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 08:42:07 -05:00
Dotta
7df74b170d Show Claude Code and Codex as recommended, collapse other adapter types
In the onboarding wizard step 2 ("Create your first agent"), Claude Code and
Codex are now shown prominently as recommended options. Other adapter types
(OpenCode, Pi, Cursor, OpenClaw Gateway) are hidden behind a collapsible
"More Agent Adapter Types" toggle to reduce visual noise for new users.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 08:41:22 -05:00
Dotta
7e6a5682fa Animate onboarding layout when switching between Company and Agent steps
When on the Company step (step 1), the form takes up the left half with
ASCII art on the right. Switching to Agent or any other step smoothly
animates the form to full width while fading out the ASCII art panel.
Switching back reverses the animation.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 08:39:41 -05:00
Dotta
e6a684d96a Make onboarding wizard steps clickable tabs for easier dev navigation
Replace the progress bar dots with labeled tab buttons (Company, Agent,
Task, Launch) that allow clicking directly to any step. This makes it
easy to debug/preview individual onboarding screens without stepping
through the full flow.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 08:39:41 -05:00
Dotta
c3cf4279fa Add agent chat architecture plan
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 08:39:41 -05:00
Dotta
d4d1b2e7f9 Style tweaks for onboarding wizard step 1
- Set animation panel (right half) background to #1d1d1d
- Add more margin above Company name form label
- Make form labels white when input is focused or has value
  using Tailwind group/focus-within pattern

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 08:39:41 -05:00
Dotta
b7744a2215 Add direct onboarding routes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 08:39:41 -05:00
Dotta
f5c766beb9 Merge pull request #697 from paperclipai/paperclip_instance_sidebar_v2
Add instance heartbeat settings sidebar
2026-03-12 08:19:52 -05:00
Dotta
3e8993b449 Remove pnpm lockfile changes from sidebar PR 2026-03-12 08:19:16 -05:00
Dotta
32bdcf1dca Add instance heartbeat settings sidebar 2026-03-12 08:14:45 -05:00
Dotta
369dfa4397 Fix hooks order violation and UX copy on instance settings page
Move useMemo and derived state above early returns so hooks are always
called in the same order. Simplify the description to plain English
and change toggle button labels to "Enable Timer Heartbeat" /
"Disable Timer Heartbeat" for clarity.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 08:14:45 -05:00
Dotta
905403c1af Compact grouped heartbeat list on instance settings page
Group agents by company with a single card per company and dense
inline rows instead of one card per agent. Replaces the three stat
cards with a slim inline summary. Each row shows status badge, linked
agent name, role, interval, last heartbeat time, a config link icon,
and an enable/disable button.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 08:14:45 -05:00
Dotta
dc3f3776ea Merge pull request #695 from paperclipai/nm/public-master-polish-batch
Polish inbox, transcripts, and log redaction flows
2026-03-12 08:13:33 -05:00
Dotta
44396be7c1 Merge remote-tracking branch 'public-gh/master'
* public-gh/master:
  Default Gemini adapter to yolo mode and add API access prompt note
  fix: remove Cmd+1..9 company-switch shortcut
  fix(ui): prevent IME composition Enter from moving focus in new issue title
  fix(cli): add restart hint after allowed-hostname change
  docs: remove obsolete TODO for CONTRIBUTING.md
  fix: default dangerouslySkipPermissions to true for unattended agents
  fix: route heartbeat cost recording through costService
  Show issue creator in properties sidebar
2026-03-12 08:09:06 -05:00
Dotta
c49e5e90be Merge pull request #656 from aaaaron/gemini-tool-permissions
Default Gemini adapter to yolo mode, add API access note
2026-03-12 07:59:14 -05:00
Dotta
01180d3027 Move maintainer skills into .agents/skills
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 07:36:14 -05:00
Dotta
397e6d0915 Document worktree CLI commands with full option reference
Add a "Worktree CLI Reference" subsection to doc/DEVELOPING.md with
complete option tables and examples for worktree init, worktree:make,
and worktree env subcommands.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 07:31:14 -05:00
Dotta
778afd31b1 Tighten dashboard agent pane typography
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 07:02:06 -05:00
Dotta
6fe7f7a510 Hide saved-session resume noise from nice transcripts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-12 06:59:25 -05:00
Dotta
088eaea0cb Redact current user in comments and token checks
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 22:17:21 -05:00
Dotta
b1bf09970f Render transcript markdown and fold command stdout
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 21:51:23 -05:00
Dotta
6540084ddf Add inbox live badge and timestamp
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 21:47:21 -05:00
Dotta
cde3a8c604 Move inbox read action beside tabs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 21:43:16 -05:00
Dotta
57113b1075 Merge pull request #386 from domocarroll/fix/heartbeat-budget-enforcement
fix: route heartbeat cost recording through costService
2026-03-11 21:30:09 -05:00
Dotta
cbe5cfe603 Merge pull request #443 from adamrobbie-nudge/docs/remove-contributing-todo
docs: remove obsolete TODO for CONTRIBUTING.md
2026-03-11 21:16:29 -05:00
Dotta
833ccb9921 Merge pull request #549 from mvanhorn/osc/538-fix-allowed-hostname-restart-hint
fix(cli): add restart hint after allowed-hostname change
2026-03-11 21:15:49 -05:00
Dotta
bfbb42a9fc Merge pull request #578 from kaonash/fix_ime_composition_enter_in_new_issue_dialog
fix(ui): prevent IME composition Enter from moving focus in new issue title
2026-03-11 21:14:39 -05:00
Dotta
c4e64be4bc Merge pull request #628 from STRML/fix/remove-cmd-number-shortcut
fix: remove Cmd+1..9 company-switch shortcut
2026-03-11 21:12:10 -05:00
Dotta
88b47c805c Merge pull request #145 from cschneid/show-issue-creator
Show issue creator in properties sidebar
2026-03-11 21:11:00 -05:00
Dotta
908e01655a Merge pull request #388 from ohld/fix/default-skip-permissions
fix: default dangerouslySkipPermissions to true
2026-03-11 21:07:03 -05:00
Dotta
ea54c018ad Match inbox row edges to issues
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 21:05:13 -05:00
Dotta
6c351cb37d Use issues page as issue row baseline
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 20:59:34 -05:00
Dotta
ee3d8c1890 Redact home paths in transcript views
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 20:56:47 -05:00
Dotta
3b9da0ee95 Refactor shared issue rows
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 20:51:28 -05:00
Aaron
6bfe0b8422 Default Gemini adapter to yolo mode and add API access prompt note
Gemini CLI only registers run_shell_command in --approval-mode yolo.
Non-yolo modes don't expose it at all, making Paperclip API calls
impossible. Always pass --approval-mode yolo and remove the now-unused
policy engine code, approval mode config, and UI toggles.

Add a "Paperclip API access note" to the prompt with curl examples
via run_shell_command, since the universal SKILL.md is tool-agnostic.

Also extract structured question events from Gemini assistant messages
to support interactive approval flows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 01:45:08 +00:00
Dotta
33c6d093ab Adjust inbox issue rows on mobile
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 20:37:42 -05:00
Dotta
d0b1079b9b Remove new issue draft autosave footer copy
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 20:37:36 -05:00
Dotta
7945e7e780 Redact current user from run logs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 17:46:23 -05:00
Dotta
6e7266eeb4 Merge pull request #649 from paperclipai/fix/issue-runs-heartbeat-regressions
Fix issue run lookup and heartbeat run summaries
2026-03-11 17:26:43 -05:00
Dotta
d19ff3f4dd Fix issue run lookup and heartbeat run summaries 2026-03-11 17:23:33 -05:00
Dotta
4435e14838 Merge branch 'master' of github.com-dotta:paperclipai/paperclip
* 'master' of github.com-dotta:paperclipai/paperclip:
  Tighten transcript label styling
  Fix env-sensitive worktree and runtime config tests
  Refine executed command row centering
  Tighten live run transcript streaming and stdout
  Center collapsed command group rows
  Refine collapsed command failure styling
  Tighten command transcript rows and dashboard card
  Polish transcript event widgets
  Refine transcript chrome and labels
  fix: remove paperclip property from OpenClaw Gateway agent params
  Add a run transcript UX fixture lab
  Humanize run transcripts across run detail and live surfaces
  fix(adapters/gemini-local): address PR review feedback
  fix(adapters/gemini-local): inject skills into ~/.gemini/ instead of tmpdir
  fix(adapters/gemini-local): downgrade missing API key to info level
  feat(adapters/gemini-local): add auth detection, turn-limit handling, sandbox, and approval modes
  fix(adapters/gemini-local): address PR review feedback for skills and formatting
  feat(adapters): add Gemini CLI local adapter support

# Conflicts:
#	cli/src/__tests__/worktree.test.ts
2026-03-11 17:04:30 -05:00
Dotta
df121c61dc Merge pull request #648 from paperclipai/paperclip-nicer-runlogs-formats
Humanize run transcripts and polish transcript UX
2026-03-11 17:02:33 -05:00
Dotta
1f204e4d76 Fix issue description overflow with break-words and overflow-hidden
Long unbroken text (stack traces, file paths, URLs) in issue descriptions
could overflow the container. Add break-words to MarkdownBody and
overflow-hidden to InlineEditor display wrapper.

Fixes PAP-476

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 16:41:14 -05:00
Dotta
8194132996 Tighten transcript label styling
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 16:40:26 -05:00
Dotta
f7cc292742 Fix env-sensitive worktree and runtime config tests
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 16:38:31 -05:00
Dotta
2efc3a3ef6 Fix worktree JWT env persistence
Ensure worktree init writes PAPERCLIP_AGENT_JWT_SECRET into the new .paperclip/.env when the source instance already has a usable secret loaded or configured. Also harden the affected integration tests against shell env leakage and full-suite timeout pressure.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 16:38:16 -05:00
Volodymyr Kartavyi
057e3a494c fix: ensure embedded PostgreSQL databases use UTF-8 encoding
On macOS, `initdb` defaults to SQL_ASCII encoding because it infers
locale from the system environment. When `ensurePostgresDatabase()`
creates a database without specifying encoding, the new database
inherits SQL_ASCII from the cluster. This causes string functions like
`left()` to operate on bytes instead of characters, producing invalid
UTF-8 when multi-byte characters are truncated.

Two-part fix:
1. Pass `--encoding=UTF8 --locale=C` via `initdbFlags` to all
   EmbeddedPostgres constructors so the cluster defaults to UTF-8.
2. Explicitly set `encoding 'UTF8'` in the CREATE DATABASE statement
   with `template template0` (required because template1 may already
   have a different encoding) and `C` locale for portability.

Existing databases created with SQL_ASCII are NOT automatically fixed;
users must delete their local `data/db` directory and restart to
re-initialize the cluster.

Relates to #636

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 21:59:02 +01:00
User
bb6e721567 fix: remove Cmd+1..9 company-switch shortcut
This shortcut interfered with browser tab-switching (Cmd+1..9) and
produced a black screen when used. Removes the handler, the Layout
callback, and the design-guide documentation entry.

Closes RUS-56
2026-03-11 15:32:39 -04:00
Dotta
e76adf6ed1 Refine executed command row centering
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 13:58:24 -05:00
Dotta
2b4d82bfdd Merge pull request #452 from aaaaron/feat/gemini-adapter-improvements
feat(adapters/gemini-local): Gemini CLI adapter with auth, skills, and sandbox support
2026-03-11 13:43:28 -05:00
Dotta
5e9c223077 Tighten live run transcript streaming and stdout
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 13:29:40 -05:00
Dotta
98ede67b9b Center collapsed command group rows
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 13:24:45 -05:00
Dotta
f594edd39f Refine collapsed command failure styling
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 13:21:58 -05:00
Dotta
487c86f58e Tighten command transcript rows and dashboard card
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 13:14:08 -05:00
Dotta
b3e71ca562 Polish transcript event widgets
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 12:14:12 -05:00
Dotta
ab2f9e90eb Refine transcript chrome and labels
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 11:45:05 -05:00
Dotta
cb77b2eb7e Merge pull request #626 from openagen/fix/606-remove-paperclip-property
fix: remove paperclip property from OpenClaw Gateway agent params
2026-03-11 11:39:55 -05:00
lin
6c9e639a68 fix: remove paperclip property from OpenClaw Gateway agent params
The OpenClaw Gateway's agent method has strict parameter validation
that rejects unknown properties. The paperclip property was being
sent at the root level of agentParams, causing validation failures
with error: "invalid agent params: at root: unexpected property 'paperclip'"

The paperclip metadata is already included in the message field
via wakeText, so removing the separate paperclip property resolves
the validation error while preserving the necessary information.

Fixes #606

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 00:27:41 +08:00
Dotta
6e4694716b Add a run transcript UX fixture lab
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 10:47:22 -05:00
Dotta
87b8e21701 Humanize run transcripts across run detail and live surfaces
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-11 10:35:41 -05:00
Dotta
dd5d2c7c92 Merge pull request #618 from paperclipai/skill/update-paperclip-skill-md
Update paperclip skill: co-author rule + markdown fixes
2026-03-11 09:53:15 -05:00
Dotta
e168dc7b97 Update paperclip skill: add co-author rule and fix markdown formatting
Add commit co-author requirement for Paperclip agents and fix markdown
lint issues (blank lines before lists, table column alignment).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 09:52:21 -05:00
Dotta
4670f60d3e Merge pull request #616 from paperclipai/public/worktree-pnpm-install
Install dependencies after creating worktree
2026-03-11 09:32:26 -05:00
Dotta
472322de24 Install dependencies after creating worktree
Run pnpm install in the new worktree directory before initializing the
Paperclip instance so that node_modules are ready when the init step runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 09:31:41 -05:00
Dotta
3770e94d56 Merge pull request #613 from paperclipai/public/inbox-runs-worktree-history
Polish inbox workflows, agent runs, and worktree setup
2026-03-11 09:21:51 -05:00
Dotta
d9492f02d6 Add worktree start-point support 2026-03-11 09:15:27 -05:00
Dotta
57d8d01079 Align inbox badge with visible unread items 2026-03-11 09:02:23 -05:00
Dotta
345c7f4a88 Remove inbox recent issues label 2026-03-11 08:51:30 -05:00
Dotta
521b24da3d Tighten recent inbox tab behavior 2026-03-11 08:42:41 -05:00
Dotta
96e03b45b9 Refine inbox tabs and layout 2026-03-11 08:26:41 -05:00
Dotta
57dcdb51af ui: apply interface polish from design article review
- Add global font smoothing (antialiased) to body
- Add tabular-nums to all numeric displays: MetricCard values, Costs page,
  AgentDetail token/cost grids and tables, IssueDetail cost summary,
  Companies page budget display
- Replace markdown image hard border with subtle inset box-shadow overlay
- Replace all animate-ping status dots with calmer animate-pulse across
  AgentDetail, IssueDetail, Agents, sidebar, kanban, issues list, and
  active agents panel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 08:20:24 -05:00
Dotta
a503d2c12c Adjust inbox tab memory and badge counts 2026-03-11 07:42:19 -05:00
teknium1
97d628d784 feat: add Hermes Agent adapter (hermes_local)
Adds support for Hermes Agent (https://github.com/NousResearch/hermes-agent)
as a managed employee in Paperclip companies.

Hermes Agent is a full-featured AI agent by Nous Research with 30+ native
tools, persistent memory, session persistence, 80+ skills, MCP support,
and multi-provider model access.

Changes:
- Add 'hermes_local' to AGENT_ADAPTER_TYPES (packages/shared)
- Add @nousresearch/paperclip-adapter-hermes dependency (server)
- Register hermesLocalAdapter in the adapter registry (server)

The adapter package is maintained at:
https://github.com/NousResearch/hermes-paperclip-adapter
2026-03-10 23:12:13 -07:00
Dotta
21d2b075e7 Fix inbox badge logic and landing view 2026-03-10 22:55:45 -05:00
Ken Shimizu
426b16987a fix(ui): prevent IME composition Enter from moving focus in new issue title 2026-03-11 11:57:46 +09:00
Dotta
92aef9bae8 Slim heartbeat run list payloads 2026-03-10 21:16:33 -05:00
Dotta
5f76d03913 ui: smooth new issue submit state 2026-03-10 21:06:16 -05:00
Dotta
d3ac8722be Add agent runs tab to detail page 2026-03-10 21:06:15 -05:00
Dotta
183d71eb7c Restore native mobile page scrolling 2026-03-10 21:06:10 -05:00
Dotta
3273692944 Fix markdown link dialog positioning 2026-03-10 21:01:47 -05:00
Dotta
b5935349ed Preserve issue breadcrumb source 2026-03-10 20:59:55 -05:00
Dotta
4b49efa02e Smooth agent config save button state 2026-03-10 20:58:18 -05:00
Dotta
c2c63868e9 Refine issue markdown typography 2026-03-10 20:55:41 -05:00
Matt Van Horn
bc5b30eccf feat(ui): add project filter to issues list
Add a "Project" filter section to the issues filter popover, following the
same pattern as the existing Assignee and Labels filters. Issues can now
be filtered by one or more projects from the filter dropdown.

Closes #129

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:57:01 -07:00
Matt Van Horn
d114927814 fix: embed uploaded images inline in comments via paperclip button
The paperclip button in comments uploaded images to the issue-level
attachment section but didn't insert a markdown image reference into
the comment body. Now it uses the imageUploadHandler to get the URL
and appends an inline image to the comment text.

Fixes #272

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:50:57 -07:00
Matt Van Horn
b41c00a9ef fix: graceful fallback when AGENTS.md is missing in claude-local adapter
The codex-local and cursor-local adapters already wrap the
instructionsFilePath read in try/catch, logging a warning and
continuing without instructions. The claude-local adapter was missing
this handling, causing ENOENT crashes when the instructions file
doesn't exist.

Fixes #529

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:46:48 -07:00
Matt Van Horn
9d2800e691 fix(cli): add restart hint after allowed-hostname change
The server builds its hostname allow-set once at startup. When users
add a new hostname via the CLI, the config file is updated but the
running server doesn't reload it. This adds a clear message telling
users to restart the server for the change to take effect.

Fixes #538

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:43:40 -07:00
Dotta
3a003e11cc Merge pull request #545 from paperclipai/feat/worktree-and-routing-polish
Add worktree:make and routing polish
2026-03-10 17:38:17 -05:00
Dotta
d388255e66 Remove inbox New tab badge count 2026-03-10 17:05:47 -05:00
Dotta
80d87d3b4e Add paperclipai worktree:make command 2026-03-10 16:52:26 -05:00
Dotta
21eb904a4d fix(server): keep pretty logger metadata on one line 2026-03-10 16:42:36 -05:00
Dotta
d62b89cadd ui: add company-aware not found handling 2026-03-10 16:38:46 -05:00
Dotta
78207304d4 Merge pull request #540 from paperclipai/nm/worktree-favicon
Add worktree-specific favicon branding
2026-03-10 16:33:20 -05:00
Dotta
c799fca313 Add worktree-specific favicon branding 2026-03-10 16:15:11 -05:00
Dotta
50db379db2 Merge pull request #536 from paperclipai/fix/dev-migration-flow
Fix dev migration prompt and embedded db:migrate
2026-03-10 15:32:10 -05:00
Dotta
56aeddfa1c Fix dev migration prompt and embedded db:migrate 2026-03-10 15:31:05 -05:00
Dotta
42c8aca5c0 Merge branch 'master' of github.com-dotta:paperclipai/paperclip
* 'master' of github.com-dotta:paperclipai/paperclip:
  Fix approvals service idempotency test
  Add workspace strategy plan doc
  Copy git hooks during worktree init
2026-03-10 15:09:00 -05:00
Dotta
00495d3d89 Merge pull request #534 from paperclipai/fix/approval-service-idempotency
Fix approvals service idempotency test
2026-03-10 15:07:45 -05:00
Dotta
a613435249 Fix approvals service idempotency test 2026-03-10 15:05:19 -05:00
Dotta
576b408682 Merge pull request #532 from paperclipai/feature/worktree-init-copy-hooks
Copy git hooks during worktree init
2026-03-10 14:58:18 -05:00
Dotta
193b7c0570 Add workspace strategy plan doc 2026-03-10 14:57:18 -05:00
Dotta
93a8b55ff8 Copy git hooks during worktree init 2026-03-10 14:55:35 -05:00
Dotta
24a553c255 doc for workspace strategy 2026-03-10 14:52:43 -05:00
Dotta
2332a79e0b Merge branch 'master' of github.com-dotta:paperclipai/paperclip
* 'master' of github.com-dotta:paperclipai/paperclip:
  updating paths
  Rebind seeded project workspaces to the current worktree
  Add command-based worktree provisioning
  Refine project and agent configuration UI
  Add configuration tabs to project and agent pages
  Add project-first execution workspace policies
  Add worktree-aware workspace runtime support
2026-03-10 14:47:50 -05:00
Dotta
65af1d77a4 Merge pull request #530 from paperclipai/feature/workspace-runtime-support
Add issue worktree runtime support
2026-03-10 14:46:29 -05:00
Dotta
b0b7ec779a updating paths 2026-03-10 14:43:34 -05:00
Dotta
859c82aa12 Merge remote-tracking branch 'public-gh/master' into feature/workspace-runtime-support
* public-gh/master:
  Rebind seeded project workspaces to the current worktree
  Copy seeded secrets key into worktree instances
  server: make approval retries idempotent (#499)
  fix: address review feedback — stale error message and * wildcard
  Update server/src/routes/assets.ts
  feat: make attachment content types configurable via env var
  fix: wire parentId query filter into issues list endpoint
2026-03-10 14:19:11 -05:00
Dotta
6fd29e05ad Merge pull request #522 from paperclipai/feature/worktree-rebind-seeded-workspaces
Rebind seeded project workspaces to the current worktree
2026-03-10 13:54:50 -05:00
Dotta
12216b5cc6 Rebind seeded project workspaces to the current worktree 2026-03-10 13:50:29 -05:00
Dotta
0c525febf2 Merge remote-tracking branch 'public-gh/master'
* public-gh/master:
  Copy seeded secrets key into worktree instances
  server: make approval retries idempotent (#499)
  Fix doctor summary after repairs
  Fix worktree minimal clone startup
  Add minimal worktree seed mode
  Add worktree init CLI for isolated development instances
  fix: address review feedback — stale error message and * wildcard
  Update server/src/routes/assets.ts
  feat: make attachment content types configurable via env var
  fix: wire parentId query filter into issues list endpoint
  Apply suggestions from code review
  fix(adapter-utils): strip Claude Code env vars from child processes
2026-03-10 13:41:47 -05:00
Dotta
b0fe48b730 Revert "ui: move settings to footer icon beside theme toggle"
This reverts commit f3a9b6de21.
2026-03-10 13:41:37 -05:00
Dotta
f3a9b6de21 ui: move settings to footer icon beside theme toggle 2026-03-10 13:23:35 -05:00
Dotta
31561724f7 Merge pull request #491 from lazmo88/fix/parentid-filter-issues-list
fix: wire parentId query filter into issues list endpoint
2026-03-10 13:07:20 -05:00
Dotta
c363428966 Merge pull request #517 from paperclipai/feature/worktree-seed-secrets-key
Copy seeded secrets key into worktree instances
2026-03-10 12:59:54 -05:00
Dotta
f783f66866 Merge pull request #495 from subhendukundu/feat/configurable-attachment-types
feat: make attachment content types configurable via env var
2026-03-10 12:58:15 -05:00
Dotta
deec68ab16 Copy seeded secrets key into worktree instances 2026-03-10 12:57:53 -05:00
Dotta
6733a6cd7e Merge pull request #502 from davidahmann/codex/issue-499-approval-idempotency
Make approval resolution retries idempotent
2026-03-10 12:56:30 -05:00
Dotta
dfbb4f1ccb Add command-based worktree provisioning 2026-03-10 12:42:36 -05:00
Aaron
6956dad53a fix(adapters/gemini-local): address PR review feedback
- Update stale doc comment in index.ts to reflect direct ~/.gemini/skills/
  injection instead of tmpdir approach
- Remove bare GEMINI_API_KEY/GOOGLE_API_KEY from auth regex to prevent
  false positives when those strings appear in assistant output
- Align hello probe sandbox/approvalMode flags with execute.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:46:04 +00:00
Aaron
e9fc403b94 fix(adapters/gemini-local): inject skills into ~/.gemini/ instead of tmpdir
GEMINI_CLI_HOME pointed to a tmpdir which broke OAuth auth since the CLI
couldn't find credentials in the real home directory.

Instead, inject Paperclip skills directly into ~/.gemini/skills/ (matching
the pattern used by cursor, codex, pi, and opencode adapters). This lets
the Gemini CLI find both auth credentials and skills in their natural
location without any GEMINI_CLI_HOME override.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:46:04 +00:00
Aaron
8eb8b16047 fix(adapters/gemini-local): downgrade missing API key to info level
The Gemini CLI supports OAuth login via `gemini auth login` which stores
credentials locally without setting any env vars. The previous warn-level
check on missing GEMINI_API_KEY caused false alarms when CLI-based OAuth
was used. The hello probe that follows is the real auth authority — if
auth is actually broken, it will catch it and report appropriately.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:46:04 +00:00
Aaron
4e5f67ef96 feat(adapters/gemini-local): add auth detection, turn-limit handling, sandbox, and approval modes
Incorporate improvements from PR #13 and #105 into the gemini-local adapter:

- Add detectGeminiAuthRequired() for runtime auth failure detection with
  errorCode: "gemini_auth_required" on execution results
- Add isGeminiTurnLimitResult() to detect exit code 53 / turn_limit status
  and clear session to prevent stuck sessions on next heartbeat
- Add describeGeminiFailure() for structured error messages from parsed
  result events including errors array extraction
- Return parsed resultEvent in resultJson instead of raw stdout/stderr
- Add isRetry guard to prevent stale session ID fallback after retry
- Replace boolean yolo with approvalMode string (default/auto_edit/yolo)
  with backwards-compatible config.yolo fallback
- Add sandbox config option (--sandbox / --sandbox=none)
- Add GOOGLE_GENAI_USE_GCA auth detection in environment test
- Consolidate auth detection regex into shared detectGeminiAuthRequired()
- Add gemini-2.0-flash and gemini-2.0-flash-lite model IDs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:46:04 +00:00
Aditya Sasidhar
ec445e4cc9 fix(adapters/gemini-local): address PR review feedback for skills and formatting
- Isolate skills injection using a temporary directory mapped via
  GEMINI_CLI_HOME, mirroring the claude-local sandbox approach
  instead of polluting the global ~/.gemini/skills directory.
- Update the environment probe to use `--output-format stream-json`
  so the payload matches the downstream parseGeminiJsonl parser.
- Deduplicate `firstNonEmptyLine` helper by extracting it to a
  shared `utils.ts` module.
- Clean up orphaned internal exports and update adapter documentation.
2026-03-10 16:46:04 +00:00
Aditya Sasidhar
af97259a9c feat(adapters): add Gemini CLI local adapter support
Signed-off-by: Aditya Sasidhar <telikicherlaadityasasidhar@gmail.com>
2026-03-10 16:46:04 +00:00
David Ahmann
9c68c1b80b server: make approval retries idempotent (#499) 2026-03-10 12:00:29 -04:00
Dotta
e94ce47ba5 Refine project and agent configuration UI 2026-03-10 10:58:43 -05:00
Dotta
6186eba098 Add configuration tabs to project and agent pages 2026-03-10 10:58:43 -05:00
Dotta
b83a87f42f Add project-first execution workspace policies 2026-03-10 10:58:43 -05:00
Dotta
3120c72372 Add worktree-aware workspace runtime support 2026-03-10 10:58:38 -05:00
Dotta
7934952a77 Merge pull request #496 from paperclipai/feature/worktree-development-tools
feat(cli): add isolated worktree-local Paperclip instance tools
2026-03-10 10:56:36 -05:00
Dotta
d9574fea71 Fix doctor summary after repairs 2026-03-10 10:13:05 -05:00
Dotta
83738b45cd Fix worktree minimal clone startup 2026-03-10 10:13:05 -05:00
Dotta
4a67db6a4d Add minimal worktree seed mode 2026-03-10 10:13:05 -05:00
Dotta
0704854926 Add worktree init CLI for isolated development instances 2026-03-10 10:13:05 -05:00
Subhendu Kundu
1959badde7 fix: address review feedback — stale error message and * wildcard
- assets.ts: change "Image exceeds" to "File exceeds" in size-limit error
- attachment-types.ts: handle plain "*" as allow-all wildcard pattern
- Add test for "*" wildcard (12 tests total)
2026-03-10 20:01:08 +05:30
Subhendu Kundu
3ff07c23d2 Update server/src/routes/assets.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-10 19:54:42 +05:30
Subhendu Kundu
dec02225f1 feat: make attachment content types configurable via env var
Add PAPERCLIP_ALLOWED_ATTACHMENT_TYPES env var to configure allowed
MIME types for issue attachments and asset uploads. Supports exact
types (application/pdf) and wildcard patterns (image/*, text/*).

Falls back to the existing image-only defaults when the env var is
unset, preserving backward compatibility.

- Extract shared module `attachment-types.ts` with `isAllowedContentType()`
  and `matchesContentType()` (pure, testable)
- Update `routes/issues.ts` and `routes/assets.ts` to use shared module
- Add unit tests for parsing and wildcard matching

Closes #487
2026-03-10 19:40:22 +05:30
Claude
f6f5fee200 fix: wire parentId query filter into issues list endpoint
The parentId parameter on GET /api/companies/:companyId/issues was
silently ignored — the filter was never extracted from the query string,
never passed to the service layer, and the IssueFilters type did not
include it. All other filters (status, assigneeAgentId, projectId, etc.)
worked correctly.

This caused subtask lookups to return every issue in the company instead
of only children of the specified parent.

Changes:
- Add parentId to IssueFilters interface
- Add eq(issues.parentId, filters.parentId) condition in list()
- Extract parentId from req.query in the route handler

Fixes: LAS-101
2026-03-10 15:54:31 +02:00
Dotta
49b9511889 Merge pull request #485 from jknair/fix/strip-claudecode-env-from-child-processes
fix(adapter-utils): strip Claude Code env vars from child processes
2026-03-10 07:25:30 -05:00
Dotta
1a53567cb6 Apply suggestions from code review
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-10 07:24:48 -05:00
Jayakrishnan
9248881d42 fix(adapter-utils): strip Claude Code env vars from child processes
When the Paperclip server is started from within a Claude Code session
(e.g. `npx paperclipai run` in a Claude Code terminal), the `CLAUDECODE`
and related env vars (`CLAUDE_CODE_ENTRYPOINT`, `CLAUDE_CODE_SESSION`,
`CLAUDE_CODE_PARENT_SESSION`) leak into `process.env`. Since
`runChildProcess()` spreads `process.env` into the child environment,
every spawned `claude` CLI process inherits these vars and immediately
exits with: "Claude Code cannot be launched inside another Claude Code
session."

This is particularly disruptive for the `claude-local` adapter, where
every agent run spawns a `claude` child process. A single contaminated
server start (or cron job that inherits the env) silently breaks all
agent executions until the server is restarted in a clean environment.

The fix deletes the four known Claude Code nesting-guard env vars from
the merged environment before passing it to `spawn()`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 12:01:46 +00:00
Dotta
ef978dd601 Merge pull request #446 from paperclipai/codex/pr-report-skill
feat: add pr-report skill
2026-03-09 17:05:35 -05:00
Dotta
fbf9d5714f feat: add pr-report skill 2026-03-09 17:01:45 -05:00
Dotta
8ac064499f Merge pull request #445 from paperclipai/release/0.3.0
Release/0.3.0
2026-03-09 16:45:02 -05:00
Dotta
cbbf695c35 release files 2026-03-09 16:43:53 -05:00
Dotta
7e8908afa2 chore: release v0.3.0 2026-03-09 16:31:12 -05:00
Dotta
58d4d04e99 Merge pull request #444 from paperclipai/release/0.3.0
Release/0.3.0
2026-03-09 16:20:22 -05:00
Dotta
c672b71f7f Refresh bootstrap gate while setup is pending 2026-03-09 16:13:15 -05:00
Dotta
01c5a6f198 Unblock canary onboard smoke bootstrap 2026-03-09 16:06:16 -05:00
adamrobbie
8a7b7a2383 docs: remove obsolete TODO for CONTRIBUTING.md 2026-03-09 16:58:57 -04:00
Dotta
64f5c3f837 Fix authenticated smoke bootstrap flow 2026-03-09 15:30:08 -05:00
Dotta
c62266aa6a tweaks to docker smoke 2026-03-09 14:41:00 -05:00
Dotta
5dd1e6335a Fix root TypeScript solution config 2026-03-09 14:09:30 -05:00
Dotta
469bfe3953 chore: add release train workflow 2026-03-09 13:55:30 -05:00
Dotta
d20341c797 Merge pull request #413 from online5880/fix/windows-command-compat
fix: support Windows command wrappers for local adapters
2026-03-09 12:50:20 -05:00
online5880
756ddb6cf7 fix: remove lockfile changes from PR 2026-03-10 02:34:52 +09:00
Dotta
200dd66f63 Merge pull request #400 from AiMagic5000/fix/docker-non-root-node-user
fix(docker): run production server as non-root node user
2026-03-09 12:18:20 -05:00
Dotta
9859bac440 Merge pull request #423 from RememberV/fix/onboarding-navigates-to-dashboard
fix: navigate to dashboard after onboarding, not first issue
2026-03-09 12:14:58 -05:00
online5880
8d6b20b47b Merge branch 'master' into fix/windows-command-compat 2026-03-10 02:05:41 +09:00
online5880
a418106005 fix: restore cross-env in server dev watch 2026-03-10 01:43:45 +09:00
Dotta
84ef17bf85 Merge pull request #424 from paperclipai/chore/refresh-lockfile
chore(lockfile): refresh pnpm-lock.yaml
2026-03-09 11:43:44 -05:00
lockfile-bot
23dec980e2 chore(lockfile): refresh pnpm-lock.yaml 2026-03-09 16:41:30 +00:00
Dotta
03c37f8dea Merge pull request #427 from paperclipai/dotta-releases
Dotta releases
2026-03-09 11:41:11 -05:00
Dotta
8360b2e3e3 fix: complete authenticated onboarding startup 2026-03-09 11:26:58 -05:00
Dotta
d9ba4790e9 Merge branch 'master' into fix/windows-command-compat 2026-03-09 11:25:18 -05:00
Dotta
3ec96fdb73 fix: complete authenticated docker onboard smoke 2026-03-09 11:12:34 -05:00
Dotta
eecb780dd7 Merge pull request #420 from paperclipai/dotta-releases
Dotta releases
2026-03-09 11:04:16 -05:00
Dotta
632079ae3b chore: require frozen lockfile for releases 2026-03-09 10:43:04 -05:00
Dotta
7d8d6a5caf chore: remove lockfile changes from release branch 2026-03-09 10:38:18 -05:00
Dotta
948080fee9 Revert "chore: restore pnpm-lock.yaml from master"
This reverts commit 8d53800c19.
2026-03-09 10:37:38 -05:00
RememberV
af0e05f38c fix: onboarding wizard navigates to dashboard instead of first issue
After onboarding, the wizard navigated to the newly created issue
(e.g. /JAR/issues/JAR-1). useCompanyPageMemory then saved this path,
causing every subsequent company switch to land on that stale issue
instead of the dashboard.

Remove the issue-specific navigation branch so handleLaunch always
falls through to the dashboard route.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:35:40 +00:00
Dotta
8d53800c19 chore: restore pnpm-lock.yaml from master 2026-03-09 10:35:32 -05:00
Dotta
422f57b160 chore: use public-gh for manual release flow 2026-03-09 10:33:56 -05:00
Dotta
31c947bf7f fix: publish canaries in changesets pre mode 2026-03-09 10:23:04 -05:00
Dotta
f5bf743745 fix: support older git in release cleanup 2026-03-09 10:11:46 -05:00
Dotta
0a8b96cdb3 http clone 2026-03-09 10:03:45 -05:00
Dotta
a47ea343ba feat: add committed-ref onboarding smoke script 2026-03-09 09:59:43 -05:00
Dotta
0781b7a15c v0.3.0.md release changelog 2026-03-09 09:53:35 -05:00
Dotta
30ee59c324 chore: simplify release preflight workflow 2026-03-09 09:37:18 -05:00
Dotta
aa2b11d528 feat: extend release preflight smoke options 2026-03-09 09:21:56 -05:00
Dotta
e1ddcbb71f fix: disable git pagers in release preflight 2026-03-09 09:13:49 -05:00
Dotta
df94c98494 chore: add release preflight workflow 2026-03-09 09:06:45 -05:00
Dotta
a7cfd9f24b chore: formalize release workflow 2026-03-09 08:49:42 -05:00
Dotta
e48beafc90 Merge pull request #415 from paperclipai/chore/refresh-lockfile
chore(lockfile): refresh pnpm-lock.yaml
2026-03-09 08:27:43 -05:00
lockfile-bot
e6e41dba9d chore(lockfile): refresh pnpm-lock.yaml 2026-03-09 13:27:18 +00:00
online5880
f4a9788f2d fix: tighten Windows adapter command handling 2026-03-09 22:08:50 +09:00
Dotta
ccd501ea02 feat: add Playwright e2e tests for onboarding wizard flow
Scaffolds end-to-end testing with Playwright for the onboarding wizard.
Runs in skip_llm mode by default (UI-only, no LLM costs). Set
PAPERCLIP_E2E_SKIP_LLM=false for full heartbeat verification.

- tests/e2e/playwright.config.ts: Playwright config with webServer
- tests/e2e/onboarding.spec.ts: 4-step wizard flow test
- .github/workflows/e2e.yml: manual workflow_dispatch CI workflow
- package.json: test:e2e and test:e2e:headed scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 08:00:08 -05:00
online5880
d7b98a72b4 fix: support Windows command wrappers for local adapters 2026-03-09 21:52:06 +09:00
Dotta
210715117c Merge pull request #412 from paperclipai/dotta
Dotta
2026-03-09 07:50:18 -05:00
Dotta
38cb2bf3c4 fix: add missing disableSignUp to auth config objects in CLI
All auth config literals in the CLI were missing the required
disableSignUp field after it was added to authConfigSchema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 07:43:47 -05:00
Dotta
f2a0a0b804 fix: restore force push in lockfile refresh workflow
Simplify the PR-based flow: force push to update the branch if it
already exists, and only create a new PR when one doesn't exist yet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 07:38:49 -05:00
Dotta
035e1a9333 fix: use lockfile-bot identity and remove force push in refresh workflow
- Use lockfile-bot name/email instead of github-actions[bot]
- Remove force push: close any stale PR and delete branch first,
  then create a fresh branch and PR each time

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 07:33:49 -05:00
Dotta
ec4667c8b2 Merge remote-tracking branch 'public-gh/master'
* public-gh/master:
  fix: disable secure cookies for HTTP deployments
  feat(adapters): add claude-sonnet-4-6 and claude-haiku-4-6 models
  Add opencode-ai to global npm install in Dockerfile
  fix: correct env var priority for authDisableSignUp
  Add pi-local package.json to Dockerfile
  feat: add auth.disableSignUp config option
  refactor: extract roleLabels to shared constants
  fix(secrets): add secretKeys tracking to resolveEnvBindings for consistent redaction
  fix(db): reuse MIGRATIONS_FOLDER constant instead of recomputing
  fix(server): wake agent when issue status changes from backlog
  fix(server): use home-based path for run logs instead of cwd
  fix(db): use fileURLToPath for Windows-safe migration paths
  fix(server): auto-deduplicate agent names on creation instead of rejecting
  feat(ui): show human-readable role labels in agent list and properties
  fix(ui): prevent blank screen when prompt template is emptied
  fix(server): redact secret-sourced env vars in run logs by provenance
  fix(cli): split path and query in buildUrl to prevent %3F encoding
  fix(scripts): use shell on Windows to fix spawn EINVAL in dev-runner
2026-03-09 07:29:34 -05:00
Dotta
f32b76f213 fix: replace third-party action with gh CLI for lockfile PR creation
Replace peter-evans/create-pull-request with plain gh CLI commands to
avoid third-party supply chain risk. Uses only GitHub's own tooling
(GITHUB_TOKEN + gh CLI) to create the lockfile refresh PR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 07:26:31 -05:00
Dotta
ee7fddf8d5 fix: convert lockfile refresh to PR-based flow for protected master
The refresh-lockfile workflow was pushing directly to master, which fails
with branch protection rules. Convert to use peter-evans/create-pull-request
to create a PR instead. Exempt the bot's branch from the lockfile policy check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 07:22:34 -05:00
Dotta
77e04407b9 fix(publish): always bundle ui-dist into server package 2026-03-09 07:21:33 -05:00
Daniil Okhlopkov
1a75e6d15c fix: default dangerouslySkipPermissions to true for unattended agents
Agents run unattended and cannot respond to interactive permission
prompts from Claude Code. When dangerouslySkipPermissions is false
(the previous default), Claude Code blocks file operations with
"Claude requested permissions to write to /path, but you haven't
granted it yet" — making agents unable to edit files.

The OnboardingWizard already sets this to true for claude_local
agents (OnboardingWizard.tsx:277), but agents created or edited
outside the wizard inherit the default of false, breaking them.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:56:40 +09:00
Dominic O'Carroll
5e18ccace7 fix: route heartbeat cost recording through costService
Heartbeat runs recorded costs via direct SQL inserts into costEvents and
agents.spentMonthlyCents, bypassing costService.createEvent(). This skipped:
- companies.spentMonthlyCents update (company budget never incremented)
- Agent auto-pause when budget exceeded (enforcement gap)

Now calls costService(db).createEvent() which handles all three:
insert cost event, update agent spend, update company spend, and
auto-pause agent when budgetMonthlyCents is exceeded.
2026-03-09 14:49:01 +10:00
Dotta
7b70713fcb Merge pull request #376 from dalestubblefield/fix/http-secure-cookies
fix: disable secure cookies for HTTP deployments
2026-03-08 22:15:54 -05:00
Dale Stubblefield
ad55af04cc fix: disable secure cookies for HTTP deployments
Fixes login failing silently on authenticated + private deployments
served over plain HTTP (e.g. Tailscale, LAN). Users can sign up and
sign in, but the session cookie is rejected by the browser so they
are immediately redirected back to the login page.

Better Auth defaults to __Secure- prefixed cookies with the Secure
flag when NODE_ENV=production. Browsers silently reject Secure cookies
on non-HTTPS origins. This detects when PAPERCLIP_PUBLIC_URL uses
http:// and sets useSecureCookies: false so session cookies work
without HTTPS.
2026-03-08 22:00:51 -05:00
AiMagic5000
57406dbc90 fix(docker): run production server as non-root node user
Switch the production stage to the built-in node user from
node:lts-trixie-slim, fixing two runtime failures:

1. Claude CLI rejects --dangerously-skip-permissions when the
   process UID is 0, making the claude-local adapter unusable.
2. The server crashed at startup (EACCES) because /paperclip was
   root-owned and the process could not write logs or instance data.

Changes vs the naive fix:
- Use COPY --chown=node:node instead of a separate RUN chown -R,
  avoiding a duplicate image layer that would double the size of
  the /app tree in the final image.
- Consolidate mkdir /paperclip + chown into the same RUN layer as
  the npm global install (already runs as root) to keep layer count
  minimal.
- Add USER node before CMD so the process runs unprivileged.

The VOLUME declaration comes after chown so freshly-mounted
anonymous volumes inherit the correct node:node ownership.

Fixes #344
2026-03-08 13:47:59 -07:00
Kevin Mok
432d7e72fa Merge upstream/master into add-gpt-5-4-xhigh-effort 2026-03-08 12:10:59 -05:00
Dotta
e35e2c4343 Fine-tune mobile status icon alignment on issues page: add 1px top padding
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 11:00:02 -05:00
Dotta
d58f269281 Fix mobile status icon vertical alignment: remove pt-0.5 to center with text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 10:58:39 -05:00
Dotta
2a7043d677 GitHub-style mobile issue rows: status left column, hide priority, unread dot right
- Move status icon to left column on mobile across issues list, inbox, and dashboard
- Hide priority icon on mobile (only show on desktop)
- Move unread indicator dot to right side vertically centered on mobile inbox
- Stale work section: show status icon instead of clock on mobile
- Desktop layout unchanged

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 10:56:17 -05:00
Dotta
31b5ff1c61 Add bottom padding to new issue description editor for iOS keyboard
The description text was being covered by the property chips bar when
typing long content on iOS with the virtual keyboard open.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 08:21:07 -05:00
Dotta
c674462a02 Merge pull request #238 from fahmmin/fix/dev-runner-windows-spawn
fix(scripts): use shell on Windows to fix spawn EINVAL in dev-runner
2026-03-07 21:47:47 -06:00
Dotta
e3ff0c8e1b Merge pull request #279 from JasonOA888/feat/issue-241-disable-signup
feat: add auth.disableSignUp config option
2026-03-07 21:30:16 -06:00
Dotta
17b10c43fe Merge pull request #260 from mvanhorn/fix/204-heartbeat-url-encoding
fix(cli): split path and query in buildUrl to prevent %3F encoding
2026-03-07 21:23:05 -06:00
Dotta
343d4e5877 Merge pull request #261 from mvanhorn/fix/234-secret-env-redaction
fix(server): redact secret-sourced env vars in logs by provenance
2026-03-07 21:22:00 -06:00
Dotta
1078c7dd2b Merge pull request #262 from mvanhorn/fix/191-blank-screen-prompt-template
fix(ui): prevent blank screen when prompt template is emptied
2026-03-07 21:20:46 -06:00
Dotta
4c630bc66e Merge pull request #263 from mvanhorn/feat/180-agent-role-labels
feat(ui): show human-readable role labels in agent list and properties
2026-03-07 21:19:58 -06:00
Dotta
f5190f28d1 Merge pull request #264 from mvanhorn/fix/232-deduplicate-agent-names
fix(server): auto-deduplicate agent names on creation
2026-03-07 21:18:31 -06:00
Dotta
edfc6be63c Merge pull request #265 from mvanhorn/fix/132-windows-path-doubling
fix(db): use fileURLToPath for Windows-safe migration paths
2026-03-07 21:16:58 -06:00
Dotta
61551ffea3 Merge pull request #266 from mvanhorn/fix/89-run-log-location
fix(server): use home-based path for run logs instead of cwd
2026-03-07 21:15:56 -06:00
Dotta
0fedd8a395 Merge pull request #267 from mvanhorn/fix/167-backlog-assignee-wake
fix(server): wake agent when issue status changes from backlog
2026-03-07 21:14:52 -06:00
Dotta
b090c33ca1 Merge pull request #283 from mingfang/patch-1
Add pi-local package.json to Dockerfile
2026-03-07 21:07:07 -06:00
Dotta
3fb96506bd Merge pull request #284 from mingfang/patch-2
Add opencode-ai to global npm install in Dockerfile
2026-03-07 21:06:09 -06:00
Dotta
dcf879f6fb Merge pull request #293 from cpfarhood/feat/add-claude-4-6-models
feat(adapters): add claude-sonnet-4-6 and claude-haiku-4-6 to claude_local model list
2026-03-07 21:00:07 -06:00
Chris Farhood
4e01633202 feat(adapters): add claude-sonnet-4-6 and claude-haiku-4-6 models 2026-03-07 21:57:06 -05:00
Ming Fang
ff3f04ff48 Add opencode-ai to global npm install in Dockerfile 2026-03-07 21:24:56 -05:00
Jason
91fda5d04f fix: correct env var priority for authDisableSignUp
- Env var now properly overrides file config in both directions
- Follows established pattern for boolean config flags
- Removed redundant ?? false (field is typed boolean)
- PAPERCLIP_AUTH_DISABLE_SIGN_UP can now set to 'false' to
  override file config's 'true'
2026-03-08 10:18:27 +08:00
Ming Fang
77e06c57f9 Add pi-local package.json to Dockerfile 2026-03-07 21:15:12 -05:00
Dotta
0f75c35392 Fix unread dot making inbox rows taller than read rows
Replace <button> with <span role="button"> for the unread dot to
eliminate browser UA button styles (min-height, padding) that caused
unread rows to be taller despite explicit h-4 constraint. Also
reduce dot from h-2.5/w-2.5 to h-2/w-2 for a more subtle indicator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 20:12:26 -06:00
Dotta
45473b3e72 Move scroll-to-bottom button to issue detail and run pages
Removed the scroll-to-bottom button from IssuesList (wrong location)
and created a shared ScrollToBottom component. Added it to IssueDetail
and RunDetail pages. On mobile, the button sits above the bottom nav
to avoid overlap.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 20:07:39 -06:00
Dotta
a96556b8f4 Move unread badge inline with metadata row in inbox
Moves the unread dot from a separate left column into the metadata
line (alongside status/identifier), with an empty placeholder for
read items to keep spacing consistent. Reduces left padding on
mobile inbox rows to reclaim space.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 20:04:41 -06:00
Dotta
ce8fe38ffc Unify mobile issue row layout across issues, inbox, and dashboard
Add PriorityIcon and timeAgo to IssuesList mobile rows to match the
pattern used in Inbox and Dashboard. Align Dashboard row padding to
match Inbox. All mobile issue rows now show: title (2-line clamp),
then priority + status + identifier + relative time.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 20:04:32 -06:00
Dotta
6e86f69f95 Unify issue rows with GitHub-style mobile layout across app
On mobile, all issue rows now show title first (up to 2 lines),
with metadata (icons, identifier, timestamp) on a second line below.
Desktop layout is preserved as single-line rows.

Affected locations: Inbox recent issues, Inbox stale work,
Dashboard recent tasks, and IssuesList.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 20:01:16 -06:00
Dotta
7661fae4b3 Fix inbox row irregular heights on mobile from unread badge
- Give unread dot container fixed h-5 so rows are consistent height
  regardless of badge presence
- Use flex-wrap on mobile so title gets its own line with line-clamp-2
- On sm+ screens, keep single-line truncated layout
- Move timestamp to ml-auto with sm:order-last for clean wrapping

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:55:32 -06:00
Dotta
ba080cb4dd Fix stale work section overflowing on mobile in inbox
Add min-w-0 and overflow-hidden to the stale work row flex containers
so the title truncates properly on narrow screens. Add shrink-0 to
identifier and assignee spans to prevent them from being compressed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:52:29 -06:00
Jason
3860812323 feat: add auth.disableSignUp config option
- Added disableSignUp to authConfigSchema in config-schema.ts
- Added authDisableSignUp to Config interface
- Added parsing from PAPERCLIP_AUTH_DISABLE_SIGN_UP env or config file
- Passed to better-auth emailAndPassword.disableSignUp

When true, blocks new user registrations on public instances.
Defaults to false (backward compatible).

Fixes #241
2026-03-08 09:38:32 +08:00
Matt Van Horn
2639184f46 refactor: extract roleLabels to shared constants
Move duplicated roleLabels map from AgentProperties.tsx, Agents.tsx,
OrgChart.tsx, and agent-config-primitives.tsx into AGENT_ROLE_LABELS
in packages/shared/src/constants.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:07:14 -08:00
Matt Van Horn
61966fba1f fix(secrets): add secretKeys tracking to resolveEnvBindings for consistent redaction
resolveEnvBindings now returns { env, secretKeys } matching the pattern
already used by resolveAdapterConfigForRuntime, so any caller can redact
secret-sourced values by provenance rather than key-name heuristics alone.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:06:10 -08:00
Matt Van Horn
54b512f9e0 fix(db): reuse MIGRATIONS_FOLDER constant instead of recomputing
The local migrationsFolder variable in migratePostgresIfEmpty duplicated
the module-level MIGRATIONS_FOLDER constant. Reuse the constant to keep
a single source of truth for the migration path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:01:36 -08:00
Dotta
667d23e79e Merge remote-tracking branch 'public-gh/master'
* public-gh/master:
  Revert lockfile changes from OpenClaw gateway cleanup
  Enhance plugin architecture by introducing agent tool contributions and plugin-to-plugin communication. Update workspace plugin definitions for direct OS access and refine UI extension surfaces. Document new capabilities for plugin settings UI and lifecycle management.
  Remove legacy OpenClaw adapter and keep gateway-only flow
  Fix CI typecheck and default OpenClaw sessions to issue scope
  fix(server): serve cached index.html in SPA catch-all to prevent 500
  Add CEO OpenClaw invite endpoint and update onboarding UX
  openclaw gateway: auto-approve first pairing and retry
  openclaw gateway: persist device keys on create/update and clarify pairing flow
  plugin spec draft ideas v0
  openclaw gateway: persist device keys and smoke pairing flow
  openclaw-gateway: document and surface pairing-mode requirements
  fix(smoke): disambiguate case C ack comment target
  fix(openclaw-gateway): enforce join token validation and add smoke preflight gates
  fix(openclaw): make invite snippet/onboarding gateway-first
2026-03-07 18:59:23 -06:00
Dotta
416177ae4c Merge pull request #270 from paperclipai/openclawgateway
Openclaw gateway
2026-03-07 18:57:16 -06:00
Dotta
72cc748aa8 Merge pull request #273 from gsxdsm/feature/plugins
Enhance plugin architecture by introducing agent tool contributions a…
2026-03-07 18:56:00 -06:00
Dotta
9299660388 Merge pull request #269 from mvanhorn/fix/233-spa-fallback-500
fix(server): serve cached index.html in SPA catch-all to prevent 500
2026-03-07 18:54:33 -06:00
Dotta
2cb82f326f Revert lockfile changes from OpenClaw gateway cleanup 2026-03-07 18:54:16 -06:00
gsxdsm
f81d2ebcc4 Enhance plugin architecture by introducing agent tool contributions and plugin-to-plugin communication. Update workspace plugin definitions for direct OS access and refine UI extension surfaces. Document new capabilities for plugin settings UI and lifecycle management. 2026-03-07 16:52:35 -08:00
Dotta
048e2b1bfe Remove legacy OpenClaw adapter and keep gateway-only flow 2026-03-07 18:50:25 -06:00
Dotta
5fae7d4de7 Fix CI typecheck and default OpenClaw sessions to issue scope 2026-03-07 18:33:40 -06:00
Matt Van Horn
0f32fffe79 fix(server): serve cached index.html in SPA catch-all to prevent 500
res.sendFile can emit NotFoundError from the send module in certain
path resolution scenarios, causing 500s on company-scoped SPA routes.
Cache index.html at startup and serve it directly, which is both
more reliable and faster.

Fixes #233

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 16:20:19 -08:00
Dotta
0233525e99 Add CEO OpenClaw invite endpoint and update onboarding UX 2026-03-07 18:19:06 -06:00
Matt Van Horn
20b171bd16 fix(server): wake agent when issue status changes from backlog
Previously, agents were only woken when the assignee changed. Now
also wakes the assigned agent when an issue transitions out of
backlog status (e.g. backlog -> todo).

Fixes #167

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 16:18:41 -08:00
Matt Van Horn
3f2274cd8d fix(server): use home-based path for run logs instead of cwd
Run logs defaulted to process.cwd()/data/run-logs, placing logs in
unexpected locations when launched from non-home directories. Now
defaults to ~/.paperclip/instances/<id>/data/run-logs/.

Fixes #89

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 16:18:14 -08:00
Matt Van Horn
c59e059976 fix(db): use fileURLToPath for Windows-safe migration paths
URL.pathname returns /C:/... on Windows, causing doubled drive letters
when Node prepends the current drive. fileURLToPath handles this
correctly across platforms.

Fixes #132

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 16:17:45 -08:00
Matt Van Horn
9933039094 fix(server): auto-deduplicate agent names on creation instead of rejecting
Replace assertCompanyShortnameAvailable with deduplicateAgentName in
the create path so duplicate names get auto-suffixed (e.g. Engineer 2)
instead of throwing a conflict error.

Fixes #232

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 16:08:53 -08:00
Matt Van Horn
b886eb3cf0 feat(ui): show human-readable role labels in agent list and properties
Use roleLabels lookup in list view subtitle and AgentProperties
panel instead of raw role strings.

Fixes #180

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 16:08:52 -08:00
Matt Van Horn
53c944e8bc fix(ui): prevent blank screen when prompt template is emptied
Change onChange handler from v || undefined to v ?? "" so empty
strings don't become undefined and crash downstream .trim() calls.

Fixes #191

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 16:08:51 -08:00
Matt Van Horn
977f5570be fix(server): redact secret-sourced env vars in run logs by provenance
resolveAdapterConfigForRuntime now returns a secretKeys set tracking
which env vars came from secret_ref bindings. The onAdapterMeta
callback uses this to redact them regardless of key name.

Fixes #234

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 16:08:49 -08:00
Matt Van Horn
609b55f530 fix(cli): split path and query in buildUrl to prevent %3F encoding
The URL constructor's pathname setter encodes ? as %3F, breaking
heartbeat event polling. Split query params before assignment.

Fixes #204

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 16:08:48 -08:00
Dotta
2223afa0e9 openclaw gateway: auto-approve first pairing and retry 2026-03-07 17:46:55 -06:00
Dotta
3479ea6e80 openclaw gateway: persist device keys on create/update and clarify pairing flow 2026-03-07 17:34:38 -06:00
Dotta
63a876ca3c Merge pull request #256 from paperclipai/dotta
Dotta
2026-03-07 17:18:46 -06:00
Dotta
df0f101fbd plugin spec draft ideas v0 2026-03-07 17:16:37 -06:00
Dotta
0abb6a1205 openclaw gateway: persist device keys and smoke pairing flow 2026-03-07 17:05:36 -06:00
Dotta
d52f1d4b44 openclaw-gateway: document and surface pairing-mode requirements 2026-03-07 16:32:49 -06:00
Dotta
e27ec5de8c fix(smoke): disambiguate case C ack comment target 2026-03-07 16:16:53 -06:00
Dotta
83488b4ed0 fix(openclaw-gateway): enforce join token validation and add smoke preflight gates 2026-03-07 16:01:19 -06:00
Dotta
271a632f1c fix(openclaw): make invite snippet/onboarding gateway-first 2026-03-07 15:39:12 -06:00
Dotta
9a0e3a8425 Merge pull request #252 from paperclipai/dotta
Dotta updates - sorry it's so large
2026-03-07 15:20:39 -06:00
Dotta
1c1b86f495 fix(issues): guard missing companyId and enrich activity log context
Add 400 response for /issues without companyId, tag issue.updated
activity with source:comment when triggered by a comment, and mark
comment activities with updated:true when field changes accompany them.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 15:19:08 -06:00
Dotta
1420b86aa7 fix(server): attach raw Error to res.err and avoid pino err key collision
Extract attachErrorContext helper to DRY up the error handler, attach the
original Error object to res.err so pino can serialize stack traces, and
rename the log context key from err to errorContext so it doesn't clash
with pino's built-in err serializer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 15:19:03 -06:00
Dotta
22053d18e4 chore: regenerate pnpm-lock.yaml after merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 15:18:23 -06:00
Dotta
3b4db7a3bc Merge remote-tracking branch 'public-gh/master'
* public-gh/master:
  Address PR feedback: keep testEnvironment non-destructive, warn on swallowed errors
  Apply suggestion from @greptile-apps[bot]
  Fix opencode-local adapter: parser, UI, CLI, and environment tests
  Rename Invoke button to Run Heartbeat for clarity
  fixing overhanging recommended text in onboarding
  Add Contributing guide
  feat(pi-local): fix bugs, add RPC mode, improve cost tracking and output handling
  fix(sidebar-badges): include approvals in inbox badge count
  feat: add Pi adapter support to constants and onboarding UI
  Adding support for pi-local
  ci: clarify fail-fast lockfile refresh behavior
  ci: remove unnecessary full-history checkout
  ci: fix pnpm lockfile policy checks
  ci: split workflows and move pnpm lockfile ownership to GitHub Actions
  Add License
  fix: use root option in sendFile to avoid dotfile 500 on SPA refresh

# Conflicts:
#	cli/src/adapters/registry.ts
#	pnpm-lock.yaml
#	server/src/adapters/registry.ts
#	ui/package.json
#	ui/src/adapters/registry.ts
2026-03-07 15:18:02 -06:00
Dotta
db15dfaf5e fix(server): return 404 JSON for unmatched API routes
Add catch-all handler after API router to return a proper 404 JSON
response instead of falling through to the SPA handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 15:15:51 -06:00
Dotta
1afadd7354 Merge pull request #173 from zvictor/ci
ci: split workflows and move pnpm lockfile ownership to GitHub Actions
2026-03-07 15:15:20 -06:00
Dotta
9ac2e71187 fix(smoke): pin OpenClaw docker harness to stable stock ref 2026-03-07 15:09:52 -06:00
Dotta
3bde21bb06 Merge pull request #240 from aaaaron/fix-opencode-local-adapter-tests
Fix opencode-local adapter tests and behavior
2026-03-07 14:56:31 -06:00
Aaron
fb684f25e9 Address PR feedback: keep testEnvironment non-destructive, warn on swallowed errors
- Update cwd test to expect an error for missing directories (matches
  createIfMissing: false accepted from review)
- Add warn-level check for non-ProviderModelNotFoundError failures
  during best-effort model discovery when no model is configured

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 15:50:14 -05:00
Aaron
fa7acd2482 Apply suggestion from @greptile-apps[bot]
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-07 15:50:14 -05:00
Aaron
5114c32810 Fix opencode-local adapter: parser, UI, CLI, and environment tests
- Move costUsd to top-level return field in parseOpenCodeJsonl (out of usage)
- Fix session-not-found regex to match "Session not found" pattern
- Use callID for toolUseId in UI stdout parser, add status/metadata header
- Fix CLI formatter: separate tool_call/tool_result lines, split step_finish
- Enable createIfMissing for cwd validation in environment tests
- Add empty OPENAI_API_KEY override detection
- Classify ProviderModelNotFoundError as warning during model discovery
- Make model discovery best-effort when no model is configured

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 15:50:14 -05:00
Aaron
672d769c68 Address PR feedback: keep testEnvironment non-destructive, warn on swallowed errors
- Update cwd test to expect an error for missing directories (matches
  createIfMissing: false accepted from review)
- Add warn-level check for non-ProviderModelNotFoundError failures
  during best-effort model discovery when no model is configured

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:15:10 +00:00
Aaron
46c343f81d Apply suggestion from @greptile-apps[bot]
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-07 14:12:22 -05:00
Aaron
17058dd751 Fix opencode-local adapter: parser, UI, CLI, and environment tests
- Move costUsd to top-level return field in parseOpenCodeJsonl (out of usage)
- Fix session-not-found regex to match "Session not found" pattern
- Use callID for toolUseId in UI stdout parser, add status/metadata header
- Fix CLI formatter: separate tool_call/tool_result lines, split step_finish
- Enable createIfMissing for cwd validation in environment tests
- Add empty OPENAI_API_KEY override detection
- Classify ProviderModelNotFoundError as warning during model discovery
- Make model discovery best-effort when no model is configured

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:01:04 +00:00
Fahmin
346152f67d fix(scripts): use shell on Windows to fix spawn EINVAL in dev-runner 2026-03-08 00:15:56 +05:30
JonCSykes
a765d342e0 Merge remote-tracking branch 'origin/feature/upload-company-logo' into feature/upload-company-logo
# Conflicts:
#	pnpm-lock.yaml
#	server/package.json
2026-03-07 13:36:41 -05:00
Jon Sykes
a5fda1546b Merge branch 'master' into feature/upload-company-logo 2026-03-07 13:34:57 -05:00
JonCSykes
4dffdc4de2 Merge master into feature/upload-company-logo 2026-03-07 12:58:02 -05:00
Dotta
dd14643848 ui: unify comment/update issue toasts 2026-03-07 10:32:22 -06:00
Dotta
1dac0ec7cf docs: add manual OpenClaw onboarding checklist 2026-03-07 10:32:22 -06:00
Dotta
7c0a3efea6 feat(ui): add plus button to sidebar AGENTS header
Add a "+" button next to "AGENTS" in the sidebar, matching the existing
pattern used by Projects. Clicking it opens the agent creation choice
modal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:19:51 -06:00
Dotta
671a8ae554 Merge pull request #221 from richardanaya/rename-invoke-to-run-heartbeat
Rename Invoke button to Run Heartbeat for UX consistency
2026-03-07 10:17:04 -06:00
Dotta
baa71d6a08 fix(ui): enforce min 2-line height for agent issue titles on dashboard
Short titles now always occupy two lines of vertical space, ensuring
consistent horizontal alignment across agent cards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:16:31 -06:00
Richard Anaya
638f2303bb Rename Invoke button to Run Heartbeat for clarity 2026-03-07 08:12:37 -08:00
Dotta
a4d0901e89 Revert "Fix markdown editor escaped list markers"
This reverts commit fbcd80948e.
2026-03-07 10:09:04 -06:00
Dotta
f85f2fbcc2 feat(ui): add copy-as-markdown button to comment headers
Adds a small copy icon to the right of each comment's date that copies
the comment body as raw markdown to the clipboard. Shows a checkmark
briefly after copying.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:57:50 -06:00
Dotta
fbcd80948e Fix markdown editor escaped list markers 2026-03-07 09:56:46 -06:00
Dotta
9d6a83dcca docs(openclaw-gateway): record e2e execution findings from stock lane validation
Document observed failures before wake-text fix and successful
stock-clean lane pass after fix. Note instrumentation lane limitations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:52:08 -06:00
Dotta
a251a53571 fix(openclaw): embed explicit heartbeat workflow in wake text
Include the full Paperclip API workflow (checkout, fetch, update) and
endpoint bans in the wake text sent to OpenClaw agents, preventing
them from guessing non-existent endpoints. Applied to both openclaw
and openclaw_gateway adapters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:52:01 -06:00
Dotta
63afce3692 Merge pull request #151 from eltociear/add-license
Add License
2026-03-07 09:50:28 -06:00
Dotta
e07646bade Merge pull request #218 from richardanaya/fix-recommended-pill
fixing overhanging recommended text in onboarding
2026-03-07 09:49:20 -06:00
Richard Anaya
ddb7101fa5 fixing overhanging recommended text in onboarding 2026-03-07 07:47:24 -08:00
Dotta
3f42357e5f Merge pull request #196 from hougangdev/fix/inbox-badge-missing-approvals
fix(sidebar-badges): include approvals in inbox badge count
2026-03-07 09:46:03 -06:00
Dotta
3b08d4d582 Merge pull request #217 from aaaaron/CONTRIBUTING
Add CONTRIBUTING.md guide
2026-03-07 09:39:51 -06:00
Aaron
049f768bc7 Add Contributing guide 2026-03-07 15:38:56 +00:00
Dotta
19c295ec03 Merge pull request #183 from richardanaya/master
Adding support for pi-local
2026-03-07 09:31:35 -06:00
Richard Anaya
a6b5f12daf feat(pi-local): fix bugs, add RPC mode, improve cost tracking and output handling
Major improvements to the Pi local adapter:

Bug Fixes (Greptile-identified):
- Fix string interpolation in models.ts error message (was showing literal ${detail})
- Fix tool matching in parse.ts to use toolCallId instead of toolName for correct
  multi-call handling and result assignment
- Fix dead code in execute.ts by tracking instructionsReadFailed flag

Feature Improvements:
- Switch from print mode (-p) to RPC mode (--mode rpc) to prevent agent from
  exiting prematurely and ensure proper lifecycle completion
- Add stdin command sending via JSON-RPC format for prompt delivery
- Add line buffering in execute.ts to handle partial JSON chunks correctly
- Filter RPC protocol messages (response, extension_ui_request, etc.) from transcript

Cost Tracking:
- Extract cost and usage data from turn_end assistant messages
- Support both Pi format (input/output/cacheRead/cost.total) and generic format
- Add tests for cost extraction and accumulation across multiple turns

All tests pass (12/12), typecheck clean, server builds successfully.
2026-03-07 07:23:44 -08:00
Dotta
4bd6961020 fix(openclaw-gateway): add diagnostics capture and two-lane validation to e2e
Capture run events, logs, issue state, and container logs on failures
or timeouts for debugging. Write compatibility JSON keys for claimed
API key. Add two-lane validation requirement to test plan.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:22:40 -06:00
Dotta
fd0799fd71 Merge pull request #78 from MumuTW/fix/dashboard-spa-refresh-500
fix: resolve 500 error on SPA route refresh (#48)
2026-03-07 09:05:19 -06:00
Dotta
b91820afd3 Merge remote-tracking branch 'public-gh/master'
* public-gh/master:
  fix(issues): skip agent wakeup when issue status is backlog
2026-03-07 09:04:39 -06:00
Dotta
0315e4cdc2 docs: add local-cli mode and self-test playbook to paperclip skill
Document the agent local-cli command for manual CLI usage and add a
step-by-step self-test playbook for validating assignment flows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:59:51 -06:00
Dotta
654463c28f feat(cli): add agent local-cli command for skill install and env export
New subcommand to install Paperclip skills for Claude/Codex agents and
print the required PAPERCLIP_* environment variables for local CLI
usage outside heartbeat runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:59:45 -06:00
Dotta
f1ad727f8e feat(ui): render mermaid diagrams in markdown content
Lazy-load mermaid.js and render fenced mermaid code blocks as inline
SVG diagrams with dark/light mode support. Falls back to showing the
source code on parse errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:59:39 -06:00
Dotta
10cccc07cd feat: deduplicate project shortnames on create and update
Ensure unique URL-safe shortnames by appending numeric suffixes when
collisions occur. Applied during project creation, update, and company
import flows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:59:34 -06:00
Dotta
a498c268c5 feat: add openclaw_gateway adapter
New adapter type for invoking OpenClaw agents via the gateway protocol.
Registers in server, CLI, and UI adapter registries. Adds onboarding
wizard support with gateway URL field and e2e smoke test script.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:59:29 -06:00
Dotta
fa8499719a ui: remove local toast on issue create 2026-03-07 08:55:19 -06:00
Dotta
1fcc6900ff ui: suppress self-authored issue toasts 2026-03-07 08:42:58 -06:00
Dotta
45708a06f1 ui: avoid duplicate and self comment toasts 2026-03-07 08:31:59 -06:00
Dotta
792397c2a9 feat(ui): add agent creation choice modal and full-page config
Replace the direct agent config dialog with a choice modal offering two
paths: "Ask the CEO to create a new agent" (opens pre-filled issue) or
"I want advanced configuration myself" (navigates to /agents/new).

- Extend NewIssueDefaults with title/description for pre-fill support
- Add /agents/new route with full-page agent configuration form
- NewAgentDialog now shows CEO recommendation modal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:26:49 -06:00
hougangdev
36e4e67025 fix(sidebar-badges): include approvals in inbox badge count
When a company has "require board approval for new agents" enabled,
hiring an agent creates a pending approval that requires the user
(as a board member) to approve before the agent can start working.
However, the sidebar inbox badge did not include pending approvals
in its count, so there was no visual indicator that action was needed.
Users had no way of knowing an approval was waiting unless they
happened to open the Inbox page manually.

The root cause: the sidebar-badges service correctly included
approvals in the inbox total, but the route handler overwrites
badges.inbox to add alertsCount and staleIssueCount — and in
doing so dropped badges.approvals from the sum.

Add badges.approvals to the inbox count recalculation so that
pending and revision-requested approvals surface in the sidebar
notification badge alongside failed runs, alerts, stale work,
and join requests.

Affected files:
- server/src/routes/sidebar-badges.ts
2026-03-07 17:38:07 +08:00
JonCSykes
f44efce265 Add @types/node as a devDependency in cursor-local package 2026-03-06 23:15:45 -05:00
JonCSykes
6f16fc0a93 Merge remote-tracking branch 'origin/master' into feature/upload-company-logo 2026-03-06 22:55:25 -05:00
Richard Anaya
6077ae6064 feat: add Pi adapter support to constants and onboarding UI 2026-03-06 18:47:44 -08:00
Richard Anaya
eb7f690ceb Adding support for pi-local 2026-03-06 18:29:38 -08:00
zvictor
ef0e08b8ed ci: clarify fail-fast lockfile refresh behavior 2026-03-06 21:49:13 -03:00
zvictor
3bcdf3e3ad ci: remove unnecessary full-history checkout 2026-03-06 21:40:05 -03:00
zvictor
fccec94805 ci: fix pnpm lockfile policy checks 2026-03-06 21:29:16 -03:00
zvictor
bee9fdd207 ci: split workflows and move pnpm lockfile ownership to GitHub Actions 2026-03-06 21:21:28 -03:00
Dotta
0ae5d81deb fix(ui): show agent issue title as two lines on dashboard
Change the issue title in agent run cards from single-line truncate
to line-clamp-2 so titles always occupy two lines for consistent card height.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:09:21 -06:00
Dotta
ffc59f5b08 Merge pull request #159 from Logesh-waran2003/logesh/fix-backlog-issue-wake
fix(issues): skip agent wakeup when issue status is backlog
2026-03-06 16:57:17 -06:00
Dotta
f5f8c4a883 Merge branch 'master' of github.com-dotta:paperclipai/paperclip
* 'master' of github.com-dotta:paperclipai/paperclip:
  skip agent self-wake on comment and closed-issue wake without reopen
2026-03-06 16:52:11 -06:00
Dotta
e693e3d466 feat(release): auto-create GitHub Release on publish
Add create_github_release helper to release script that creates a
GitHub Release via gh CLI after tagging, using release notes from
releases/vX.Y.Z.md if available or auto-generated notes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 16:50:25 -06:00
Dotta
e4928f3a10 feat(openclaw): support x-openclaw-token header alongside legacy x-openclaw-auth
Accept x-openclaw-token as the preferred auth header for OpenClaw
invite/join flows, falling back to x-openclaw-auth for backwards
compatibility. Update diagnostics messages accordingly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 16:50:20 -06:00
Dotta
514dc43923 feat(openclaw): support /hooks/agent endpoint and multi-endpoint detection
Add OpenClawEndpointKind type to distinguish between /hooks/wake,
/hooks/agent, open_responses, and generic endpoints. Build appropriate
payloads per endpoint kind with optional sessionKey inclusion.
Refactor webhook execution to use endpoint-aware payload construction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 16:50:15 -06:00
JonCSykes
4599fc5a8d Merge remote-tracking branch 'origin/master' into feature/upload-company-logo 2026-03-06 17:21:06 -05:00
JonCSykes
a4702e48f9 Add sanitization for SVG uploads and enhance security headers for asset responses
- Introduced SVG sanitization using `dompurify` to prevent malicious content.
- Updated tests to validate SVG sanitization with various scenarios.
- Enhanced response headers for assets, adding CSP and nosniff for SVGs.
- Adjusted UI to better clarify supported file types for logo uploads.
- Updated dependencies to include `jsdom` and `dompurify`.
2026-03-06 17:18:43 -05:00
Dotta
b539462319 Revert "openclaw: force webhook transport to use hooks/wake"
This reverts commit aa7e069044.
2026-03-06 16:18:26 -06:00
Dotta
aa7e069044 openclaw: force webhook transport to use hooks/wake 2026-03-06 16:11:11 -06:00
Dotta
3b0ff94e3f Merge pull request #154 from cschneid/fix/no-self-wake-on-comment
skip agent self-wake on comment and closed-issue wake without reopen
2026-03-06 16:05:28 -06:00
Dotta
5ab1c18530 fix openclaw webhook payload for /v1/responses 2026-03-06 15:50:08 -06:00
Dotta
36013c35d9 dev: make pnpm dev watch workspace package changes 2026-03-06 15:48:35 -06:00
JonCSykes
1448b55ca4 Improve error handling in CompanySettings for mutation failure messages. 2026-03-06 16:47:04 -05:00
JonCSykes
b19d0b6f3b Add support for company logos, including schema adjustments, validation, assets handling, and UI display enhancements. 2026-03-06 16:39:35 -05:00
Dotta
b155415d7d Reintroduce OpenClaw webhook transport alongside SSE 2026-03-06 15:15:24 -06:00
Logesh
d7f68ec1c9 fix(issues): skip agent wakeup when issue status is backlog
Agents were being woken immediately when an issue was created or
reassigned with status "backlog", defeating the purpose of backlog
as "not ready to work on yet" (issue #96).

Added `&& issue.status !== "backlog"` guard to both the CREATE and
PATCH wakeup paths, mirroring the existing done/cancelled suppression
pattern already present in the file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 02:40:53 +05:30
Dotta
af09510f6a fix openclaw openresponses terminal event detection 2026-03-06 14:56:40 -06:00
Dotta
a2bdfb0dd3 stream live run detail output via websocket 2026-03-06 14:39:49 -06:00
Dotta
67247b5d6a feat(ui): hide sensitive OpenClaw fields behind password toggle with eye icon
Webhook auth header and gateway auth token fields now render as password
inputs by default, with an eye/eye-off toggle icon on the left to reveal
the value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:36:02 -06:00
Dotta
5f2dfcb94e fix(ui): prevent long issue titles from overflowing timestamp in inbox
Add min-w-0 to the flex container so truncate works correctly on titles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:30:02 -06:00
Dotta
67491483b7 feat(ui): show toast notification for new join requests
When a join request arrives via WebSocket live event, display a toast
prompting the user to review it in the inbox.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:28:25 -06:00
Dotta
54a4f784a4 fix(ui): clickable unread dot in inbox with fade-out, remove empty circle for read issues
In the My Recent Issues section, the blue unread dot is now a button that
marks the issue as read on click with a smooth opacity fade-out. Already-read
issues show empty space instead of a hollow circle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:27:12 -06:00
Dotta
5aecb148a2 Clarify missing companyId error for malformed issues path 2026-03-06 14:25:34 -06:00
Dotta
f49a003bd9 fix: improve invite onboarding text and callback reachability prompt
Add skill URL note to onboarding text document and strengthen callback
reachability test language in company settings invite snippet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:16:39 -06:00
Dotta
feb384acca feat(ui): coalesce streaming deltas and deduplicate live feed items
Merge consecutive assistant/thinking deltas into a single feed entry
instead of creating one per chunk. Add dedupeKey to FeedItem, increase
streaming text cap to 4000 chars, and bump seen-keys limit to 6000.
Applied consistently to both ActiveAgentsPanel and LiveRunWidget.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:16:34 -06:00
Dotta
c9718dc27a Merge remote-tracking branch 'public-gh/master'
* public-gh/master:
  fix(auth): apply effective trusted origins and honor allowed hostnames in public mode
  fix(onboard): preserve env-derived secrets defaults and report ignored exposure env in local_trusted mode
  fix: parseBooleanFromEnv silently treats common truthy values as false
  set `PAPERCLIP_PUBLIC_URL` in compose files
  centralize URLs into single canonical URL var
2026-03-06 14:15:55 -06:00
Dotta
0b42045053 Auto-deduplicate agent shortname on join request approval
When approving an agent join request with a shortname already in use,
append a numeric suffix (e.g. "openclaw-2") instead of returning an error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:54:58 -06:00
Dotta
d8f7c6bf81 Brighten destructive/error red in dark mode for readability
The dark-mode --destructive color was too dim (lightness 0.396) to read
against dark backgrounds. Bumped to 0.637 lightness with higher chroma
so error text is clearly visible. Set --destructive-foreground to white
for contrast on destructive button backgrounds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:53:08 -06:00
Dotta
c8bd578415 fix run detail live log polling when log ref is delayed 2026-03-06 13:24:20 -06:00
Chris Schneider
5dfd9a2429 skip agent self-wake on comment and closed-issue wake without reopen 2026-03-06 19:21:08 +00:00
Dotta
0324259da3 Handle single-key wrapped OpenClaw auth headers 2026-03-06 12:49:41 -06:00
Dotta
7af9aa61fa Merge pull request #99 from zvictor/canonical-url
feat: Canonical Public URL for Authenticated Deployments (`PAPERCLIP_PUBLIC_URL`)
2026-03-06 12:41:58 -06:00
zvictor
55bb3012ea fix(auth): apply effective trusted origins and honor allowed hostnames in public mode 2026-03-06 15:39:36 -03:00
Victor Duarte
ca919d73f9 Merge branch 'master' into canonical-url 2026-03-06 19:32:29 +01:00
Dotta
70051735f6 Fix OpenClaw invite header normalization compatibility 2026-03-06 12:31:58 -06:00
Dotta
2ad616780f Include join requests in inbox badge and auto-refresh via push
The sidebar badge count was missing join requests from its inbox total,
and the live updates provider had no handler for join_request entity
type, so new join requests wouldn't appear until manual page refresh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:13:25 -06:00
Dotta
fa43e5b0dd Accept OpenClaw auth headers in more payload formats 2026-03-06 12:06:08 -06:00
Dotta
1d42b6e726 ci: add github actions verification workflow 2026-03-06 12:01:25 -06:00
Dotta
a3493dbb74 Allow OpenClaw invite reaccept to refresh join defaults 2026-03-06 11:59:13 -06:00
Ikko Ashimine
59a07324ec Add License 2026-03-07 02:57:28 +09:00
Dotta
4d8663ebc8 Merge remote-tracking branch 'public-gh/master'
* public-gh/master:
  Fix review feedback: duplicate wizard entry, command resolution, @types/node
  Fix server: remove DEFAULT_OPENCODE_LOCAL_MODEL from agents route
  Fix TS errors: remove DEFAULT_OPENCODE_LOCAL_MODEL references
  Regenerate pnpm-lock.yaml after PR #62 merge
  fix(onboard): preserve env-derived secrets defaults and report ignored exposure env in local_trusted mode
  fix: parseBooleanFromEnv silently treats common truthy values as false
  `onboard` now derives defaults from env vars before writing config
  Use precomputed runtime env in OpenCode execute
  Fix remaining OpenCode review comments
  Address PR feedback for OpenCode integration
  Add OpenCode provider integration and strict model selection
2026-03-06 11:56:42 -06:00
Dotta
2e7bf85e7a Fix 500 error logs still showing generic pino-http message
The previous fix (8151331) set res.err but pino-http wasn't picking it
up (likely Express 5 response object behavior). Switch to a custom
__errorContext property on the response that customErrorMessage and
customProps read directly, bypassing pino-http's unreliable res.err
check. Remove duplicate manual logger.error calls from the error
handler since pino-http now gets the full context.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:41:06 -06:00
Dotta
35e4897256 Merge pull request #141 from aaaaron/integrate-opencode-pr62
Integrate opencode pr62 & pr104
2026-03-06 11:32:03 -06:00
Dotta
68ee3f8ea0 Merge pull request #91 from zvictor/onboard-cherry
feat: Make `paperclipai onboard` Environment-Aware
2026-03-06 11:24:58 -06:00
Dotta
cf1ccd1e14 Assign invite-joined agents to company CEO 2026-03-06 11:22:24 -06:00
Dotta
f56901b473 Clarify OpenClaw claimed API key handling 2026-03-06 11:21:55 -06:00
Chris Schneider
f99f174e2d Show issue creator in properties sidebar 2026-03-06 17:16:39 +00:00
Dotta
cec372f9bb Fix phantom inbox badge count when failed runs exist
The server-side badge counted agent error alerts independently of failed
runs, but the UI suppresses agent error alerts when individual failed run
cards are already shown. This mismatch caused the badge to show e.g. 2
while only 1 item was visible. Align server logic with the client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:01:29 -06:00
Dotta
8355dd7905 Strengthen OpenClaw onboarding auth-token requirements 2026-03-06 11:00:44 -06:00
Dotta
8151331375 Enrich 500 error logs with request context and actual error messages
- pino-http customErrorMessage now includes the real error message
- customProps includes reqBody, reqParams, reqQuery, and routePath for 4xx/5xx
- Error handler logs full request context (body, params, query) for both
  HttpError 500s and unhandled errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:58:22 -06:00
Aaron
b06e41bed2 Fix review feedback: duplicate wizard entry, command resolution, @types/node
- Remove duplicate opencode_local adapter entry in OnboardingWizard
  (old Code-icon version), keeping only the OpenCodeLogoIcon entry
- Extract resolveOpenCodeCommand() helper to deduplicate the
  PAPERCLIP_OPENCODE_COMMAND env-var fallback logic in models.ts
- Bump @types/node from ^22.12.0 to ^24.6.0 to match the monorepo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 16:53:50 +00:00
Dotta
1179d7e75a Log redacted OpenClaw outbound payload details 2026-03-06 10:38:16 -06:00
Dotta
2ec2dcf9c6 Fix OpenClaw auth propagation and debug visibility 2026-03-06 10:14:57 -06:00
Dotta
cbce8bfbc3 Merge remote-tracking branch 'public-gh/master'
* public-gh/master:
  fix(ui): wrap failed run card actions on mobile
  feat(codex): add gpt-5.4 to codex_local model list
  persist paperclip data in a named volume
  add support to `cursor` and `opencode` in containerized instances
  force `@types/node@24` in the server
  Add artifact-check to fail fast on broken builds
  remove an insecure default auth secret
  expose `PAPERCLIP_ALLOWED_HOSTNAMES` in compose files
  wait for a health db
  update lock file
  update typing to node v24 from v20
  add missing `openclaw` adapter from deps stage
  fix incorrect pkg scope
  update docker base image
  move docker into `authenticated` deployment mode
2026-03-06 10:14:11 -06:00
Dotta
0f895a8cf9 Enforce 10-minute TTL for generated company invites 2026-03-06 10:10:23 -06:00
Dotta
c3ac209e5f Auto-copy agent snippet to clipboard on generation
When "Generate agent snippet" is clicked, the snippet is now
automatically copied to the clipboard and the "Copied" delight
animation is shown, matching the existing manual copy behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:00:34 -06:00
Dotta
192d76678e Prevent duplicate agent shortnames per company 2026-03-06 09:54:27 -06:00
Aaron
7bcf994064 Fix server: remove DEFAULT_OPENCODE_LOCAL_MODEL from agents route
Same issue as the UI fix — merge conflict resolution kept HEAD's
reference to the removed constant. OpenCode uses strict model
selection with no default.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:48:31 +00:00
Dotta
e670324334 Adjust recent issue sorting to ignore self-comment bumps 2026-03-06 09:46:08 -06:00
Dotta
c23ddbad3f Refine inbox ordering and alert-focused badges 2026-03-06 09:42:47 -06:00
Dotta
e6339e911d Fix OpenClaw invite accept config mapping and logging 2026-03-06 09:36:20 -06:00
Dotta
c0c64fe682 Refine touched issue unread indicators in inbox 2026-03-06 09:35:36 -06:00
Aaron
ae60879507 Fix TS errors: remove DEFAULT_OPENCODE_LOCAL_MODEL references
PR #62 uses strict model selection with no default — the merge
conflict resolution incorrectly kept HEAD's references to this
removed constant. Also remove dead opencode_local branch in
OnboardingWizard (already handled by prior condition).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:30:13 +00:00
Aaron
de60519ef6 Regenerate pnpm-lock.yaml after PR #62 merge
The lockfile was out of sync with the merged package.json files
(adapter-utils @types/node bumped to ^24.6.0 by PR #62).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:28:32 +00:00
Aaron
44a00596a4 Merge PR #62: Full OpenCode adapter integration
Merges paperclipai/paperclip#62 onto latest master (494448d).
Adds complete OpenCode provider with strict model selection,
dynamic model discovery, CLI/server/UI adapter registration.

Resolved conflicts with master's cursor adapter additions,
node v24 typing, and containerized opencode support (201d91b).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:23:55 +00:00
Dotta
a57732f7dd fix(ui): switch service worker to network-first to prevent stale content
The cache-first strategy for static assets was causing pages to serve
stale content on refresh. Switched to network-first for all requests
with cache as offline-only fallback. Bumped cache version to clear
existing caches on activate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:08:39 -06:00
Dotta
63c0e22a2a Handle unresolved agent shortnames without UUID errors 2026-03-06 09:06:55 -06:00
Dotta
2405851436 Normalize derived issue timestamps to avoid 500s 2026-03-06 09:03:27 -06:00
Dotta
d9d2ad209d prompt 2026-03-06 08:42:28 -06:00
zvictor
e1d4e37776 fix(onboard): preserve env-derived secrets defaults and report ignored exposure env in local_trusted mode 2026-03-06 11:41:11 -03:00
zvictor
08ac2bc9a7 fix: parseBooleanFromEnv silently treats common truthy values as false 2026-03-06 11:41:11 -03:00
Dotta
b213eb695b Add OpenClaw Paperclip API URL override for onboarding 2026-03-06 08:39:29 -06:00
Dotta
494448dcf7 Merge pull request #118 from MumuTW/fix/inbox-mobile-overflow-117
fix: wrap failed run card controls on mobile inbox
2026-03-06 08:37:42 -06:00
Dotta
854e818b74 Improve OpenClaw delta parsing and live stream coalescing 2026-03-06 08:34:51 -06:00
Dotta
38d3d5fa59 Persist issue read state and clear unread on open 2026-03-06 08:34:19 -06:00
Dotta
86bd26ee8a fix(ui): stabilize issue search URL sync and debounce query execution 2026-03-06 08:32:16 -06:00
zvictor
9cacf4a981 fix(onboard): preserve env-derived secrets defaults and report ignored exposure env in local_trusted mode 2026-03-06 11:29:28 -03:00
zvictor
9184cf92dd fix: parseBooleanFromEnv silently treats common truthy values as false 2026-03-06 11:28:31 -03:00
Dotta
38b9a55eab Add touched/unread inbox issue semantics 2026-03-06 08:21:03 -06:00
Dotta
3369a9e685 feat(openclaw): add adapter hire-approved hooks 2026-03-06 08:17:42 -06:00
Dotta
553c939f1f Merge pull request #67 from zvictor/master
epic: Fix and improve Docker deployments
2026-03-06 08:14:04 -06:00
Dotta
67bc601258 fix(ui): use history.replaceState for search URL sync to prevent page re-renders
setSearchParams from React Router triggers router state updates that cause
the Issues component tree to re-render on each debounced URL update. Switch
to window.history.replaceState which updates the URL silently without
triggering React Router re-renders.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:06:57 -06:00
Dotta
9d570b3ed7 fix(ui): enable scroll wheel in selectors inside dialogs
Radix Dialog wraps content in react-remove-scroll, which blocks wheel
events on portaled Popover content (rendered outside the Dialog DOM
tree). Add a disablePortal option to PopoverContent and use it for all
InlineEntitySelector instances inside NewIssueDialog so the Popover
stays in the Dialog's DOM tree and scrolling works.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:04:35 -06:00
Dotta
d4eb502389 feat(ui): unify comment assignee selector with icons and fix click flash
Add renderTriggerValue/renderOption to the comment thread's assignee
selector so it shows agent icons, matching the new issue dialog. Fix
the InlineEntitySelector flash on click by only auto-opening on
keyboard focus (not pointer-triggered focus).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:58:30 -06:00
Dotta
50276ed981 Fix OpenClaw wake env injection for OpenResponses input 2026-03-06 07:48:38 -06:00
Dotta
2d21045424 feat(ui): sync issues search with URL query parameter
Debounces search input (300ms) and syncs it to a ?q= URL parameter so
searches persist across navigation and can be shared via URL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:46:44 -06:00
Dotta
eb607f7df8 feat(ui): auto-hide sidebar scrollbar when not hovering
Replace scrollbar-none with a scrollbar-auto-hide utility that keeps the
scrollbar thumb transparent by default and reveals it on container hover.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:39:35 -06:00
Dotta
eb033a221f feat(ui): add dismiss buttons to inbox errors and failures
Failed runs, alerts, and stale work items can now be dismissed via an
X button that appears on hover. Dismissed items are stored in
localStorage and filtered from the inbox view and item count.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:27:35 -06:00
Victor Duarte
5f6e68e7aa Merge branch 'paperclipai:master' into master 2026-03-06 12:10:01 +01:00
MumuTW
88682632f9 fix(ui): wrap failed run card actions on mobile 2026-03-06 07:47:11 +00:00
MumuTW
264d40e6ca fix: use root option in sendFile to avoid dotfile 500 on SPA refresh
When served via npx, the absolute path to index.html traverses .npm,
triggering Express 5 / send's dotfile guard. Using the root option
makes send only check the relative filename for dotfiles.

Fixes #48
2026-03-06 07:15:15 +00:00
Dotta
de7d6294ea feat(ui): add scroll-to-bottom button on issues page
Shows a floating arrow button in the bottom-right corner when the user
is more than 300px from the bottom of the issues list. Hides
automatically when near the bottom. Smooth-scrolls on click.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:42:35 -06:00
Kevin Mok
666ab53648 Remove redundant opencode model assertion 2026-03-05 19:55:15 -06:00
Dotta
f41373dc46 feat(ui): make project status clickable in properties pane
Add a ProjectStatusPicker component that renders the status badge as a
clickable button opening a popover with all project statuses. Falls back
to the read-only StatusBadge when no onUpdate handler is provided.
Works in both desktop side panel and mobile sheet layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:00:23 -06:00
Kevin Mok
314288ff82 Add gpt-5.4 fallback and xhigh effort options 2026-03-05 18:59:42 -06:00
Dotta
1bbb98aaa9 fix(ui): add mobile properties toggle on project detail page
On mobile, the sidebar panel toggle was hidden (md:flex only). Add a
mobile-visible SlidersHorizontal button that opens a bottom Sheet drawer
with ProjectProperties, matching the existing pattern from IssueDetail.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 18:57:48 -06:00
Dotta
cecb94213d smoke: default openclaw docker ui state to temp folder 2026-03-05 18:53:07 -06:00
Dotta
0cdc9547d9 fix(ui): close sidebar on mobile when command palette opens
When searching via the command palette on mobile, the left sidebar
now automatically closes so search results are visible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 18:23:36 -06:00
Dotta
4c1504872f Merge pull request #110 from artokun/feat/codex-add-gpt-5.4-model
feat(codex): add gpt-5.4 to model list
2026-03-05 18:22:42 -06:00
Arthur R Longbottom
7086ad00ae feat(codex): add gpt-5.4 to codex_local model list
Add the newly released gpt-5.4 model to the codex_local adapter's
available models list.

にゃ~ 🐱
2026-03-05 16:13:29 -08:00
Dotta
222e0624a8 smoke: fully reset openclaw docker ui state by default 2026-03-05 17:38:06 -06:00
Dotta
81bc8c7313 Improve OpenClaw SSE transcript parsing and stream readability 2026-03-05 17:26:00 -06:00
Dotta
5134cac993 fix(ui): fix mobile popover issues in InlineEntitySelector
Force popover to always open downward (side="bottom") to prevent it from
flipping upward and going off-screen on mobile. Skip auto-focusing the
search input on touch devices so the virtual keyboard doesn't open and
reshape the viewport. Add touch-manipulation on option buttons to remove
tap delays and improve scroll gesture recognition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:23:01 -06:00
Dotta
e401979851 fix(ui): vertically center close button in command palette on mobile
The X close button in the command palette dialog used the generic dialog
positioning (absolute top-4 right-4), which was visually offset from the
search input on mobile. Replace with a custom close button that matches
the input wrapper height (h-12) and uses flex centering for proper
vertical alignment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:21:45 -06:00
Dotta
4569d57f5b fix(ui): improve mobile layout for assignee/project selectors in new issue dialog
Allow the "For [Assignee] in [Project]" row to wrap on mobile instead of
requiring horizontal scroll that pushes selectors off-screen. On desktop
(sm+), the row stays inline with min-w-max. Added overscroll-x-contain
for better touch scroll containment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:18:15 -06:00
Dotta
eff0c506fa fix(ui): hide assignee name in search results on mobile
On small screens, the assignee Identity badge takes up valuable space
in the command palette search results, truncating issue titles. Hide
it below the `sm` breakpoint so more of the title is visible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:18:01 -06:00
Dotta
c486bad2dd fix(ui): restore mobile touch scroll in popover dropdowns inside dialogs
Radix Dialog's modal DismissableLayer calls preventDefault() on pointerdown
events originating outside the Dialog DOM tree. Popover portals render at the
body level (outside the Dialog), so touch events on popover content were
treated as 'outside' — killing scroll gesture recognition on mobile.

Fix: add onPointerDownOutside to NewIssueDialog's DialogContent that detects
events from Radix popper wrappers and calls event.preventDefault() on the
Radix event (not the native event), which skips the Dialog's native
preventDefault and restores touch scrolling.

Also cleans up previous CSS-only workarounds (-webkit-overflow-scrolling,
touch-pan-y on individual buttons) that couldn't override JS preventDefault.
2026-03-05 17:04:25 -06:00
Dotta
0e387426fa feat(ui): add PWA configuration with service worker and enhanced manifest
Adds service worker with network-first navigation (SPA fallback) and
cache-first static assets. Enhances web manifest with start_url, scope,
id, description, orientation, and maskable icon entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:01:07 -06:00
Dotta
6ee4315eef feat(ui): add gateway config guidance to agent invite snippet
Add OpenResponses gateway enablement instructions to the end of the
agent snippet in CompanySettings. Refactor buildAgentSnippet to use
a template literal for easier future editing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 16:13:43 -06:00
Dotta
7c07b16f80 feat(release): add --canary and --promote flags to release.sh
Support two-phase canary release flow:
- --canary: publishes packages under @canary npm tag, skips git commit/tag
- --promote <version>: moves canary packages to @latest, then commits and tags

Both flags work with --dry-run. Existing behavior unchanged when neither flag is passed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 16:03:49 -06:00
Dotta
77500b50d9 docs(skills): add canary release flow to release coordination skill
New Steps 3-5 in the release skill:
- Step 3: Publish canary to npm with --tag canary (latest untouched)
- Step 4: Smoke test canary in Docker (uses existing smoke infrastructure)
- Step 5: Promote canary to latest after verification

Also updates idempotency table for new intermediate states (canary
published but not promoted) and adds release flow summary.

Script changes still needed: --canary flag for release.sh, --promote
command for dist-tag promotion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:57:23 -06:00
Dotta
0cc75c6e10 Cut over OpenClaw adapter to strict SSE streaming 2026-03-05 15:54:55 -06:00
zvictor
82d97418b2 set PAPERCLIP_PUBLIC_URL in compose files 2026-03-05 18:48:41 -03:00
Dotta
35a7acc058 fix(ui): add properties panel toggle to project detail page
When the properties pane was closed on a project page, there was no way
to reopen it. Add the same SlidersHorizontal toggle button used on issue
detail pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:25:19 -06:00
Dotta
bd32c871b7 fix(run-log-store): close fd leak in log append that caused spawn EBADF
The append method created a new WriteStream for every log chunk and resolved
the promise on the end callback (data flushed) rather than the close event
(fd released). Over many agent runs the leaked fds corrupted the fd table,
causing child_process.spawn to fail with EBADF.

Replace with fs.appendFile which properly opens, writes, and closes the fd
before resolving.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:23:25 -06:00
Dotta
8e63dd44b6 openclaw: accept webhook json ack in sse mode 2026-03-05 15:16:26 -06:00
zvictor
4eedf15870 persist paperclip data in a named volume 2026-03-05 18:11:50 -03:00
Dotta
a0e6ad0b7d openclaw: preserve run metadata in wake compatibility payload 2026-03-05 15:06:23 -06:00
Dotta
4b90784183 fix(openclaw): fallback to wake compatibility for /hooks/wake in sse mode 2026-03-05 14:56:49 -06:00
zvictor
ab6ec999c5 centralize URLs into single canonical URL var 2026-03-05 17:55:34 -03:00
Dotta
babea25649 feat(openclaw): add SSE-first transport and session routing 2026-03-05 14:28:59 -06:00
Dotta
e9ffde610b docs: add tailscale private access guide 2026-03-05 14:21:47 -06:00
Dotta
a05aa99c7e fix(openclaw): support /hooks/wake compatibility payload 2026-03-05 13:43:37 -06:00
Dotta
690149d555 Fix 500 error logging to show actual error instead of generic message
pino-http checks res.err before falling back to its generic
"failed with status code 500" error. Set res.err to the real error
in the error handler so logs include the actual message and stack trace.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:28:26 -06:00
zvictor
ffd1631b14 onboard now derives defaults from env vars before writing config 2026-03-05 16:17:26 -03:00
Dotta
185317c153 Simplify settings invite UI to snippet-only flow 2026-03-05 13:12:07 -06:00
Dotta
988f1244e5 Add invite callback-resolution test endpoint and snippet guidance 2026-03-05 13:05:04 -06:00
Dotta
38b855e495 Polish invite links and agent snippet UX 2026-03-05 12:52:39 -06:00
Dotta
0ed0c0abdb Add company settings invite fallback snippet 2026-03-05 12:37:56 -06:00
Dotta
7a2ecff4f0 Add invite onboarding network host suggestions 2026-03-05 12:28:27 -06:00
Dotta
bee24e880f Add Paperclip host networking guidance to OpenClaw smoke script 2026-03-05 12:22:14 -06:00
Dotta
7ab5b8a0c2 Add blue dot indicator to browser tab when issue has running runs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:20:43 -06:00
Dotta
089a2d08bf Add agent invite message flow and txt onboarding link UX 2026-03-05 12:10:01 -06:00
Dotta
d8fb93edcf Auto-copy invite link on creation and replace Copy button with inline icon
- Invite link is now automatically copied to clipboard when created
- "Copied" badge appears next to the share link header for 2s
- Standalone "Copy link" button replaced with a small copy icon next to the link
- Icon toggles to a green checkmark while "copied" feedback is shown

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:00:38 -06:00
zvictor
201d91b4f5 add support to cursor and opencode in containerized instances 2026-03-05 14:53:42 -03:00
Dotta
9da1803f29 docs: add user-facing changelog for v0.2.7
Generated via release-changelog skill. Covers onboarding
resilience improvements, Docker flow fixes, markdown rendering
fix, and embedded postgres dependency fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:44:38 -06:00
Dotta
1b98c2b279 Isolate OpenClaw smoke state to avoid stale auth drift 2026-03-05 11:42:50 -06:00
Dotta
a85511dad2 Add idempotency guards to release skills
- release-changelog: Step 0 checks for existing changelog file before
  generating. Asks reviewer to keep/regenerate/update. Never overwrites
  silently. Clarifies this skill never triggers version bumps.
- release: Step 0 idempotency table covering all steps. Tag check before
  npm publish prevents double-release. Task search before creation prevents
  duplicate follow-up tasks. Supports iterating on changelogs pre-publish.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:38:09 -06:00
zvictor
f75a4d9589 force @types/node@24 in the server 2026-03-05 14:37:48 -03:00
zvictor
0d36cf00f8 Add artifact-check to fail fast on broken builds 2026-03-05 14:36:00 -03:00
zvictor
4b8e880a96 remove an insecure default auth secret 2026-03-05 14:36:00 -03:00
zvictor
1e5e09f0fa expose PAPERCLIP_ALLOWED_HOSTNAMES in compose files 2026-03-05 14:36:00 -03:00
zvictor
57db28e9e6 wait for a health db 2026-03-05 14:36:00 -03:00
zvictor
c610951a71 update lock file 2026-03-05 14:36:00 -03:00
zvictor
e5049a448e update typing to node v24 from v20 2026-03-05 14:36:00 -03:00
zvictor
1f261d90f3 add missing openclaw adapter from deps stage 2026-03-05 14:36:00 -03:00
zvictor
d2dd8d0cc5 fix incorrect pkg scope 2026-03-05 14:36:00 -03:00
Victor Duarte
e08362b667 update docker base image 2026-03-05 14:36:00 -03:00
Victor Duarte
2c809d55c0 move docker into authenticated deployment mode 2026-03-05 14:36:00 -03:00
Dotta
529d53acc0 Pin OpenClaw Docker UI smoke defaults to OpenAI models 2026-03-05 11:29:29 -06:00
Dotta
fd73d6fcab docs(skills): add release coordination workflow 2026-03-05 11:23:58 -06:00
Dotta
cdf63d0024 Add release-changelog skill and releases/ directory
Introduces the release-changelog skill (skills/release-changelog/SKILL.md)
that teaches agents to generate user-facing changelogs from git history,
changesets, and merged PRs. Includes breaking change detection, categorization
heuristics, and structured markdown output template.

Also creates the releases/ directory convention for storing versioned
release notes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:21:44 -06:00
Dotta
09a8ecbded Sort assignee picker: recent selections first, then alphabetical
Tracks most recently selected assignees in localStorage and sorts both
the issue properties and new issue dialog assignee lists accordingly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:19:56 -06:00
Dotta
6f98c5f25c Clarify zero-flag OpenClaw Docker UI smoke defaults 2026-03-05 11:14:30 -06:00
Dotta
70e41150c5 Merge branch 'master' of github.com-dotta:paperclipai/paperclip
* 'master' of github.com-dotta:paperclipai/paperclip:
  fix: skip pending_approval agents in heartbeat tickTimers
2026-03-05 11:08:11 -06:00
Dotta
bc765b0867 Merge pull request #72 from STRML/fix/heartbeat-pending-approval
fix: skip pending_approval agents in heartbeat tickTimers
2026-03-05 11:06:35 -06:00
Dotta
9dbd72cffd Improve OpenClaw Docker UI smoke pairing ergonomics 2026-03-05 11:04:14 -06:00
Dotta
084c0a19a2 Merge branch 'master' of github.com-dotta:paperclipai/paperclip
* 'master' of github.com-dotta:paperclipai/paperclip:
  fix(ui): render sub-goals in goal detail tree
  fix: exclude terminated agents from list and org chart endpoints
2026-03-05 11:03:55 -06:00
Dotta
85f95c4542 Add permalink anchors to comments and GET comment-by-ID API
- Comment dates are now clickable anchor links (#comment-{id})
- Pages scroll to and highlight the target comment when URL has a hash
- Added GET /api/issues/:id/comments/:commentId endpoint
- Updated skill docs with new endpoint and comment URL format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:02:22 -06:00
Dotta
732ae4e46c feat(cursor): compact shell tool calls and format results in run log
Show only the command for shellToolCall/shell inputs instead of the
full payload. Format shell results with exit code + truncated
stdout/stderr sections for readability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 10:59:49 -06:00
Samuel Reed
c1a92d8520 fix: skip pending_approval agents in heartbeat tickTimers
Agents in pending_approval status are not invokable, but tickTimers only
skipped paused and terminated agents, causing enqueueWakeup to throw a
409 conflict error on every heartbeat tick for pending_approval agents.

Added pending_approval to the skip guard in tickTimers to match the
existing guard in enqueueWakeup.
2026-03-05 11:16:59 -05:00
Dotta
69b2875060 cursor adapter: use --yolo instead of --trust
The --yolo flag bypasses interactive prompts more broadly than --trust.
Updated execute, test probe, docs, and test expectations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 10:00:22 -06:00
Dotta
7cb46d97f6 Merge pull request #31 from tylerwince/codex/goal-detail-subgoals-render-fix
fix(ui): render sub-goals on goal detail pages
2026-03-05 09:48:52 -06:00
Dotta
e31d77bc47 Reset sessions for manual and timer heartbeat wakes 2026-03-05 09:48:11 -06:00
Dotta
90d39b9cbd Merge pull request #8 from numman-ali/fix/filter-terminated-agents-from-list-and-org
fix: exclude terminated agents from list and org chart endpoints
2026-03-05 09:46:38 -06:00
Dotta
bc68c3a504 cursor adapter: pipe prompts over stdin 2026-03-05 09:35:43 -06:00
Dotta
59bc52f527 cursor adapter: do not default to read-only ask mode 2026-03-05 09:27:20 -06:00
Dotta
d37e1d3dc3 preserve thinking delta whitespace in runlog streaming 2026-03-05 09:25:03 -06:00
Dotta
34d9122b45 Add one-command OpenClaw Docker UI smoke script 2026-03-05 09:15:46 -06:00
Dotta
1f7218640c cursor adapter: clarify paperclip env availability 2026-03-05 09:12:13 -06:00
Konan69
0078fa66a3 Use precomputed runtime env in OpenCode execute 2026-03-05 16:11:11 +01:00
Konan69
f4f9d6fd3f Fix remaining OpenCode review comments 2026-03-05 16:07:12 +01:00
Konan69
69c453b274 Address PR feedback for OpenCode integration 2026-03-05 15:52:59 +01:00
Dotta
d54ee6c4dc docs: add ClipHub marketplace spec
Product spec for ClipHub — a marketplace for Paperclip team
configurations, agent blueprints, skills, and governance templates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 08:36:09 -06:00
Dotta
b48f0314e7 fix(opencode): pretty-print JSON tool output in run transcript
Detect JSON-like tool output and format it with indentation instead of
displaying it as a single compressed line.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 08:36:04 -06:00
Dotta
e1b24c1d5c feat(cursor): export skill injection helper and document auto-behaviors
Export ensureCursorSkillsInjected from the server entrypoint and add
a test for skill directory injection. Document the auto-inject and
auto-trust behaviors in the adapter notes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 08:35:59 -06:00
Dotta
1c9b7ef918 coalesce cursor thinking deltas in run log streaming 2026-03-05 08:35:00 -06:00
Dotta
8f70e79240 cursor adapter: auto-pass trust flag for non-interactive runs 2026-03-05 08:28:12 -06:00
Dotta
eabfd9d9f6 expand cursor stream-json event parsing coverage 2026-03-05 08:25:41 -06:00
Konan69
6a101e0da1 Add OpenCode provider integration and strict model selection 2026-03-05 15:24:20 +01:00
Dotta
426c1044b6 fix cursor stream-json multiplexed output handling 2026-03-05 08:07:20 -06:00
Dotta
875924a7f3 Fix OpenCode default model and model-unavailable diagnostics 2026-03-05 08:02:39 -06:00
Dotta
e835c5cee9 Fix cursor model defaults and add dynamic model discovery 2026-03-05 07:52:23 -06:00
Dotta
db54f77b73 Add retry button to run detail page for failed/timed_out runs
The retry button was only on the Inbox page but missing from the individual
run detail view. This adds it next to the status badge, matching the same
wakeup pattern used in the Inbox (retry_failed_run with context snapshot).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 07:50:54 -06:00
Dotta
67eb5e5734 Limit session reset to assignment wakes only 2026-03-05 07:39:30 -06:00
Dotta
758a5538c5 Improve 500 error logging with actual error details and correct log levels
pino-http was logging 500s at INFO level with a generic "failed with status
code 500" message. Now 500s log at ERROR level and include the actual error
(message, stack, name) via res.locals handoff from the error handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 07:39:05 -06:00
Dotta
3ae112acff Improve OpenCode auth diagnostics for model lookup failures 2026-03-05 07:29:31 -06:00
Dotta
9454f76c0c Fix issue active-run fallback to match issue context 2026-03-05 07:27:48 -06:00
Dotta
944263f44b Reset sessions for comment-triggered wakeups 2026-03-05 07:10:24 -06:00
Dotta
a1944fceab feat: add star history section to README
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 06:58:16 -06:00
Dotta
8d5c9fde3b fix: use company-prefixed URLs in paperclip skill documentation
Update comment style guide and API reference to use /<prefix>/issues/...
format instead of /issues/... so agent-generated markdown links navigate
directly to the correct company-scoped route.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 06:56:47 -06:00
Dotta
d8688bbd93 fix: apply sorting to search results on issues page
Previously, sorting was skipped when a search query was active, so
search results were only ordered by backend relevance ranking.
Now client-side sorting applies to search results too.
Also changed default sort from "created" to "updated" desc so most
recently updated issues appear first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 06:55:33 -06:00
Dotta
306cd65353 Reset task session on issue assignment wakes 2026-03-05 06:54:36 -06:00
Dotta
8a85173150 feat: add cursor local adapter across server ui and cli 2026-03-05 06:31:22 -06:00
Dotta
b4a02ebc3f Improve workspace fallback logging and session resume migration 2026-03-05 06:14:32 -06:00
Dotta
1f57577c54 Add green 'Recommended' badge to Claude Code and Codex in onboarding wizard
Shows a green pill badge on the Claude Code and Codex adapter cards
during CEO agent configuration in the onboarding flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 06:04:42 -06:00
Dotta
ec0b7daca2 Add paperclipai db:backup CLI command 2026-03-05 06:02:12 -06:00
Tyler Wince
bdc0480e62 fix(ui): render sub-goals in goal detail tree 2026-03-04 20:20:21 -07:00
Dotta
c145074daf Add configurable automatic database backup scheduling 2026-03-04 18:03:23 -06:00
Dotta
f6a09bcbea feat: add opencode local adapter support 2026-03-04 16:48:54 -06:00
Dotta
d4a2fc6464 Improve OpenClaw smoke auth error handling 2026-03-04 16:37:55 -06:00
Dotta
be50daba42 Add OpenClaw onboarding text endpoint and join smoke harness 2026-03-04 16:29:14 -06:00
Numman Ali
7b334ff2b7 fix: exclude terminated agents from list and org chart endpoints
Terminated agents (e.g. from rejected hire approvals) were visible in
GET /companies/:companyId/agents and GET /companies/:companyId/org because
list() and orgForCompany() had no status filtering.

- Add ne(agents.status, "terminated") filter to both queries
- Add optional { includeTerminated: true } param to list() for callers
  that need all agents (e.g. company-portability export with skip counting)
- orgForCompany() always excludes terminated (no escape hatch needed)

Fixes #5
2026-03-04 22:28:22 +00:00
Dotta
5bbfddf70d Update README Roadmap section with detailed items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 14:59:08 -06:00
Dotta
358467a506 chore: release v0.2.7 2026-03-04 14:51:33 -06:00
Dotta
59507f18ec Allow onboarding to continue after failed env test 2026-03-04 14:46:29 -06:00
Dotta
b198b4a02c fix(server): require embedded-postgres for embedded DB mode 2026-03-04 14:46:03 -06:00
Dotta
5606f76ab4 Add onboarding retry action to unset ANTHROPIC_API_KEY 2026-03-04 14:40:12 -06:00
Dotta
0542f555ba Fix markdown list markers in editor and comment rendering 2026-03-04 12:20:29 -06:00
Dotta
18c9eb7b1e Filter out archived companies from new issue company selector
Companies with status "archived" are now hidden from the company
dropdown in the NewIssueDialog, matching the expected behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:20:14 -06:00
Dotta
675e0dcff1 docs: document issue search (q= param) in Paperclip skill
The API already supports full-text search via ?q= on the issues list
endpoint. Added documentation to both SKILL.md and the API reference
so agents know they can search issues by title, identifier,
description, and comments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 11:35:13 -06:00
Dotta
b66c6d017a Adjust docker onboard smoke defaults and console guidance 2026-03-04 10:48:36 -06:00
Dotta
bbf7490f32 Fix onboard smoke Docker flow for clean npx runs 2026-03-04 10:42:07 -06:00
Dotta
5dffdbb382 chore: release v0.2.6 2026-03-04 10:24:03 -06:00
Dotta
ea637110ac Add Ubuntu onboard smoke flow and lazy-load auth startup 2026-03-04 10:15:11 -06:00
Dotta
3ae9d95354 fix: stabilize paperclipai run server import errors 2026-03-04 10:02:23 -06:00
Dotta
a95e38485d fix: doctor command auto-creates directories and treats LLM as optional
Instead of showing alarming warnings on first run when storage, log,
and database directories don't exist, the doctor checks now silently
create them and report pass. LLM provider is treated as optional
rather than a warning when not configured.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 09:58:30 -06:00
Dotta
c7c96feef7 docs: simplify quickstart to npx onboard, mention create-adapter skill
- Remove Docker option from quickstart, make `npx paperclipai onboard --yes` the recommended path
- Add tip about `create-agent-adapter` skill in the creating-an-adapter guide

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 08:11:06 -06:00
Dotta
7e387a1883 docs: update Discord invite link to https://discord.gg/m4HZY7xNG3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 07:54:55 -06:00
Dotta
108bb9bd15 docs: update quickstart CTA to npx paperclipai onboard --yes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:47:33 -06:00
Dotta
6141d5c3f2 chore: release v0.2.5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:14:33 -06:00
Dotta
a4da932d8d fix: remove stale server/ui-dist from git add in release.sh
The release script cleaned up temporary build artifacts (ui-dist, skills)
before staging files for the commit, then tried to git add server/ui-dist
which no longer existed.  Remove it from the git add list since these
artifacts should never be committed — they're only needed during publish.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:14:28 -06:00
Dotta
ab3b9ab19f chore: bump all packages to 0.2.4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:10:52 -06:00
Dotta
f4a5b00116 fix: bundle skills directory into npm packages for runtime discovery
The claude-local, codex-local adapters and the server all resolve a
skills/ directory using __dirname-relative paths that only work inside
the monorepo.  When installed from npm the paths point outside the
package and cause ENOENT on readdir/readFile.

- Update both adapter execute.ts files to try a published-path
  candidate (../../skills from dist/) before falling back to the
  monorepo dev path (../../../../../skills from src/).
- Update server readSkillMarkdown() to try the published path first.
- Add "skills" to the files array in server, claude-local, and
  codex-local package.json so npm includes them.
- Update release.sh to copy the repo-root skills/ into each package
  before publish, and clean up after.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:06:12 -06:00
Dotta
09d2ef1a37 fix: restore docs deleted in v0.2.3 release, add Paperclip branding
- Restored docs/ directory that was accidentally deleted by `git add -A`
  in the v0.2.3 release script
- Replaced generic "P" favicon with actual paperclip icon using brand
  primary color (#2563EB)
- Added light/dark logo SVGs for Mintlify navbar (paperclip icon + wordmark)
- Updated docs.json with logo configuration for dark/light mode
- Fixed release.sh to stage only release-related files instead of `git add -A`
  to prevent sweeping unrelated changes into release commits

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:49:43 -06:00
Dotta
d18312d6de fix: bundle UI dist into server package for npm publishing
The server's static-ui mode resolves the UI dist path relative to its
own directory. In the monorepo it finds ../../ui/dist, but when published
to npm the UI package isn't available.

- server/src/app.ts: try ../ui-dist (published) then ../../ui/dist (dev),
  gracefully degrade to API-only if neither exists
- server/package.json: include ui-dist/ in published files
- scripts/release.sh: build UI and copy dist to server/ui-dist before
  publishing, clean up in restore step

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:45:45 -06:00
Dotta
28bf5e9e9b chore: release v0.2.3 2026-03-03 15:39:13 -06:00
Dotta
925680f736 fix: include migration files in @paperclipai/db and improve server error msg
- db build now copies src/migrations/ to dist/migrations/ after tsc,
  so SQL + meta JSON files are included in the published package.
  Without this, `import("@paperclipai/server")` fails at runtime with
  ENOENT when scanning for migration files.

- CLI's importServerEntry() now distinguishes between "module not found"
  and "server crashed during startup" for clearer error messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:30:50 -06:00
Dotta
4e9c1d83be Fix domain: paperclip.dev -> paperclip.ing
Updated all references across README and doc plans.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:25:19 -06:00
Dotta
b2737b9571 chore: release v0.2.2 2026-03-03 15:10:25 -06:00
1253 changed files with 528499 additions and 10999 deletions

View File

@@ -0,0 +1,269 @@
---
name: company-creator
description: >
Create agent company packages conforming to the Agent Companies specification
(agentcompanies/v1). Use when a user wants to create a new agent company from
scratch, build a company around an existing git repo or skills collection, or
scaffold a team/department of agents. Triggers on: "create a company", "make me
a company", "build a company from this repo", "set up an agent company",
"create a team of agents", "hire some agents", or when given a repo URL and
asked to turn it into a company. Do NOT use for importing an existing company
package (use the CLI import command instead) or for modifying a company that
is already running in Paperclip.
---
# Company Creator
Create agent company packages that conform to the Agent Companies specification.
Spec references:
- Normative spec: `docs/companies/companies-spec.md` (read this before generating files)
- Web spec: https://agentcompanies.io/specification
- Protocol site: https://agentcompanies.io/
## Two Modes
### Mode 1: Company From Scratch
The user describes what they want. Interview them to flesh out the vision, then generate the package.
### Mode 2: Company From a Repo
The user provides a git repo URL, local path, or tweet. Analyze the repo, then create a company that wraps it.
See [references/from-repo-guide.md](references/from-repo-guide.md) for detailed repo analysis steps.
## Process
### Step 1: Gather Context
Determine which mode applies:
- **From scratch**: What kind of company or team? What domain? What should the agents do?
- **From repo**: Clone/read the repo. Scan for existing skills, agent configs, README, source structure.
### Step 2: Interview (Use AskUserQuestion)
Do not skip this step. Use AskUserQuestion to align with the user before writing any files.
**For from-scratch companies**, ask about:
- Company purpose and domain (1-2 sentences is fine)
- What agents they need - propose a hiring plan based on what they described
- Whether this is a full company (needs a CEO) or a team/department (no CEO required)
- Any specific skills the agents should have
- How work flows through the organization (see "Workflow" below)
- Whether they want projects and starter tasks
**For from-repo companies**, present your analysis and ask:
- Confirm the agents you plan to create and their roles
- Whether to reference or vendor any discovered skills (default: reference)
- Any additional agents or skills beyond what the repo provides
- Company name and any customization
- Confirm the workflow you inferred from the repo (see "Workflow" below)
**Workflow — how does work move through this company?**
A company is not just a list of agents with skills. It's an organization that takes ideas and turns them into work products. You need to understand the workflow so each agent knows:
- Who gives them work and in what form (a task, a branch, a question, a review request)
- What they do with it
- Who they hand off to when they're done, and what that handoff looks like
- What "done" means for their role
**Not every company is a pipeline.** Infer the right workflow pattern from context:
- **Pipeline** — sequential stages, each agent hands off to the next. Use when the repo/domain has a clear linear process (e.g. plan → build → review → ship → QA, or content ideation → draft → edit → publish).
- **Hub-and-spoke** — a manager delegates to specialists who report back independently. Use when agents do different kinds of work that don't feed into each other (e.g. a CEO who dispatches to a researcher, a marketer, and an analyst).
- **Collaborative** — agents work together on the same things as peers. Use for small teams where everyone contributes to the same output (e.g. a design studio, a brainstorming team).
- **On-demand** — agents are summoned as needed with no fixed flow. Use when agents are more like a toolbox of specialists the user calls directly.
For from-scratch companies, propose a workflow pattern based on what they described and ask if it fits.
For from-repo companies, infer the pattern from the repo's structure. If skills have a clear sequential dependency (like `plan-ceo-review → plan-eng-review → review → ship → qa`), that's a pipeline. If skills are independent capabilities, it's more likely hub-and-spoke or on-demand. State your inference in the interview so the user can confirm or adjust.
**Key interviewing principles:**
- Propose a concrete hiring plan. Don't ask open-ended "what agents do you want?" - suggest specific agents based on context and let the user adjust.
- Keep it lean. Most users are new to agent companies. A few agents (3-5) is typical for a startup. Don't suggest 10+ agents unless the scope demands it.
- From-scratch companies should start with a CEO who manages everyone. Teams/departments don't need one.
- Ask 2-3 focused questions per round, not 10.
### Step 3: Read the Spec
Before generating any files, read the normative spec:
```
docs/companies/companies-spec.md
```
Also read the quick reference: [references/companies-spec.md](references/companies-spec.md)
And the example: [references/example-company.md](references/example-company.md)
### Step 4: Generate the Package
Create the directory structure and all files. Follow the spec's conventions exactly.
**Directory structure:**
```
<company-slug>/
├── COMPANY.md
├── agents/
│ └── <slug>/AGENTS.md
├── teams/
│ └── <slug>/TEAM.md (if teams are needed)
├── projects/
│ └── <slug>/PROJECT.md (if projects are needed)
├── tasks/
│ └── <slug>/TASK.md (if tasks are needed)
├── skills/
│ └── <slug>/SKILL.md (if custom skills are needed)
└── .paperclip.yaml (Paperclip vendor extension)
```
**Rules:**
- Slugs must be URL-safe, lowercase, hyphenated
- COMPANY.md gets `schema: agentcompanies/v1` - other files inherit it
- Agent instructions go in the AGENTS.md body, not in .paperclip.yaml
- Skills referenced by shortname in AGENTS.md resolve to `skills/<shortname>/SKILL.md`
- For external skills, use `sources` with `usage: referenced` (see spec section 12)
- Do not export secrets, machine-local paths, or database IDs
- Omit empty/default fields
- For companies generated from a repo, add a references footer at the bottom of COMPANY.md body:
`Generated from [repo-name](repo-url) with the company-creator skill from [Paperclip](https://github.com/paperclipai/paperclip)`
**Reporting structure:**
- Every agent except the CEO should have `reportsTo` set to their manager's slug
- The CEO has `reportsTo: null`
- For teams without a CEO, the top-level agent has `reportsTo: null`
**Writing workflow-aware agent instructions:**
Each AGENTS.md body should include not just what the agent does, but how they fit into the organization's workflow. Include:
1. **Where work comes from** — "You receive feature ideas from the user" or "You pick up tasks assigned to you by the CTO"
2. **What you produce** — "You produce a technical plan with architecture diagrams" or "You produce a reviewed, approved branch ready for shipping"
3. **Who you hand off to** — "When your plan is locked, hand off to the Staff Engineer for implementation" or "When review passes, hand off to the Release Engineer to ship"
4. **What triggers you** — "You are activated when a new feature idea needs product-level thinking" or "You are activated when a branch is ready for pre-landing review"
This turns a collection of agents into an organization that actually works together. Without workflow context, agents operate in isolation — they do their job but don't know what happens before or after them.
### Step 5: Confirm Output Location
Ask the user where to write the package. Common options:
- A subdirectory in the current repo
- A new directory the user specifies
- The current directory (if it's empty or they confirm)
### Step 6: Write README.md and LICENSE
**README.md** — every company package gets a README. It should be a nice, readable introduction that someone browsing GitHub would appreciate. Include:
- Company name and what it does
- The workflow / how the company operates
- Org chart as a markdown list or table showing agents, titles, reporting structure, and skills
- Brief description of each agent's role
- Citations and references: link to the source repo (if from-repo), link to the Agent Companies spec (https://agentcompanies.io/specification), and link to Paperclip (https://github.com/paperclipai/paperclip)
- A "Getting Started" section explaining how to import: `paperclipai company import --from <path>`
**LICENSE** — include a LICENSE file. The copyright holder is the user creating the company, not the upstream repo author (they made the skills, the user is making the company). Use the same license type as the source repo (if from-repo) or ask the user (if from-scratch). Default to MIT if unclear.
### Step 7: Write Files and Summarize
Write all files, then give a brief summary:
- Company name and what it does
- Agent roster with roles and reporting structure
- Skills (custom + referenced)
- Projects and tasks if any
- The output path
## .paperclip.yaml Guidelines
The `.paperclip.yaml` file is the Paperclip vendor extension. It configures adapters and env inputs per agent.
### Adapter Rules
**Do not specify an adapter unless the repo or user context warrants it.** If you don't know what adapter the user wants, omit the adapter block entirely — Paperclip will use its default. Specifying an unknown adapter type causes an import error.
Paperclip's supported adapter types (these are the ONLY valid values):
- `claude_local` — Claude Code CLI
- `codex_local` — Codex CLI
- `opencode_local` — OpenCode CLI
- `pi_local` — Pi CLI
- `cursor` — Cursor
- `gemini_local` — Gemini CLI
- `openclaw_gateway` — OpenClaw gateway
Only set an adapter when:
- The repo or its skills clearly target a specific runtime (e.g. gstack is built for Claude Code, so `claude_local` is appropriate)
- The user explicitly requests a specific adapter
- The agent's role requires a specific runtime capability
### Env Inputs Rules
**Do not add boilerplate env variables.** Only add env inputs that the agent actually needs based on its skills or role:
- `GH_TOKEN` for agents that push code, create PRs, or interact with GitHub
- API keys only when a skill explicitly requires them
- Never set `ANTHROPIC_API_KEY` as a default empty env variable — the runtime handles this
Example with adapter (only when warranted):
```yaml
schema: paperclip/v1
agents:
release-engineer:
adapter:
type: claude_local
config:
model: claude-sonnet-4-6
inputs:
env:
GH_TOKEN:
kind: secret
requirement: optional
```
Example — only agents with actual overrides appear:
```yaml
schema: paperclip/v1
agents:
release-engineer:
inputs:
env:
GH_TOKEN:
kind: secret
requirement: optional
```
In this example, only `release-engineer` appears because it needs `GH_TOKEN`. The other agents (ceo, cto, etc.) have no overrides, so they are omitted entirely from `.paperclip.yaml`.
## External Skill References
When referencing skills from a GitHub repo, always use the references pattern:
```yaml
metadata:
sources:
- kind: github-file
repo: owner/repo
path: path/to/SKILL.md
commit: <full SHA from git ls-remote or the repo>
attribution: Owner or Org Name
license: <from the repo's LICENSE>
usage: referenced
```
Get the commit SHA with:
```bash
git ls-remote https://github.com/owner/repo HEAD
```
Do NOT copy external skill content into the package unless the user explicitly asks.

View File

@@ -0,0 +1,144 @@
# Agent Companies Specification Reference
The normative specification lives at:
- Web: https://agentcompanies.io/specification
- Local: docs/companies/companies-spec.md
Read the local spec file before generating any package files. The spec defines the canonical format and all frontmatter fields. Below is a quick-reference summary for common authoring tasks.
## Package Kinds
| File | Kind | Purpose |
| ---------- | ------- | ------------------------------------------------- |
| COMPANY.md | company | Root entrypoint, org boundary and defaults |
| TEAM.md | team | Reusable org subtree |
| AGENTS.md | agent | One role, instructions, and attached skills |
| PROJECT.md | project | Planned work grouping |
| TASK.md | task | Portable starter task |
| SKILL.md | skill | Agent Skills capability package (do not redefine) |
## Directory Layout
```
company-package/
├── COMPANY.md
├── agents/
│ └── <slug>/AGENTS.md
├── teams/
│ └── <slug>/TEAM.md
├── projects/
│ └── <slug>/
│ ├── PROJECT.md
│ └── tasks/
│ └── <slug>/TASK.md
├── tasks/
│ └── <slug>/TASK.md
├── skills/
│ └── <slug>/SKILL.md
├── assets/
├── scripts/
├── references/
└── .paperclip.yaml (optional vendor extension)
```
## Common Frontmatter Fields
```yaml
schema: agentcompanies/v1
kind: company | team | agent | project | task
slug: url-safe-stable-identity
name: Human Readable Name
description: Short description for discovery
version: 0.1.0
license: MIT
authors:
- name: Jane Doe
tags: []
metadata: {}
sources: []
```
- `schema` usually appears only at package root
- `kind` is optional when filename makes it obvious
- `slug` must be URL-safe and stable
- exporters should omit empty or default-valued fields
## COMPANY.md Required Fields
```yaml
name: Company Name
description: What this company does
slug: company-slug
schema: agentcompanies/v1
```
Optional: `version`, `license`, `authors`, `goals`, `includes`, `requirements.secrets`
## AGENTS.md Key Fields
```yaml
name: Agent Name
title: Role Title
reportsTo: <agent-slug or null>
skills:
- skill-shortname
```
- Body content is the agent's default instructions
- Skills resolve by shortname: `skills/<shortname>/SKILL.md`
- Do not export machine-specific paths or secrets
## TEAM.md Key Fields
```yaml
name: Team Name
description: What this team does
slug: team-slug
manager: ../agent-slug/AGENTS.md
includes:
- ../agent-slug/AGENTS.md
- ../../skills/skill-slug/SKILL.md
```
## PROJECT.md Key Fields
```yaml
name: Project Name
description: What this project delivers
owner: agent-slug
```
## TASK.md Key Fields
```yaml
name: Task Name
assignee: agent-slug
project: project-slug
schedule:
timezone: America/Chicago
startsAt: 2026-03-16T09:00:00-05:00
recurrence:
frequency: weekly
interval: 1
weekdays: [monday]
time: { hour: 9, minute: 0 }
```
## Source References (for external skills/content)
```yaml
sources:
- kind: github-file
repo: owner/repo
path: path/to/SKILL.md
commit: <full-sha>
sha256: <hash>
attribution: Owner Name
license: MIT
usage: referenced
```
Usage modes: `vendored` (bytes included), `referenced` (pointer only), `mirrored` (cached locally)
Default to `referenced` for third-party content.

View File

@@ -0,0 +1,184 @@
# Example Company Package
A minimal but complete example of an agent company package.
## Directory Structure
```
lean-dev-shop/
├── COMPANY.md
├── agents/
│ ├── ceo/AGENTS.md
│ ├── cto/AGENTS.md
│ └── engineer/AGENTS.md
├── teams/
│ └── engineering/TEAM.md
├── projects/
│ └── q2-launch/
│ ├── PROJECT.md
│ └── tasks/
│ └── monday-review/TASK.md
├── tasks/
│ └── weekly-standup/TASK.md
├── skills/
│ └── code-review/SKILL.md
└── .paperclip.yaml
```
## COMPANY.md
```markdown
---
name: Lean Dev Shop
description: Small engineering-focused AI company that builds and ships software products
slug: lean-dev-shop
schema: agentcompanies/v1
version: 1.0.0
license: MIT
authors:
- name: Example Org
goals:
- Build and ship software products
- Maintain high code quality
---
Lean Dev Shop is a small, focused engineering company. The CEO oversees strategy and coordinates work. The CTO leads the engineering team. Engineers build and ship code.
```
## agents/ceo/AGENTS.md
```markdown
---
name: CEO
title: Chief Executive Officer
reportsTo: null
skills:
- paperclip
---
You are the CEO of Lean Dev Shop. You oversee company strategy, coordinate work across the team, and ensure projects ship on time.
Your responsibilities:
- Review and prioritize work across projects
- Coordinate with the CTO on technical decisions
- Ensure the company goals are being met
```
## agents/cto/AGENTS.md
```markdown
---
name: CTO
title: Chief Technology Officer
reportsTo: ceo
skills:
- code-review
- paperclip
---
You are the CTO of Lean Dev Shop. You lead the engineering team and make technical decisions.
Your responsibilities:
- Set technical direction and architecture
- Review code and ensure quality standards
- Mentor engineers and unblock technical challenges
```
## agents/engineer/AGENTS.md
```markdown
---
name: Engineer
title: Software Engineer
reportsTo: cto
skills:
- code-review
- paperclip
---
You are a software engineer at Lean Dev Shop. You write code, fix bugs, and ship features.
Your responsibilities:
- Implement features and fix bugs
- Write tests and documentation
- Participate in code reviews
```
## teams/engineering/TEAM.md
```markdown
---
name: Engineering
description: Product and platform engineering team
slug: engineering
schema: agentcompanies/v1
manager: ../../agents/cto/AGENTS.md
includes:
- ../../agents/engineer/AGENTS.md
- ../../skills/code-review/SKILL.md
tags:
- engineering
---
The engineering team builds and maintains all software products.
```
## projects/q2-launch/PROJECT.md
```markdown
---
name: Q2 Launch
description: Ship the Q2 product launch
slug: q2-launch
owner: cto
---
Deliver all features planned for the Q2 launch, including the new dashboard and API improvements.
```
## projects/q2-launch/tasks/monday-review/TASK.md
```markdown
---
name: Monday Review
assignee: ceo
project: q2-launch
schedule:
timezone: America/Chicago
startsAt: 2026-03-16T09:00:00-05:00
recurrence:
frequency: weekly
interval: 1
weekdays:
- monday
time:
hour: 9
minute: 0
---
Review the status of Q2 Launch project. Check progress on all open tasks, identify blockers, and update priorities for the week.
```
## skills/code-review/SKILL.md (with external reference)
```markdown
---
name: code-review
description: Thorough code review skill for pull requests and diffs
metadata:
sources:
- kind: github-file
repo: anthropics/claude-code
path: skills/code-review/SKILL.md
commit: abc123def456
sha256: 3b7e...9a
attribution: Anthropic
license: MIT
usage: referenced
---
Review code changes for correctness, style, and potential issues.
```

View File

@@ -0,0 +1,79 @@
# Creating a Company From an Existing Repository
When a user provides a git repo (URL, local path, or tweet linking to a repo), analyze it and create a company package that wraps its content.
## Analysis Steps
1. **Clone or read the repo** - Use `git clone` for URLs, read directly for local paths
2. **Scan for existing agent/skill files** - Look for SKILL.md, AGENTS.md, CLAUDE.md, .claude/ directories, or similar agent configuration
3. **Understand the repo's purpose** - Read README, package.json, main source files to understand what the project does
4. **Identify natural agent roles** - Based on the repo's structure and purpose, determine what agents would be useful
## Handling Existing Skills
Many repos already contain skills (SKILL.md files). When you find them:
**Default behavior: use references, not copies.**
Instead of copying skill content into your company package, create a source reference:
```yaml
metadata:
sources:
- kind: github-file
repo: owner/repo
path: path/to/SKILL.md
commit: <get the current HEAD commit SHA>
attribution: <repo owner or org name>
license: <from repo's LICENSE file>
usage: referenced
```
To get the commit SHA:
```bash
git ls-remote https://github.com/owner/repo HEAD
```
Only vendor (copy) skills when:
- The user explicitly asks to copy them
- The skill is very small and tightly coupled to the company
- The source repo is private or may become unavailable
## Handling Existing Agent Configurations
If the repo has agent configs (CLAUDE.md, .claude/ directories, codex configs, etc.):
- Use them as inspiration for AGENTS.md instructions
- Don't copy them verbatim - adapt them to the Agent Companies format
- Preserve the intent and key instructions
## Repo-Only Skills (No Agents)
When a repo contains only skills and no agents:
- Create agents that would naturally use those skills
- The agents should be minimal - just enough to give the skills a runtime context
- A single agent may use multiple skills from the repo
- Name agents based on the domain the skills cover
Example: A repo with `code-review`, `testing`, and `deployment` skills might become:
- A "Lead Engineer" agent with all three skills
- Or separate "Reviewer", "QA Engineer", and "DevOps" agents if the skills are distinct enough
## Common Repo Patterns
### Developer Tools / CLI repos
- Create agents for the tool's primary use cases
- Reference any existing skills
- Add a project maintainer or lead agent
### Library / Framework repos
- Create agents for development, testing, documentation
- Skills from the repo become agent capabilities
### Full Application repos
- Map to departments: engineering, product, QA
- Create a lean team structure appropriate to the project size
### Skills Collection repos (e.g. skills.sh repos)
- Each skill or skill group gets an agent
- Create a lightweight company or team wrapper
- Keep the agent count proportional to the skill diversity

View File

@@ -0,0 +1,230 @@
---
name: deal-with-security-advisory
description: >
Handle a GitHub Security Advisory response for Paperclip, including
confidential fix development in a temporary private fork, human coordination
on advisory-thread comments, CVE request, synchronized advisory publication,
and immediate security release steps.
---
# Security Vulnerability Response Instructions
## ⚠️ CRITICAL: This is a security vulnerability. Everything about this process is confidential until the advisory is published. Do not mention the vulnerability details in any public commit message, PR title, branch name, or comment. Do not push anything to a public branch. Do not discuss specifics in any public channel. Assume anything on the public repo is visible to attackers who will exploit the window between disclosure and user upgrades.
***
## Context
A security vulnerability has been reported via GitHub Security Advisory:
* **Advisory:** {{ghsaId}} (e.g. GHSA-x8hx-rhr2-9rf7)
* **Reporter:** {{reporterHandle}}
* **Severity:** {{severity}}
* **Notes:** {{notes}}
***
## Step 0: Fetch the Advisory Details
Pull the full advisory so you understand the vulnerability before doing anything else:
```
gh api repos/paperclipai/paperclip/security-advisories/{{ghsaId}}
```
Read the `description`, `severity`, `cvss`, and `vulnerabilities` fields. Understand the attack vector before writing code.
## Step 1: Acknowledge the Report
⚠️ **This step requires a human.** The advisory thread does not have a comment API. Ask the human operator to post a comment on the private advisory thread acknowledging the report. Provide them this template:
> Thanks for the report, @{{reporterHandle}}. We've confirmed the issue and are working on a fix. We're targeting a patch release within {{timeframe}}. We'll keep you updated here.
Give your human this template, but still continue
Below we use `gh` tools - you do have access and credentials outside of your sandbox, so use them.
## Step 2: Create the Temporary Private Fork
This is where all fix development happens. Never push to the public repo.
```
gh api --method POST \
repos/paperclipai/paperclip/security-advisories/{{ghsaId}}/forks
```
This returns a repository object for the private fork. Save the `full_name` and `clone_url`.
Clone it and set up your workspace:
```
# Clone the private fork somewhere outside ~/paperclip
git clone <clone_url_from_response> ~/security-patch-{{ghsaId}}
cd ~/security-patch-{{ghsaId}}
git checkout -b security-fix
```
**Do not edit `~/paperclip`** — the dev server is running off the `~/paperclip` master branch and we don't want to touch it. All work happens in the private fork clone.
**TIPS:**
* Do not commit `pnpm-lock.yaml` — the repo has actions to manage this
* Do not use descriptive branch names that leak the vulnerability (e.g., no `fix-dns-rebinding-rce`). Use something generic like `security-fix`
* All work stays in the private fork until publication
* CI/GitHub Actions will NOT run on the temporary private fork — this is a GitHub limitation by design. You must run tests locally
## Step 3: Develop and Validate the Fix
Write the patch. Same content standards as any PR:
* It must functionally work — **run tests locally** since CI won't run on the private fork
* Consider the whole codebase, not just the narrow vulnerability path. A patch that fixes one vector but opens another is worse than no patch
* Ensure backwards compatibility for the database, or be explicit about what breaks
* Make sure any UI components still look correct if the fix touches them
* The fix should be minimal and focused — don't bundle unrelated changes into a security patch. Reviewers (and the reporter) should be able to read the diff and understand exactly what changed and why
**Specific to security fixes:**
* Verify the fix actually closes the attack vector described in the advisory. Reproduce the vulnerability first (using the reporter's description), then confirm the patch prevents it
* Consider adjacent attack vectors — if DNS rebinding is the issue, are there other endpoints or modes with the same class of problem?
* Do not introduce new dependencies unless absolutely necessary — new deps in a security patch raise eyebrows
Push your fix to the private fork:
```
git add -A
git commit -m "Fix security vulnerability"
git push origin security-fix
```
## Step 4: Coordinate with the Reporter
⚠️ **This step requires a human.** Ask the human operator to post on the advisory thread letting the reporter know the fix is ready and giving them a chance to review. Provide them this template:
> @{{reporterHandle}} — fix is ready in the private fork if you'd like to review before we publish. Planning to release within {{timeframe}}.
Proceed
## Step 5: Request a CVE
This makes vulnerability scanners (npm audit, Snyk, Dependabot) warn users to upgrade. Without it, nobody gets automated notification.
```
gh api --method POST \
repos/paperclipai/paperclip/security-advisories/{{ghsaId}}/cve
```
GitHub is a CVE Numbering Authority and will assign one automatically. The CVE may take a few hours to propagate after the advisory is published.
## Step 6: Publish Everything Simultaneously
This all happens at once — do not stagger these steps. The goal is **zero window** between the vulnerability becoming public knowledge and the fix being available.
### 6a. Verify reporter credit before publishing
```
gh api repos/paperclipai/paperclip/security-advisories/{{ghsaId}} --jq '.credits'
```
If the reporter is not credited, add them:
```
gh api --method PATCH \
repos/paperclipai/paperclip/security-advisories/{{ghsaId}} \
--input - << 'EOF'
{
"credits": [
{
"login": "{{reporterHandle}}",
"type": "reporter"
}
]
}
EOF
```
### 6b. Update the advisory with the patched version and publish
```
gh api --method PATCH \
repos/paperclipai/paperclip/security-advisories/{{ghsaId}} \
--input - << 'EOF'
{
"state": "published",
"vulnerabilities": [
{
"package": {
"ecosystem": "npm",
"name": "paperclip"
},
"vulnerable_version_range": "< {{patchedVersion}}",
"patched_versions": "{{patchedVersion}}"
}
]
}
EOF
```
Publishing the advisory simultaneously:
* Makes the GHSA public
* Merges the temporary private fork into your repo
* Triggers the CVE assignment (if requested in step 5)
### 6c. Cut a release immediately after merge
```
cd ~/paperclip
git pull origin master
gh release create v{{patchedVersion}} \
--repo paperclipai/paperclip \
--title "v{{patchedVersion}} — Security Release" \
--notes "## Security Release
This release fixes a critical security vulnerability.
### What was fixed
{{briefDescription}} (e.g., Remote code execution via DNS rebinding in \`local_trusted\` mode)
### Advisory
https://github.com/paperclipai/paperclip/security/advisories/{{ghsaId}}
### Credit
Thanks to @{{reporterHandle}} for responsibly disclosing this vulnerability.
### Action required
All users running versions prior to {{patchedVersion}} should upgrade immediately."
```
## Step 7: Post-Publication Verification
```
# Verify the advisory is published and CVE is assigned
gh api repos/paperclipai/paperclip/security-advisories/{{ghsaId}} \
--jq '{state: .state, cve_id: .cve_id, published_at: .published_at}'
# Verify the release exists
gh release view v{{patchedVersion}} --repo paperclipai/paperclip
```
If the CVE hasn't been assigned yet, that's normal — it can take a few hours.
⚠️ **Human step:** Ask the human operator to post a final comment on the advisory thread confirming publication and thanking the reporter.
Tell the human operator what you did by posting a comment to this task, including:
* The published advisory URL: `https://github.com/paperclipai/paperclip/security/advisories/{{ghsaId}}`
* The release URL
* Whether the CVE has been assigned yet
* All URLs to any pull requests or branches

View File

@@ -0,0 +1,201 @@
---
name: doc-maintenance
description: >
Audit top-level documentation (README, SPEC, PRODUCT) against recent git
history to find drift — shipped features missing from docs or features
listed as upcoming that already landed. Proposes minimal edits, creates
a branch, and opens a PR. Use when asked to review docs for accuracy,
after major feature merges, or on a periodic schedule.
---
# Doc Maintenance Skill
Detect documentation drift and fix it via PR — no rewrites, no churn.
## When to Use
- Periodic doc review (e.g. weekly or after releases)
- After major feature merges
- When asked "are our docs up to date?"
- When asked to audit README / SPEC / PRODUCT accuracy
## Target Documents
| Document | Path | What matters |
|----------|------|-------------|
| README | `README.md` | Features table, roadmap, quickstart, "what is" accuracy, "works with" table |
| SPEC | `doc/SPEC.md` | No false "not supported" claims, major model/schema accuracy |
| PRODUCT | `doc/PRODUCT.md` | Core concepts, feature list, principles accuracy |
Out of scope: DEVELOPING.md, DATABASE.md, CLI.md, doc/plans/, skill files,
release notes. These are dev-facing or ephemeral — lower risk of user-facing
confusion.
## Workflow
### Step 1 — Detect what changed
Find the last review cursor:
```bash
# Read the last-reviewed commit SHA
CURSOR_FILE=".doc-review-cursor"
if [ -f "$CURSOR_FILE" ]; then
LAST_SHA=$(cat "$CURSOR_FILE" | head -1)
else
# First run: look back 60 days
LAST_SHA=$(git log --format="%H" --after="60 days ago" --reverse | head -1)
fi
```
Then gather commits since the cursor:
```bash
git log "$LAST_SHA"..HEAD --oneline --no-merges
```
### Step 2 — Classify changes
Scan commit messages and changed files. Categorize into:
- **Feature** — new capabilities (keywords: `feat`, `add`, `implement`, `support`)
- **Breaking** — removed/renamed things (keywords: `remove`, `breaking`, `drop`, `rename`)
- **Structural** — new directories, config changes, new adapters, new CLI commands
**Ignore:** refactors, test-only changes, CI config, dependency bumps, doc-only
changes, style/formatting commits. These don't affect doc accuracy.
For borderline cases, check the actual diff — a commit titled "refactor: X"
that adds a new public API is a feature.
### Step 3 — Build a change summary
Produce a concise list like:
```
Since last review (<sha>, <date>):
- FEATURE: Plugin system merged (runtime, SDK, CLI, slots, event bridge)
- FEATURE: Project archiving added
- BREAKING: Removed legacy webhook adapter
- STRUCTURAL: New .agents/skills/ directory convention
```
If there are no notable changes, skip to Step 7 (update cursor and exit).
### Step 4 — Audit each target doc
For each target document, read it fully and cross-reference against the change
summary. Check for:
1. **False negatives** — major shipped features not mentioned at all
2. **False positives** — features listed as "coming soon" / "roadmap" / "planned"
/ "not supported" / "TBD" that already shipped
3. **Quickstart accuracy** — install commands, prereqs, and startup instructions
still correct (README only)
4. **Feature table accuracy** — does the features section reflect current
capabilities? (README only)
5. **Works-with accuracy** — are supported adapters/integrations listed correctly?
Use `references/audit-checklist.md` as the structured checklist.
Use `references/section-map.md` to know where to look for each feature area.
### Step 5 — Create branch and apply minimal edits
```bash
# Create a branch for the doc updates
BRANCH="docs/maintenance-$(date +%Y%m%d)"
git checkout -b "$BRANCH"
```
Apply **only** the edits needed to fix drift. Rules:
- **Minimal patches only.** Fix inaccuracies, don't rewrite sections.
- **Preserve voice and style.** Match the existing tone of each document.
- **No cosmetic changes.** Don't fix typos, reformat tables, or reorganize
sections unless they're part of a factual fix.
- **No new sections.** If a feature needs a whole new section, note it in the
PR description as a follow-up — don't add it in a maintenance pass.
- **Roadmap items:** Move shipped features out of Roadmap. Add a brief mention
in the appropriate existing section if there isn't one already. Don't add
long descriptions.
### Step 6 — Open a PR
Commit the changes and open a PR:
```bash
git add README.md doc/SPEC.md doc/PRODUCT.md .doc-review-cursor
git commit -m "docs: update documentation for accuracy
- [list each fix briefly]
Co-Authored-By: Paperclip <noreply@paperclip.ing>"
git push -u origin "$BRANCH"
gh pr create \
--title "docs: periodic documentation accuracy update" \
--body "$(cat <<'EOF'
## Summary
Automated doc maintenance pass. Fixes documentation drift detected since
last review.
### Changes
- [list each fix]
### Change summary (since last review)
- [list notable code changes that triggered doc updates]
## Review notes
- Only factual accuracy fixes — no style/cosmetic changes
- Preserves existing voice and structure
- Larger doc additions (new sections, tutorials) noted as follow-ups
🤖 Generated by doc-maintenance skill
EOF
)"
```
### Step 7 — Update the cursor
After a successful audit (whether or not edits were needed), update the cursor:
```bash
git rev-parse HEAD > .doc-review-cursor
```
If edits were made, this is already committed in the PR branch. If no edits
were needed, commit the cursor update to the current branch.
## Change Classification Rules
| Signal | Category | Doc update needed? |
|--------|----------|-------------------|
| `feat:`, `add`, `implement`, `support` in message | Feature | Yes if user-facing |
| `remove`, `drop`, `breaking`, `!:` in message | Breaking | Yes |
| New top-level directory or config file | Structural | Maybe |
| `fix:`, `bugfix` | Fix | No (unless it changes behavior described in docs) |
| `refactor:`, `chore:`, `ci:`, `test:` | Maintenance | No |
| `docs:` | Doc change | No (already handled) |
| Dependency bumps only | Maintenance | No |
## Patch Style Guide
- Fix the fact, not the prose
- If removing a roadmap item, don't leave a gap — remove the bullet cleanly
- If adding a feature mention, match the format of surrounding entries
(e.g. if features are in a table, add a table row)
- Keep README changes especially minimal — it shouldn't churn often
- For SPEC/PRODUCT, prefer updating existing statements over adding new ones
(e.g. change "not supported in V1" to "supported via X" rather than adding
a new section)
## Output
When the skill completes, report:
- How many commits were scanned
- How many notable changes were found
- How many doc edits were made (and to which files)
- PR link (if edits were made)
- Any follow-up items that need larger doc work

View File

@@ -0,0 +1,85 @@
# Doc Maintenance Audit Checklist
Use this checklist when auditing each target document. For each item, compare
against the change summary from git history.
## README.md
### Features table
- [ ] Each feature card reflects a shipped capability
- [ ] No feature cards for things that don't exist yet
- [ ] No major shipped features missing from the table
### Roadmap
- [ ] Nothing listed as "planned" or "coming soon" that already shipped
- [ ] No removed/cancelled items still listed
- [ ] Items reflect current priorities (cross-check with recent PRs)
### Quickstart
- [ ] `npx paperclipai onboard` command is correct
- [ ] Manual install steps are accurate (clone URL, commands)
- [ ] Prerequisites (Node version, pnpm version) are current
- [ ] Server URL and port are correct
### "What is Paperclip" section
- [ ] High-level description is accurate
- [ ] Step table (Define goal / Hire team / Approve and run) is correct
### "Works with" table
- [ ] All supported adapters/runtimes are listed
- [ ] No removed adapters still listed
- [ ] Logos and labels match current adapter names
### "Paperclip is right for you if"
- [ ] Use cases are still accurate
- [ ] No claims about capabilities that don't exist
### "Why Paperclip is special"
- [ ] Technical claims are accurate (atomic execution, governance, etc.)
- [ ] No features listed that were removed or significantly changed
### FAQ
- [ ] Answers are still correct
- [ ] No references to removed features or outdated behavior
### Development section
- [ ] Commands are accurate (`pnpm dev`, `pnpm build`, etc.)
- [ ] Link to DEVELOPING.md is correct
## doc/SPEC.md
### Company Model
- [ ] Fields match current schema
- [ ] Governance model description is accurate
### Agent Model
- [ ] Adapter types match what's actually supported
- [ ] Agent configuration description is accurate
- [ ] No features described as "not supported" or "not V1" that shipped
### Task Model
- [ ] Task hierarchy description is accurate
- [ ] Status values match current implementation
### Extensions / Plugins
- [ ] If plugins are shipped, no "not in V1" or "future" language
- [ ] Plugin model description matches implementation
### Open Questions
- [ ] Resolved questions removed or updated
- [ ] No "TBD" items that have been decided
## doc/PRODUCT.md
### Core Concepts
- [ ] Company, Employees, Task Management descriptions accurate
- [ ] Agent Execution modes described correctly
- [ ] No missing major concepts
### Principles
- [ ] Principles haven't been contradicted by shipped features
- [ ] No principles referencing removed capabilities
### User Flow
- [ ] Dream scenario still reflects actual onboarding
- [ ] Steps are achievable with current features

View File

@@ -0,0 +1,22 @@
# Section Map
Maps feature areas to specific document sections so the skill knows where to
look when a feature ships or changes.
| Feature Area | README Section | SPEC Section | PRODUCT Section |
|-------------|---------------|-------------|----------------|
| Plugins / Extensions | Features table, Roadmap | Extensions, Agent Model | Core Concepts |
| Adapters (new runtimes) | "Works with" table, FAQ | Agent Model, Agent Configuration | Employees & Agents, Agent Execution |
| Governance / Approvals | Features table, "Why special" | Board Governance, Board Approval Gates | Principles |
| Budget / Cost Control | Features table, "Why special" | Budget Delegation | Company (revenue & expenses) |
| Task Management | Features table | Task Model | Task Management |
| Org Chart / Hierarchy | Features table | Agent Model (reporting) | Employees & Agents |
| Multi-Company | Features table, FAQ | Company Model | Company |
| Heartbeats | Features table, FAQ | Agent Execution | Agent Execution |
| CLI Commands | Development section | — | — |
| Onboarding / Quickstart | Quickstart, FAQ | — | User Flow |
| Skills / Skill Injection | "Why special" | — | — |
| Company Templates | "Why special", Roadmap (ClipMart) | — | — |
| Mobile / UI | Features table | — | — |
| Project Archiving | — | — | — |
| OpenClaw Integration | "Works with" table, FAQ | Agent Model | Agent Execution |

View File

@@ -0,0 +1,202 @@
---
name: pr-report
description: >
Review a pull request or contribution deeply, explain it tutorial-style for a
maintainer, and produce a polished report artifact such as HTML or Markdown.
Use when asked to analyze a PR, explain a contributor's design decisions,
compare it with similar systems, or prepare a merge recommendation.
---
# PR Report Skill
Produce a maintainer-grade review of a PR, branch, or large contribution.
Default posture:
- understand the change before judging it
- explain the system as built, not just the diff
- separate architectural problems from product-scope objections
- make a concrete recommendation, not a vague impression
## When to Use
Use this skill when the user asks for things like:
- "review this PR deeply"
- "explain this contribution to me"
- "make me a report or webpage for this PR"
- "compare this design to similar systems"
- "should I merge this?"
## Outputs
Common outputs:
- standalone HTML report in `tmp/reports/...`
- Markdown report in `report/` or another requested folder
- short maintainer summary in chat
If the user asks for a webpage, build a polished standalone HTML artifact with
clear sections and readable visual hierarchy.
Resources bundled with this skill:
- `references/style-guide.md` for visual direction and report presentation rules
- `assets/html-report-starter.html` for a reusable standalone HTML/CSS starter
## Workflow
### 1. Acquire and frame the target
Work from local code when possible, not just the GitHub PR page.
Gather:
- target branch or worktree
- diff size and changed subsystems
- relevant repo docs, specs, and invariants
- contributor intent if it is documented in PR text or design docs
Start by answering: what is this change *trying* to become?
### 2. Build a mental model of the system
Do not stop at file-by-file notes. Reconstruct the design:
- what new runtime or contract exists
- which layers changed: db, shared types, server, UI, CLI, docs
- lifecycle: install, startup, execution, UI, failure, disablement
- trust boundary: what code runs where, under what authority
For large contributions, include a tutorial-style section that teaches the
system from first principles.
### 3. Review like a maintainer
Findings come first. Order by severity.
Prioritize:
- behavioral regressions
- trust or security gaps
- misleading abstractions
- lifecycle and operational risks
- coupling that will be hard to unwind
- missing tests or unverifiable claims
Always cite concrete file references when possible.
### 4. Distinguish the objection type
Be explicit about whether a concern is:
- product direction
- architecture
- implementation quality
- rollout strategy
- documentation honesty
Do not hide an architectural objection inside a scope objection.
### 5. Compare to external precedents when needed
If the contribution introduces a framework or platform concept, compare it to
similar open-source systems.
When comparing:
- prefer official docs or source
- focus on extension boundaries, context passing, trust model, and UI ownership
- extract lessons, not just similarities
Good comparison questions:
- Who owns lifecycle?
- Who owns UI composition?
- Is context explicit or ambient?
- Are plugins trusted code or sandboxed code?
- Are extension points named and typed?
### 6. Make the recommendation actionable
Do not stop at "merge" or "do not merge."
Choose one:
- merge as-is
- merge after specific redesign
- salvage specific pieces
- keep as design research
If rejecting or narrowing, say what should be kept.
Useful recommendation buckets:
- keep the protocol/type model
- redesign the UI boundary
- narrow the initial surface area
- defer third-party execution
- ship a host-owned extension-point model first
### 7. Build the artifact
Suggested report structure:
1. Executive summary
2. What the PR actually adds
3. Tutorial: how the system works
4. Strengths
5. Main findings
6. Comparisons
7. Recommendation
For HTML reports:
- use intentional typography and color
- make navigation easy for long reports
- favor strong section headings and small reference labels
- avoid generic dashboard styling
Before building from scratch, read `references/style-guide.md`.
If a fast polished starter is helpful, begin from `assets/html-report-starter.html`
and replace the placeholder content with the actual report.
### 8. Verify before handoff
Check:
- artifact path exists
- findings still match the actual code
- any requested forbidden strings are absent from generated output
- if tests were not run, say so explicitly
## Review Heuristics
### Plugin and platform work
Watch closely for:
- docs claiming sandboxing while runtime executes trusted host processes
- module-global state used to smuggle React context
- hidden dependence on render order
- plugins reaching into host internals instead of using explicit APIs
- "capabilities" that are really policy labels on top of fully trusted code
### Good signs
- typed contracts shared across layers
- explicit extension points
- host-owned lifecycle
- honest trust model
- narrow first rollout with room to grow
## Final Response
In chat, summarize:
- where the report is
- your overall call
- the top one or two reasons
- whether verification or tests were skipped
Keep the chat summary shorter than the report itself.

View File

@@ -0,0 +1,426 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PR Report Starter</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=Newsreader:opsz,wght@6..72,500;6..72,700&display=swap"
rel="stylesheet"
/>
<style>
:root {
--bg: #f4efe5;
--paper: rgba(255, 251, 244, 0.88);
--paper-strong: #fffaf1;
--ink: #1f1b17;
--muted: #6a6257;
--line: rgba(31, 27, 23, 0.12);
--accent: #9c4729;
--accent-soft: rgba(156, 71, 41, 0.1);
--good: #2f6a42;
--warn: #946200;
--bad: #8c2f25;
--shadow: 0 22px 60px rgba(52, 37, 19, 0.1);
--radius: 20px;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
color: var(--ink);
font-family: "IBM Plex Sans", sans-serif;
background:
radial-gradient(circle at top left, rgba(156, 71, 41, 0.12), transparent 34rem),
radial-gradient(circle at top right, rgba(47, 106, 66, 0.08), transparent 28rem),
linear-gradient(180deg, #efe6d6 0%, var(--bg) 48%, #ece5d8 100%);
}
.shell {
width: min(1360px, calc(100vw - 32px));
margin: 24px auto;
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
gap: 24px;
}
.panel {
background: var(--paper);
backdrop-filter: blur(12px);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.nav {
position: sticky;
top: 20px;
align-self: start;
padding: 22px;
}
.eyebrow {
letter-spacing: 0.12em;
text-transform: uppercase;
font-size: 11px;
font-weight: 700;
color: var(--accent);
}
.nav h1,
.hero h1,
h2,
h3 {
font-family: "Newsreader", serif;
line-height: 0.96;
margin: 0;
}
.nav h1 {
font-size: 2rem;
margin-top: 10px;
}
.nav p {
color: var(--muted);
font-size: 0.95rem;
line-height: 1.5;
}
.nav ul {
list-style: none;
padding: 0;
margin: 18px 0 0;
display: grid;
gap: 10px;
}
.nav a {
display: block;
color: var(--ink);
text-decoration: none;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid transparent;
background: rgba(255, 255, 255, 0.35);
}
.nav a:hover {
border-color: var(--line);
background: rgba(255, 255, 255, 0.75);
}
.meta-block {
margin-top: 20px;
padding-top: 18px;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 0.86rem;
line-height: 1.5;
}
main {
display: grid;
gap: 24px;
}
section {
padding: 26px 28px 28px;
}
.hero {
padding: 28px;
overflow: hidden;
position: relative;
}
.hero::after {
content: "";
position: absolute;
inset: auto -3rem -6rem auto;
width: 18rem;
height: 18rem;
border-radius: 50%;
background: radial-gradient(circle, rgba(156, 71, 41, 0.14), transparent 68%);
pointer-events: none;
}
.hero h1 {
font-size: clamp(2.6rem, 5vw, 4.6rem);
max-width: 12ch;
margin-top: 12px;
}
.lede {
margin-top: 16px;
max-width: 70ch;
font-size: 1.05rem;
line-height: 1.65;
color: #2b2723;
}
.hero-grid,
.card-grid,
.two-col {
display: grid;
gap: 14px;
}
.hero-grid {
margin-top: 24px;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.card-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.two-col {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.metric,
.card,
.finding {
padding: 18px;
background: rgba(255, 255, 255, 0.68);
border: 1px solid var(--line);
border-radius: 18px;
}
.metric .label {
color: var(--muted);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.metric .value {
margin-top: 8px;
font-size: 1.45rem;
font-weight: 700;
}
h2 {
font-size: 2rem;
margin-bottom: 16px;
}
h3 {
font-size: 1.3rem;
margin-bottom: 10px;
}
p {
margin: 0 0 14px;
line-height: 1.65;
}
ul,
ol {
margin: 0;
padding-left: 20px;
line-height: 1.65;
}
li + li {
margin-top: 8px;
}
.badge-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 18px 0 8px;
}
.badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 700;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.68);
}
.badge.good {
color: var(--good);
}
.badge.warn {
color: var(--warn);
}
.badge.bad {
color: var(--bad);
}
.quote {
margin-top: 18px;
padding: 18px;
border-left: 4px solid var(--accent);
border-radius: 14px;
background: var(--accent-soft);
}
.severity {
display: inline-flex;
margin-bottom: 12px;
padding: 6px 10px;
border-radius: 999px;
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.severity.high {
background: rgba(140, 47, 37, 0.12);
color: var(--bad);
}
.severity.medium {
background: rgba(148, 98, 0, 0.12);
color: var(--warn);
}
.severity.low {
background: rgba(47, 106, 66, 0.12);
color: var(--good);
}
.ref {
color: var(--muted);
font-size: 0.82rem;
line-height: 1.5;
}
@media (max-width: 980px) {
.shell {
grid-template-columns: 1fr;
}
.nav {
position: static;
}
.hero-grid,
.card-grid,
.two-col {
grid-template-columns: 1fr;
}
.hero h1 {
max-width: 100%;
}
}
</style>
</head>
<body>
<div class="shell">
<aside class="panel nav">
<div class="eyebrow">Maintainer Report</div>
<h1>Report Title</h1>
<p>Replace this with a concise description of what the report covers.</p>
<ul>
<li><a href="#summary">Summary</a></li>
<li><a href="#tutorial">Tutorial</a></li>
<li><a href="#findings">Findings</a></li>
<li><a href="#recommendation">Recommendation</a></li>
</ul>
<div class="meta-block">
Replace with project metadata, review date, or scope notes.
</div>
</aside>
<main>
<section class="panel hero" id="summary">
<div class="eyebrow">Executive Summary</div>
<h1>Use the hero for the clearest one-line judgment.</h1>
<p class="lede">
Replace this with the short explanation of what the contribution does, why it matters,
and what the core maintainer question is.
</p>
<div class="badge-row">
<span class="badge good">Strength</span>
<span class="badge warn">Tradeoff</span>
<span class="badge bad">Risk</span>
</div>
<div class="hero-grid">
<div class="metric">
<div class="label">Overall Call</div>
<div class="value">Placeholder</div>
</div>
<div class="metric">
<div class="label">Main Concern</div>
<div class="value">Placeholder</div>
</div>
<div class="metric">
<div class="label">Best Part</div>
<div class="value">Placeholder</div>
</div>
<div class="metric">
<div class="label">Weakest Part</div>
<div class="value">Placeholder</div>
</div>
</div>
<div class="quote">
Use this block for the thesis, a sharp takeaway, or a key cited point.
</div>
</section>
<section class="panel" id="tutorial">
<h2>Tutorial Section</h2>
<div class="two-col">
<div class="card">
<h3>Concept Card</h3>
<p>Use cards for mental models, subsystems, or comparison slices.</p>
<div class="ref">path/to/file.ts:10</div>
</div>
<div class="card">
<h3>Second Card</h3>
<p>Keep cards fairly dense. This template is about style, not fixed structure.</p>
<div class="ref">path/to/file.ts:20</div>
</div>
</div>
</section>
<section class="panel" id="findings">
<h2>Findings</h2>
<article class="finding">
<div class="severity high">High</div>
<h3>Finding Title</h3>
<p>Use findings for the sharpest judgment calls and risks.</p>
<div class="ref">path/to/file.ts:30</div>
</article>
</section>
<section class="panel" id="recommendation">
<h2>Recommendation</h2>
<div class="card-grid">
<div class="card">
<h3>Path Forward</h3>
<p>Use this area for merge guidance, salvage plan, or rollout advice.</p>
</div>
<div class="card">
<h3>What To Keep</h3>
<p>Call out the parts worth preserving even if the whole proposal should not land.</p>
</div>
</div>
</section>
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,149 @@
# PR Report Style Guide
Use this guide when the user wants a report artifact, especially a webpage.
## Goal
Make the report feel like an editorial review, not an internal admin dashboard.
The page should make a long technical argument easy to scan without looking
generic or overdesigned.
## Visual Direction
Preferred tone:
- editorial
- warm
- serious
- high-contrast
- handcrafted, not corporate SaaS
Avoid:
- default app-shell layouts
- purple gradients on white
- generic card dashboards
- cramped pages with weak hierarchy
- novelty fonts that hurt readability
## Typography
Recommended pattern:
- one expressive serif or display face for major headings
- one sturdy sans-serif for body copy and UI labels
Good combinations:
- Newsreader + IBM Plex Sans
- Source Serif 4 + Instrument Sans
- Fraunces + Public Sans
- Libre Baskerville + Work Sans
Rules:
- headings should feel deliberate and large
- body copy should stay comfortable for long reading
- reference labels and badges should use smaller dense sans text
## Layout
Recommended structure:
- a sticky side or top navigation for long reports
- one strong hero summary at the top
- panel or paper-like sections for each major topic
- multi-column card grids for comparisons and strengths
- single-column body text for findings and recommendations
Use generous spacing. Long-form technical reports need breathing room.
## Color
Prefer muted paper-like backgrounds with one warm accent and one cool counterweight.
Suggested token categories:
- `--bg`
- `--paper`
- `--ink`
- `--muted`
- `--line`
- `--accent`
- `--good`
- `--warn`
- `--bad`
The accent should highlight navigation, badges, and important labels. Do not
let accent colors dominate body text.
## Useful UI Elements
Include small reusable styles for:
- summary metrics
- badges
- quotes or callouts
- finding cards
- severity labels
- reference labels
- comparison cards
- responsive two-column sections
## Motion
Keep motion restrained.
Good:
- soft fade/slide-in on first load
- hover response on nav items or cards
Bad:
- constant animation
- floating blobs
- decorative motion with no reading benefit
## Content Presentation
Even when the user wants design polish, clarity stays primary.
Good structure for long reports:
1. executive summary
2. what changed
3. tutorial explanation
4. strengths
5. findings
6. comparisons
7. recommendation
The exact headings can change. The important thing is to separate explanation
from judgment.
## References
Reference labels should be visually quiet but easy to spot.
Good pattern:
- small muted text
- monospace or compact sans
- keep them close to the paragraph they support
## Starter Usage
If you need a fast polished base, start from:
- `assets/html-report-starter.html`
Customize:
- fonts
- color tokens
- hero copy
- section ordering
- card density
Do not preserve the placeholder sections if they do not fit the actual report.

View File

@@ -0,0 +1,209 @@
---
name: prcheckloop
description: >
Iteratively gets a GitHub pull request's checks green. Detects the PR for the
current branch or uses a provided PR number, waits for every check on the
latest head SHA to appear and finish, investigates failing checks, fixes
actionable code or test issues, pushes, and repeats. Escalates with a precise
blocker when failures are external, flaky, or not safely fixable. Use when a
PR still has unsuccessful checks after review fixes, including after greploop.
---
# PRCheckloop
Get a GitHub PR to a fully green check state, or exit with a concrete blocker.
## Scope
- GitHub PRs only. If the repo is GitLab, stop and use `check-pr`.
- Focus on checks for the latest PR head SHA, not old commits.
- Focus on CI/status checks, not review comments or PR template cleanup.
- If the user also wants review-comment cleanup, pair this with `check-pr`.
## Inputs
- **PR number** (optional): If not provided, detect the PR for the current branch.
- **Max iterations**: default `5`.
## Workflow
### 1. Identify the PR
If no PR number is provided, detect it from the current branch:
```bash
gh pr view --json number,headRefName,headRefOid,url,isDraft
```
If needed, switch to the PR branch before making changes.
Stop early if:
- `gh` is not authenticated
- there is no PR for the branch
- the repo is not hosted on GitHub
### 2. Track the latest head SHA
Always work against the current PR head SHA:
```bash
PR_JSON=$(gh pr view "$PR_NUMBER" --json number,headRefName,headRefOid,url)
HEAD_SHA=$(echo "$PR_JSON" | jq -r .headRefOid)
PR_URL=$(echo "$PR_JSON" | jq -r .url)
```
Ignore failing checks from older SHAs. After every push, refresh `HEAD_SHA` and
restart the inspection loop.
### 3. Inventory checks for that SHA
Fetch both GitHub check runs and legacy commit status contexts:
```bash
gh api "repos/{owner}/{repo}/commits/$HEAD_SHA/check-runs?per_page=100"
gh api "repos/{owner}/{repo}/commits/$HEAD_SHA/status"
```
For a compact PR-level view, this GraphQL payload is useful:
```bash
gh api graphql -f query='
query($owner:String!, $repo:String!, $pr:Int!) {
repository(owner:$owner, name:$repo) {
pullRequest(number:$pr) {
headRefOid
url
statusCheckRollup {
contexts(first:100) {
nodes {
__typename
... on CheckRun { name status conclusion detailsUrl workflowName }
... on StatusContext { context state targetUrl description }
}
}
}
}
}
}' -F owner=OWNER -F repo=REPO -F pr="$PR_NUMBER"
```
### 4. Wait for checks to actually run
After a new push, checks can take a moment to appear. Poll every 15-30 seconds
until one of these is true:
- checks have appeared and every item is in a terminal state
- checks have appeared and at least one failed
- no checks appear after a reasonable wait, usually 2 minutes
Treat these as terminal success states:
- check runs: `SUCCESS`, `NEUTRAL`, `SKIPPED`
- status contexts: `SUCCESS`
Treat these as pending:
- check runs: `QUEUED`, `PENDING`, `WAITING`, `REQUESTED`, `IN_PROGRESS`
- status contexts: `PENDING`
Treat these as failures:
- check runs: `FAILURE`, `TIMED_OUT`, `CANCELLED`, `ACTION_REQUIRED`, `STARTUP_FAILURE`, `STALE`
- status contexts: `FAILURE`, `ERROR`
If no checks appear for the latest SHA, inspect `.github/workflows/`, workflow
path filters, and branch protection expectations. If the missing check cannot be
caused or fixed from the repo, escalate.
### 5. Investigate failing checks
For GitHub Actions failures, inspect runs and failed logs for the current SHA:
```bash
gh run list --commit "$HEAD_SHA" --json databaseId,workflowName,status,conclusion,url,headSha
gh run view <RUN_ID> --json databaseId,name,workflowName,status,conclusion,jobs,url,headSha
gh run view <RUN_ID> --log-failed
```
For each failing check, classify it:
| Failure type | Action |
|---|---|
| Code/test regression | Reproduce locally, fix, and verify |
| Lint/type/build mismatch | Run the matching local command from the workflow and fix it |
| Flake or transient infra issue | Rerun once if evidence supports flakiness |
| External service/status app failure | Escalate with the details URL and owner guess |
| Missing secret/permission/branch protection issue | Escalate immediately |
Only rerun a failed job once without code changes. Do not loop on reruns.
### 6. Fix actionable failures
If the failure is actionable from the checked-out code:
1. Read the workflow or failing command to identify the real gate.
2. Reproduce locally where reasonable.
3. Make the smallest correct fix.
4. Run focused verification first, then broader verification if needed.
5. Commit in a logical commit.
6. Push before re-checking the PR.
Do not stop at a local fix. The loop is only complete when the remote PR checks
for the new head SHA are green.
### 7. Push and repeat
After each fix:
```bash
git push
sleep 5
```
Then refresh the PR metadata, get the new `HEAD_SHA`, and restart from Step 3.
Exit the loop only when:
- all checks for the latest head SHA are green, or
- a blocker remains after reasonable repair effort, or
- the max iteration count is reached
### 8. Escalate blockers precisely
If you cannot get the PR green, report:
- PR URL
- latest head SHA
- exact failing or missing check names
- details URLs
- what you already tried
- why it is blocked
- who should likely unblock it
- the next concrete action
Good blocker examples:
- external status app outage
- missing GitHub secret or permission
- required check name mismatch in branch protection
- persistent flake after one rerun
- failure needs credentials or infrastructure access you do not have
## Output
When the skill completes, report:
- PR URL and branch
- final head SHA
- green/pending/failing check summary
- fixes made and verification run
- whether changes were pushed
- blocker summary if not fully green
## Notes
- This skill is intentionally narrower than `check-pr`: it is a repair loop for
PR checks.
- This skill complements `greploop`: Greptile can be perfect while CI is still
red.

View File

@@ -0,0 +1,192 @@
---
name: release-changelog
description: >
Generate the stable Paperclip release changelog at releases/vYYYY.MDD.P.md by
reading commits, changesets, and merged PR context since the last stable tag.
---
# Release Changelog Skill
Generate the user-facing changelog for the **stable** Paperclip release.
## Versioning Model
Paperclip uses **calendar versioning (calver)**:
- Stable releases: `YYYY.MDD.P` (e.g. `2026.318.0`)
- Canary releases: `YYYY.MDD.P-canary.N` (e.g. `2026.318.1-canary.0`)
- Git tags: `vYYYY.MDD.P` for stable, `canary/vYYYY.MDD.P-canary.N` for canary
There are no major/minor/patch bumps. The stable version is derived from the
intended release date (UTC) plus the next same-day stable patch slot.
Output:
- `releases/vYYYY.MDD.P.md`
Important rules:
- even if there are canary releases such as `2026.318.1-canary.0`, the changelog file stays `releases/v2026.318.1.md`
- do not derive versions from semver bump types
- do not create canary changelog files
## Step 0 — Idempotency Check
Before generating anything, check whether the file already exists:
```bash
ls releases/vYYYY.MDD.P.md 2>/dev/null
```
If it exists:
1. read it first
2. present it to the reviewer
3. ask whether to keep it, regenerate it, or update specific sections
4. never overwrite it silently
## Step 1 — Determine the Stable Range
Find the last stable tag:
```bash
git tag --list 'v*' --sort=-version:refname | head -1
git log v{last}..HEAD --oneline --no-merges
```
The stable version comes from one of:
- an explicit maintainer request
- `./scripts/release.sh stable --date YYYY-MM-DD --print-version`
- the release plan already agreed in `doc/RELEASING.md`
Do not derive the changelog version from a canary tag or prerelease suffix.
Do not derive major/minor/patch bumps from API intent — calver uses the date and same-day stable slot.
## Step 2 — Gather the Raw Inputs
Collect release data from:
1. git commits since the last stable tag
2. `.changeset/*.md` files
3. merged PRs via `gh` when available
Useful commands:
```bash
git log v{last}..HEAD --oneline --no-merges
git log v{last}..HEAD --format="%H %s" --no-merges
ls .changeset/*.md | grep -v README.md
gh pr list --state merged --search "merged:>={last-tag-date}" --json number,title,body,labels
```
## Step 3 — Detect Breaking Changes
Look for:
- destructive migrations
- removed or changed API fields/endpoints
- renamed or removed config keys
- `BREAKING:` or `BREAKING CHANGE:` commit signals
Key commands:
```bash
git diff --name-only v{last}..HEAD -- packages/db/src/migrations/
git diff v{last}..HEAD -- packages/db/src/schema/
git diff v{last}..HEAD -- server/src/routes/ server/src/api/
git log v{last}..HEAD --format="%s" | rg -n 'BREAKING CHANGE|BREAKING:|^[a-z]+!:' || true
```
If breaking changes are detected, flag them prominently — they must appear in the
Breaking Changes section with an upgrade path.
## Step 4 — Categorize for Users
Use these stable changelog sections:
- `Breaking Changes`
- `Highlights`
- `Improvements`
- `Fixes`
- `Upgrade Guide` when needed
Exclude purely internal refactors, CI changes, and docs-only work unless they materially affect users.
Guidelines:
- group related commits into one user-facing entry
- write from the user perspective
- keep highlights short and concrete
- spell out upgrade actions for breaking changes
### Inline PR and contributor attribution
When a bullet item clearly maps to a merged pull request, add inline attribution at the
end of the entry in this format:
```
- **Feature name** — Description. ([#123](https://github.com/paperclipai/paperclip/pull/123), @contributor1, @contributor2)
```
Rules:
- Only add a PR link when you can confidently trace the bullet to a specific merged PR.
Use merge commit messages (`Merge pull request #N from user/branch`) to map PRs.
- List the contributor(s) who authored the PR. Use GitHub usernames, not real names or emails.
- If multiple PRs contributed to a single bullet, list them all: `([#10](url), [#12](url), @user1, @user2)`.
- If you cannot determine the PR number or contributor with confidence, omit the attribution
parenthetical — do not guess.
- Core maintainer commits that don't have an external PR can omit the parenthetical.
## Step 5 — Write the File
Template:
```markdown
# vYYYY.MDD.P
> Released: YYYY-MM-DD
## Breaking Changes
## Highlights
## Improvements
## Fixes
## Upgrade Guide
## Contributors
Thank you to everyone who contributed to this release!
@username1, @username2, @username3
```
Omit empty sections except `Highlights`, `Improvements`, and `Fixes`, which should usually exist.
The `Contributors` section should always be included. List every person who authored
commits in the release range, @-mentioning them by their **GitHub username** (not their
real name or email). To find GitHub usernames:
1. Extract usernames from merge commit messages: `git log v{last}..HEAD --oneline --merges` — the branch prefix (e.g. `from username/branch`) gives the GitHub username.
2. For noreply emails like `user@users.noreply.github.com`, the username is the part before `@`.
3. For contributors whose username is ambiguous, check `gh api users/{guess}` or the PR page.
**Never expose contributor email addresses.** Use `@username` only.
Exclude bot accounts (e.g. `lockfile-bot`, `dependabot`) from the list. List contributors
in alphabetical order by GitHub username (case-insensitive).
## Step 6 — Review Before Release
Before handing it off:
1. confirm the heading is the stable version only
2. confirm there is no `-canary` language in the title or filename
3. confirm any breaking changes have an upgrade path
4. present the draft for human sign-off
This skill never publishes anything. It only prepares the stable changelog artifact.

View File

@@ -0,0 +1,247 @@
---
name: release
description: >
Coordinate a full Paperclip release across engineering verification, npm,
GitHub, smoke testing, and announcement follow-up. Use when leadership asks
to ship a release, not merely to discuss versioning.
---
# Release Coordination Skill
Run the full Paperclip maintainer release workflow, not just an npm publish.
This skill coordinates:
- stable changelog drafting via `release-changelog`
- canary verification and publish status from `master`
- Docker smoke testing via `scripts/docker-onboard-smoke.sh`
- manual stable promotion from a chosen source ref
- GitHub Release creation
- website / announcement follow-up tasks
## Trigger
Use this skill when leadership asks for:
- "do a release"
- "ship the release"
- "promote this canary to stable"
- "cut the stable release"
## Preconditions
Before proceeding, verify all of the following:
1. `.agents/skills/release-changelog/SKILL.md` exists and is usable.
2. The repo working tree is clean, including untracked files.
3. There is at least one canary or candidate commit since the last stable tag.
4. The candidate SHA has passed the verification gate or is about to.
5. If manifests changed, the CI-owned `pnpm-lock.yaml` refresh is already merged on `master`.
6. npm publish rights are available through GitHub trusted publishing, or through local npm auth for emergency/manual use.
7. If running through Paperclip, you have issue context for status updates and follow-up task creation.
If any precondition fails, stop and report the blocker.
## Inputs
Collect these inputs up front:
- whether the target is a canary check or a stable promotion
- the candidate `source_ref` for stable
- whether the stable run is dry-run or live
- release issue / company context for website and announcement follow-up
## Step 0 — Release Model
Paperclip now uses a commit-driven release model:
1. every push to `master` publishes a canary automatically
2. canaries use `YYYY.MDD.P-canary.N`
3. stable releases use `YYYY.MDD.P`
4. the middle slot is `MDD`, where `M` is the UTC month and `DD` is the zero-padded UTC day
5. the stable patch slot increments when more than one stable ships on the same UTC date
6. stable releases are manually promoted from a chosen tested commit or canary source commit
7. only stable releases get `releases/vYYYY.MDD.P.md`, git tag `vYYYY.MDD.P`, and a GitHub Release
Critical consequences:
- do not use release branches as the default path
- do not derive major/minor/patch bumps
- do not create canary changelog files
- do not create canary GitHub Releases
## Step 1 — Choose the Candidate
For canary validation:
- inspect the latest successful canary run on `master`
- record the canary version and source SHA
For stable promotion:
1. choose the tested source ref
2. confirm it is the exact SHA you want to promote
3. resolve the target stable version with `./scripts/release.sh stable --date YYYY-MM-DD --print-version`
Useful commands:
```bash
git tag --list 'v*' --sort=-version:refname | head -1
git log --oneline --no-merges
npm view paperclipai@canary version
```
## Step 2 — Draft the Stable Changelog
Stable changelog files live at:
- `releases/vYYYY.MDD.P.md`
Invoke `release-changelog` and generate or update the stable notes only.
Rules:
- review the draft with a human before publish
- preserve manual edits if the file already exists
- keep the filename stable-only
- do not create a canary changelog file
## Step 3 — Verify the Candidate SHA
Run the standard gate:
```bash
pnpm -r typecheck
pnpm test:run
pnpm build
```
If the GitHub release workflow will run the publish, it can rerun this gate. Still report local status if you checked it.
For PRs that touch release logic, the repo also runs a canary release dry-run in CI. That is a release-specific guard, not a substitute for the standard gate.
## Step 4 — Validate the Canary
The normal canary path is automatic from `master` via:
- `.github/workflows/release.yml`
Confirm:
1. verification passed
2. npm canary publish succeeded
3. git tag `canary/vYYYY.MDD.P-canary.N` exists
Useful checks:
```bash
npm view paperclipai@canary version
git tag --list 'canary/v*' --sort=-version:refname | head -5
```
## Step 5 — Smoke Test the Canary
Run:
```bash
PAPERCLIPAI_VERSION=canary ./scripts/docker-onboard-smoke.sh
```
Useful isolated variant:
```bash
HOST_PORT=3232 DATA_DIR=./data/release-smoke-canary PAPERCLIPAI_VERSION=canary ./scripts/docker-onboard-smoke.sh
```
Confirm:
1. install succeeds
2. onboarding completes without crashes
3. the server boots
4. the UI loads
5. basic company creation and dashboard load work
If smoke testing fails:
- stop the stable release
- fix the issue on `master`
- wait for the next automatic canary
- rerun smoke testing
## Step 6 — Preview or Publish Stable
The normal stable path is manual `workflow_dispatch` on:
- `.github/workflows/release.yml`
Inputs:
- `source_ref`
- `stable_date`
- `dry_run`
Before live stable:
1. resolve the target stable version with `./scripts/release.sh stable --date YYYY-MM-DD --print-version`
2. ensure `releases/vYYYY.MDD.P.md` exists on the source ref
3. run the stable workflow in dry-run mode first when practical
4. then run the real stable publish
The stable workflow:
- re-verifies the exact source ref
- computes the next stable patch slot for the chosen UTC date
- publishes `YYYY.MDD.P` under dist-tag `latest`
- creates git tag `vYYYY.MDD.P`
- creates or updates the GitHub Release from `releases/vYYYY.MDD.P.md`
Local emergency/manual commands:
```bash
./scripts/release.sh stable --dry-run
./scripts/release.sh stable
git push public-gh refs/tags/vYYYY.MDD.P
./scripts/create-github-release.sh YYYY.MDD.P
```
## Step 7 — Finish the Other Surfaces
Create or verify follow-up work for:
- website changelog publishing
- launch post / social announcement
- release summary in Paperclip issue context
These should reference the stable release, not the canary.
## Failure Handling
If the canary is bad:
- publish another canary, do not ship stable
If stable npm publish succeeds but tag push or GitHub release creation fails:
- fix the git/GitHub issue immediately from the same release result
- do not republish the same version
If `latest` is bad after stable publish:
```bash
./scripts/rollback-latest.sh <last-good-version>
```
Then fix forward with a new stable release.
## Output
When the skill completes, provide:
- candidate SHA and tested canary version, if relevant
- stable version, if promoted
- verification status
- npm status
- smoke-test status
- git tag / GitHub Release status
- website / announcement follow-up status
- rollback recommendation if anything is still partially complete

View File

@@ -1,8 +0,0 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets).
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md).

View File

@@ -1,11 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.3/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [["@paperclipai/*", "paperclipai"]],
"linked": [],
"access": "public",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": ["@paperclipai/ui"]
}

View File

@@ -0,0 +1 @@
../../.agents/skills/company-creator

View File

@@ -1,3 +1,4 @@
DATABASE_URL=postgres://paperclip:paperclip@localhost:5432/paperclip
PORT=3100
SERVE_UI=false
BETTER_AUTH_SECRET=paperclip-dev-secret

17
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,17 @@
# Replace @cryppadotta if a different maintainer or team should own release infrastructure.
.github/** @cryppadotta @devinfoley
scripts/release*.sh @cryppadotta @devinfoley
scripts/release-*.mjs @cryppadotta @devinfoley
scripts/create-github-release.sh @cryppadotta @devinfoley
scripts/rollback-latest.sh @cryppadotta @devinfoley
doc/RELEASING.md @cryppadotta @devinfoley
doc/PUBLISHING.md @cryppadotta @devinfoley
doc/RELEASE-AUTOMATION-SETUP.md @cryppadotta @devinfoley
# Package files — dependency changes require review
# package.json matches recursively at all depths (covers root + all workspaces)
package.json @cryppadotta @devinfoley
pnpm-lock.yaml @cryppadotta @devinfoley
pnpm-workspace.yaml @cryppadotta @devinfoley
.npmrc @cryppadotta @devinfoley

65
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,65 @@
## Thinking Path
<!--
Required. Trace your reasoning from the top of the project down to this
specific change. Start with what Paperclip is, then narrow through the
subsystem, the problem, and why this PR exists. Use blockquote style.
Aim for 58 steps. See CONTRIBUTING.md for full examples.
-->
> - Paperclip orchestrates AI agents for zero-human companies
> - [Which subsystem or capability is involved]
> - [What problem or gap exists]
> - [Why it needs to be addressed]
> - This pull request ...
> - The benefit is ...
## What Changed
<!-- Bullet list of concrete changes. One bullet per logical unit. -->
-
## Verification
<!--
How can a reviewer confirm this works? Include test commands, manual
steps, or both. For UI changes, include before/after screenshots.
-->
-
## Risks
<!--
What could go wrong? Mention migration safety, breaking changes,
behavioral shifts, or "Low risk" if genuinely minor.
-->
-
## Model Used
<!--
Required. Specify which AI model was used to produce or assist with
this change. Be as descriptive as possible — include:
• Provider and model name (e.g., Claude, GPT, Gemini, Codex)
• Exact model ID or version (e.g., claude-opus-4-6, gpt-4-turbo-2024-04-09)
• Context window size if relevant (e.g., 1M context)
• Reasoning/thinking mode if applicable (e.g., extended thinking, chain-of-thought)
• Any other relevant capability details (e.g., tool use, code execution)
If no AI model was used, write "None — human-authored".
-->
-
## Checklist
- [ ] I have included a thinking path that traces from project context to this change
- [ ] I have specified the model used (with version and capability details)
- [ ] I have run tests locally and they pass
- [ ] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after screenshots
- [ ] I have updated relevant documentation to reflect my changes
- [ ] I have considered and documented any risks above
- [ ] I will address all Greptile and reviewer comments before requesting merge

55
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Docker
on:
push:
branches:
- "master"
tags:
- "v*"
permissions:
contents: read
packages: write
jobs:
build-and-push:
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: docker-${{ github.ref }}
cancel-in-progress: true
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

44
.github/workflows/e2e.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: E2E Tests
on:
workflow_dispatch:
inputs:
skip_llm:
description: "Skip LLM-dependent assertions (default: true)"
type: boolean
default: true
jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 30
env:
PAPERCLIP_E2E_SKIP_LLM: ${{ inputs.skip_llm && 'true' || 'false' }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: npx playwright install --with-deps chromium
- name: Run e2e tests
run: pnpm run test:e2e
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: |
tests/e2e/playwright-report/
tests/e2e/test-results/
retention-days: 14

186
.github/workflows/pr.yml vendored Normal file
View File

@@ -0,0 +1,186 @@
name: PR
on:
pull_request:
branches:
- master
concurrency:
group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
policy:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Block manual lockfile edits
if: github.head_ref != 'chore/refresh-lockfile'
run: |
changed="$(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}")"
if printf '%s\n' "$changed" | grep -qx 'pnpm-lock.yaml'; then
echo "Do not commit pnpm-lock.yaml in pull requests. CI owns lockfile updates."
exit 1
fi
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
- name: Validate Dockerfile deps stage
run: |
missing=0
# Extract only the deps stage from the Dockerfile
deps_stage="$(awk '/^FROM .* AS deps$/{found=1; next} found && /^FROM /{exit} found{print}' Dockerfile)"
if [ -z "$deps_stage" ]; then
echo "::error::Could not extract deps stage from Dockerfile (expected 'FROM ... AS deps')"
exit 1
fi
# Derive workspace search roots from pnpm-workspace.yaml (exclude dev-only packages)
search_roots="$(grep '^ *- ' pnpm-workspace.yaml | sed 's/^ *- //' | sed 's/\*$//' | grep -v 'examples' | grep -v 'create-paperclip-plugin' | tr '\n' ' ')"
if [ -z "$search_roots" ]; then
echo "::error::Could not derive workspace roots from pnpm-workspace.yaml"
exit 1
fi
# Check all workspace package.json files are copied in the deps stage
for pkg in $(find $search_roots -maxdepth 2 -name package.json -not -path '*/examples/*' -not -path '*/create-paperclip-plugin/*' -not -path '*/node_modules/*' 2>/dev/null | sort -u); do
dir="$(dirname "$pkg")"
if ! echo "$deps_stage" | grep -q "^COPY ${dir}/package.json"; then
echo "::error::Dockerfile deps stage missing: COPY ${pkg} ${dir}/"
missing=1
fi
done
# Check patches directory is copied if it exists
if [ -d patches ] && ! echo "$deps_stage" | grep -q '^COPY patches/'; then
echo "::error::Dockerfile deps stage missing: COPY patches/ patches/"
missing=1
fi
if [ "$missing" -eq 1 ]; then
echo "Dockerfile deps stage is out of sync. Update it to include the missing files."
exit 1
fi
- name: Validate dependency resolution when manifests change
run: |
changed="$(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}")"
manifest_pattern='(^|/)package\.json$|^pnpm-workspace\.yaml$|^\.npmrc$|^pnpmfile\.(cjs|js|mjs)$'
if printf '%s\n' "$changed" | grep -Eq "$manifest_pattern"; then
pnpm install --lockfile-only --ignore-scripts --no-frozen-lockfile
fi
verify:
needs: [policy]
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Typecheck
run: pnpm -r typecheck
- name: Run tests
run: pnpm test:run
- name: Build
run: pnpm build
- name: Release canary dry run
run: |
git checkout -B master HEAD
git checkout -- pnpm-lock.yaml
./scripts/release.sh canary --skip-verify --dry-run
e2e:
needs: [policy]
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Install Playwright
run: npx playwright install --with-deps chromium
- name: Generate Paperclip config
run: |
mkdir -p ~/.paperclip/instances/default
cat > ~/.paperclip/instances/default/config.json << 'CONF'
{
"$meta": { "version": 1, "updatedAt": "2026-01-01T00:00:00.000Z", "source": "onboard" },
"database": { "mode": "embedded-postgres" },
"logging": { "mode": "file" },
"server": { "deploymentMode": "local_trusted", "host": "127.0.0.1", "port": 3100 },
"auth": { "baseUrlMode": "auto" },
"storage": { "provider": "local_disk" },
"secrets": { "provider": "local_encrypted", "strictMode": false }
}
CONF
- name: Run e2e tests
env:
PAPERCLIP_E2E_SKIP_LLM: "true"
run: pnpm run test:e2e
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: |
tests/e2e/playwright-report/
tests/e2e/test-results/
retention-days: 14

96
.github/workflows/refresh-lockfile.yml vendored Normal file
View File

@@ -0,0 +1,96 @@
name: Refresh Lockfile
on:
push:
branches:
- master
workflow_dispatch:
concurrency:
group: refresh-lockfile-master
cancel-in-progress: false
jobs:
refresh:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Refresh pnpm lockfile
run: pnpm install --lockfile-only --ignore-scripts --no-frozen-lockfile
- name: Fail on unexpected file changes
run: |
changed="$(git status --porcelain)"
if [ -z "$changed" ]; then
echo "Lockfile is already up to date."
exit 0
fi
if printf '%s\n' "$changed" | grep -Fvq ' pnpm-lock.yaml'; then
echo "Unexpected files changed during lockfile refresh:"
echo "$changed"
exit 1
fi
- name: Create or update pull request
id: upsert-pr
env:
GH_TOKEN: ${{ github.token }}
REPO_OWNER: ${{ github.repository_owner }}
run: |
if git diff --quiet -- pnpm-lock.yaml; then
echo "Lockfile unchanged, nothing to do."
echo "pr_url=" >> "$GITHUB_OUTPUT"
exit 0
fi
BRANCH="chore/refresh-lockfile"
git config user.name "lockfile-bot"
git config user.email "lockfile-bot@users.noreply.github.com"
git checkout -B "$BRANCH"
git add pnpm-lock.yaml
git commit -m "chore(lockfile): refresh pnpm-lock.yaml"
git push --force origin "$BRANCH"
# Only reuse an open PR from this repository owner, not a fork with the same branch name.
pr_url="$(
gh pr list --state open --head "$BRANCH" --json url,headRepositoryOwner \
--jq ".[] | select(.headRepositoryOwner.login == \"$REPO_OWNER\") | .url" |
head -n 1
)"
if [ -z "$pr_url" ]; then
pr_url="$(gh pr create \
--head "$BRANCH" \
--title "chore(lockfile): refresh pnpm-lock.yaml" \
--body "Auto-generated lockfile refresh after dependencies changed on master. This PR only updates pnpm-lock.yaml.")"
echo "Created new PR: $pr_url"
else
echo "PR already exists: $pr_url"
fi
echo "pr_url=$pr_url" >> "$GITHUB_OUTPUT"
- name: Enable auto-merge for lockfile PR
if: steps.upsert-pr.outputs.pr_url != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
gh pr merge --auto --squash --delete-branch "${{ steps.upsert-pr.outputs.pr_url }}"

118
.github/workflows/release-smoke.yml vendored Normal file
View File

@@ -0,0 +1,118 @@
name: Release Smoke
on:
workflow_dispatch:
inputs:
paperclip_version:
description: Published Paperclip dist-tag to test
required: true
default: canary
type: choice
options:
- canary
- latest
host_port:
description: Host port for the Docker smoke container
required: false
default: "3232"
type: string
artifact_name:
description: Artifact name for uploaded diagnostics
required: false
default: release-smoke
type: string
workflow_call:
inputs:
paperclip_version:
required: true
type: string
host_port:
required: false
default: "3232"
type: string
artifact_name:
required: false
default: release-smoke
type: string
jobs:
smoke:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Install Playwright browser
run: npx playwright install --with-deps chromium
- name: Launch Docker smoke harness
run: |
metadata_file="$RUNNER_TEMP/release-smoke.env"
HOST_PORT="${{ inputs.host_port }}" \
DATA_DIR="$RUNNER_TEMP/release-smoke-data" \
PAPERCLIPAI_VERSION="${{ inputs.paperclip_version }}" \
SMOKE_DETACH=true \
SMOKE_METADATA_FILE="$metadata_file" \
./scripts/docker-onboard-smoke.sh
set -a
source "$metadata_file"
set +a
{
echo "SMOKE_BASE_URL=$SMOKE_BASE_URL"
echo "SMOKE_ADMIN_EMAIL=$SMOKE_ADMIN_EMAIL"
echo "SMOKE_ADMIN_PASSWORD=$SMOKE_ADMIN_PASSWORD"
echo "SMOKE_CONTAINER_NAME=$SMOKE_CONTAINER_NAME"
echo "SMOKE_DATA_DIR=$SMOKE_DATA_DIR"
echo "SMOKE_IMAGE_NAME=$SMOKE_IMAGE_NAME"
echo "SMOKE_PAPERCLIPAI_VERSION=$SMOKE_PAPERCLIPAI_VERSION"
echo "SMOKE_METADATA_FILE=$metadata_file"
} >> "$GITHUB_ENV"
- name: Run release smoke Playwright suite
env:
PAPERCLIP_RELEASE_SMOKE_BASE_URL: ${{ env.SMOKE_BASE_URL }}
PAPERCLIP_RELEASE_SMOKE_EMAIL: ${{ env.SMOKE_ADMIN_EMAIL }}
PAPERCLIP_RELEASE_SMOKE_PASSWORD: ${{ env.SMOKE_ADMIN_PASSWORD }}
run: pnpm run test:release-smoke
- name: Capture Docker logs
if: always()
run: |
if [[ -n "${SMOKE_CONTAINER_NAME:-}" ]]; then
docker logs "$SMOKE_CONTAINER_NAME" >"$RUNNER_TEMP/docker-onboard-smoke.log" 2>&1 || true
fi
- name: Upload diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.artifact_name }}
path: |
${{ runner.temp }}/docker-onboard-smoke.log
${{ env.SMOKE_METADATA_FILE }}
tests/release-smoke/playwright-report/
tests/release-smoke/test-results/
retention-days: 14
- name: Stop Docker smoke container
if: always()
run: |
if [[ -n "${SMOKE_CONTAINER_NAME:-}" ]]; then
docker rm -f "$SMOKE_CONTAINER_NAME" >/dev/null 2>&1 || true
fi

261
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,261 @@
name: Release
on:
push:
branches:
- master
workflow_dispatch:
inputs:
source_ref:
description: Commit SHA, branch, or tag to publish as stable
required: true
type: string
default: master
stable_date:
description: Enter a UTC date in YYYY-MM-DD format, for example 2026-03-18. Do not enter a version string. The workflow will resolve that date to a stable version such as 2026.318.0, then 2026.318.1 for the next same-day stable.
required: false
type: string
dry_run:
description: Preview the stable release without publishing
required: true
type: boolean
default: false
concurrency:
group: release-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: false
jobs:
verify_canary:
if: github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Typecheck
run: pnpm -r typecheck
- name: Run tests
run: pnpm test:run
- name: Build
run: pnpm build
publish_canary:
if: github.event_name == 'push'
needs: verify_canary
runs-on: ubuntu-latest
timeout-minutes: 45
environment: npm-canary
permissions:
contents: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Restore tracked install-time changes
run: git checkout -- pnpm-lock.yaml
- name: Configure git author
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Publish canary
env:
GITHUB_ACTIONS: "true"
run: ./scripts/release.sh canary --skip-verify
- name: Push canary tag
run: |
tag="$(git tag --points-at HEAD | grep '^canary/v' | head -1)"
if [ -z "$tag" ]; then
echo "Error: no canary tag points at HEAD after release." >&2
exit 1
fi
git push origin "refs/tags/${tag}"
verify_stable:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.source_ref }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Typecheck
run: pnpm -r typecheck
- name: Run tests
run: pnpm test:run
- name: Build
run: pnpm build
preview_stable:
if: github.event_name == 'workflow_dispatch' && inputs.dry_run
needs: verify_stable
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.source_ref }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Dry-run stable release
env:
GITHUB_ACTIONS: "true"
run: |
args=(stable --skip-verify --dry-run)
if [ -n "${{ inputs.stable_date }}" ]; then
args+=(--date "${{ inputs.stable_date }}")
fi
./scripts/release.sh "${args[@]}"
publish_stable:
if: github.event_name == 'workflow_dispatch' && !inputs.dry_run
needs: verify_stable
runs-on: ubuntu-latest
timeout-minutes: 45
environment: npm-stable
permissions:
contents: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.source_ref }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Restore tracked install-time changes
run: git checkout -- pnpm-lock.yaml
- name: Configure git author
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Publish stable
env:
GITHUB_ACTIONS: "true"
run: |
args=(stable --skip-verify)
if [ -n "${{ inputs.stable_date }}" ]; then
args+=(--date "${{ inputs.stable_date }}")
fi
./scripts/release.sh "${args[@]}"
- name: Push stable tag
run: |
tag="$(git tag --points-at HEAD | grep '^v' | head -1)"
if [ -z "$tag" ]; then
echo "Error: no stable tag points at HEAD after release." >&2
exit 1
fi
git push origin "refs/tags/${tag}"
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
PUBLISH_REMOTE: origin
run: |
version="$(git tag --points-at HEAD | grep '^v' | head -1 | sed 's/^v//')"
if [ -z "$version" ]; then
echo "Error: no v* tag points at HEAD after stable release." >&2
exit 1
fi
./scripts/create-github-release.sh "$version"

16
.gitignore vendored
View File

@@ -31,9 +31,23 @@ server/src/**/*.js.map
server/src/**/*.d.ts
server/src/**/*.d.ts.map
tmp/
feedback-export-*
# Editor / tool temp files
*.tmp
.vscode/
.claude/settings.local.json
.paperclip-local/
.paperclip-local/
/.idea/
/.agents/
# Doc maintenance cursor
.doc-review-cursor
# Playwright
tests/e2e/test-results/
tests/e2e/playwright-report/
tests/release-smoke/test-results/
tests/release-smoke/playwright-report/
.superset/
.claude/worktrees/

View File

@@ -1 +1,3 @@
Dotta <bippadotta@protonmail.com> Forgotten <forgottenrunes@protonmail.com>
Dotta <bippadotta@protonmail.com> <34892728+cryppadotta@users.noreply.github.com>
Dotta <bippadotta@protonmail.com> <forgottenrunes@protonmail.com>
Dotta <bippadotta@protonmail.com> <dotta@example.com>

View File

@@ -26,6 +26,9 @@ Before making changes, read in this order:
- `ui/`: React + Vite board UI
- `packages/db/`: Drizzle schema, migrations, DB clients
- `packages/shared/`: shared types, constants, validators, API path constants
- `packages/adapters/`: agent adapter implementations (Claude, Codex, Cursor, etc.)
- `packages/adapter-utils/`: shared adapter utilities
- `packages/plugins/`: plugin system packages
- `doc/`: operational and product docs
## 4. Dev Setup (Auto DB)
@@ -78,6 +81,9 @@ If you change schema/API behavior, update all impacted layers:
4. Do not replace strategic docs wholesale unless asked.
Prefer additive updates. Keep `doc/SPEC.md` and `doc/SPEC-implementation.md` aligned.
5. Keep repo plan docs dated and centralized.
When you are creating a plan file in the repository itself, new plan documents belong in `doc/plans/` and should use `YYYY-MM-DD-slug.md` filenames. This does not replace Paperclip issue planning: if a Paperclip issue asks for a plan, update the issue `plan` document per the `paperclip` skill instead of creating a repo markdown file.
## 6. Database Change Workflow
When changing data model:
@@ -132,7 +138,18 @@ When adding endpoints:
- Use company selection context for company-scoped pages
- Surface failures clearly; do not silently ignore API errors
## 10. Definition of Done
## 10. Pull Request Requirements
When creating a pull request (via `gh pr create` or any other method), you **must** read and fill in every section of [`.github/PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md). Do not craft ad-hoc PR bodies — use the template as the structure for your PR description. Required sections:
- **Thinking Path** — trace reasoning from project context to this change (see `CONTRIBUTING.md` for examples)
- **What Changed** — bullet list of concrete changes
- **Verification** — how a reviewer can confirm it works
- **Risks** — what could go wrong
- **Model Used** — the AI model that produced or assisted with the change (provider, exact model ID, context window, capabilities). Write "None — human-authored" if no AI was used.
- **Checklist** — all items checked
## 11. Definition of Done
A change is done when all are true:
@@ -140,3 +157,45 @@ A change is done when all are true:
2. Typecheck, tests, and build pass
3. Contracts are synced across db/shared/server/ui
4. Docs updated when behavior or commands change
5. PR description follows the [PR template](.github/PULL_REQUEST_TEMPLATE.md) with all sections filled in (including Model Used)
## 11. Fork-Specific: HenkDz/paperclip
This is a fork of `paperclipai/paperclip` with QoL patches and an **external-only** Hermes adapter story on branch `feat/externalize-hermes-adapter` ([tree](https://github.com/HenkDz/paperclip/tree/feat/externalize-hermes-adapter)).
### Branch Strategy
- `feat/externalize-hermes-adapter` → core has **no** `hermes-paperclip-adapter` dependency and **no** built-in `hermes_local` registration. Install Hermes via the Adapter Plugin manager (`@henkey/hermes-paperclip-adapter` or a `file:` path).
- Older fork branches may still document built-in Hermes; treat this file as authoritative for the externalize branch.
### Hermes (plugin only)
- Register through **Board → Adapter manager** (same as Droid). Type remains `hermes_local` once the package is loaded.
- UI uses generic **config-schema** + **ui-parser.js** from the package — no Hermes imports in `server/` or `ui/` source.
- Optional: `file:` entry in `~/.paperclip/adapter-plugins.json` for local dev of the adapter repo.
### Local Dev
- Fork runs on port 3101+ (auto-detects if 3100 is taken by upstream instance)
- `npx vite build` hangs on NTFS — use `node node_modules/vite/bin/vite.js build` instead
- Server startup from NTFS takes 30-60s — don't assume failure immediately
- Kill ALL paperclip processes before starting: `pkill -f "paperclip"; pkill -f "tsx.*index.ts"`
- Vite cache survives `rm -rf dist` — delete both: `rm -rf ui/dist ui/node_modules/.vite`
### Fork QoL Patches (not in upstream)
These are local modifications in the fork's UI. If re-copying source, these must be re-applied:
1. **stderr_group** — amber accordion for MCP init noise in `RunTranscriptView.tsx`
2. **tool_group** — accordion for consecutive non-terminal tools (write, read, search, browser)
3. **Dashboard excerpt**`LatestRunCard` strips markdown, shows first 3 lines/280 chars
### Plugin System
PR #2218 (`feat/external-adapter-phase1`) adds external adapter support. See root `AGENTS.md` for full details.
- Adapters can be loaded as external plugins via `~/.paperclip/adapter-plugins.json`
- The plugin-loader should have ZERO hardcoded adapter imports — pure dynamic loading
- `createServerAdapter()` must include ALL optional fields (especially `detectModel`)
- Built-in UI adapters can shadow external plugin parsers — remove built-in when fully externalizing
- Reference external adapters: Hermes (`@henkey/hermes-paperclip-adapter` or `file:`) and Droid (npm)

94
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,94 @@
# Contributing Guide
Thanks for wanting to contribute!
We really appreciate both small fixes and thoughtful larger changes.
## Two Paths to Get Your Pull Request Accepted
### Path 1: Small, Focused Changes (Fastest way to get merged)
- Pick **one** clear thing to fix/improve
- Touch the **smallest possible number of files**
- Make sure the change is very targeted and easy to review
- All tests pass and CI is green
- Greptile score is 5/5 with all comments addressed
- Use the [PR template](.github/PULL_REQUEST_TEMPLATE.md)
These almost always get merged quickly when they're clean.
### Path 2: Bigger or Impactful Changes
- **First** talk about it in Discord → #dev channel
→ Describe what you're trying to solve
→ Share rough ideas / approach
- Once there's rough agreement, build it
- In your PR include:
- Before / After screenshots (or short video if UI/behavior change)
- Clear description of what & why
- Proof it works (manual testing notes)
- All tests passing and CI green
- Greptile score 5/5 with all comments addressed
- [PR template](.github/PULL_REQUEST_TEMPLATE.md) fully filled out
PRs that follow this path are **much** more likely to be accepted, even when they're large.
## PR Requirements (all PRs)
### Use the PR Template
Every pull request **must** follow the PR template at [`.github/PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md). If you create a PR via the GitHub API or other tooling that bypasses the template, copy its contents into your PR description manually. The template includes required sections: Thinking Path, What Changed, Verification, Risks, Model Used, and a Checklist.
### Model Used (Required)
Every PR must include a **Model Used** section specifying which AI model produced or assisted with the change. Include the provider, exact model ID/version, context window size, and any relevant capability details (e.g., reasoning mode, tool use). If no AI was used, write "None — human-authored". This applies to all contributors — human and AI alike.
### Tests Must Pass
All tests must pass before a PR can be merged. Run them locally first and verify CI is green after pushing.
### Greptile Review
We use [Greptile](https://greptile.com) for automated code review. Your PR must achieve a **5/5 Greptile score** with **all Greptile comments addressed** before it can be merged. If Greptile leaves comments, fix or respond to each one and request a re-review.
## General Rules (both paths)
- Write clear commit messages
- Keep PR title + description meaningful
- One PR = one logical change (unless it's a small related group)
- Run tests locally first
- Be kind in discussions 😄
## Writing a Good PR message
Your PR description must follow the [PR template](.github/PULL_REQUEST_TEMPLATE.md). All sections are required. The "thinking path" at the top explains from the top of the project down to what you fixed. E.g.:
### Thinking Path Example 1:
> - Paperclip orchestrates ai-agents for zero-human companies
> - There are many types of adapters for each LLM model provider
> - But LLM's have a context limit and not all agents can automatically compact their context
> - So we need to have an adapter-specific configuration for which adapters can and cannot automatically compact their context
> - This pull request adds per-adapter configuration of compaction, either auto or paperclip managed
> - That way we can get optimal performance from any adapter/provider in Paperclip
### Thinking Path Example 2:
> - Paperclip orchestrates ai-agents for zero-human companies
> - But humans want to watch the agents and oversee their work
> - Human users also operate in teams and so they need their own logins, profiles, views etc.
> - So we have a multi-user system for humans
> - But humans want to be able to update their own profile picture and avatar
> - But the avatar upload form wasn't saving the avatar to the file storage system
> - So this PR fixes the avatar upload form to use the file storage service
> - The benefit is we don't have a one-off file storage for just one aspect of the system, which would cause confusion and extra configuration
Then have the rest of your normal PR message after the Thinking Path.
This should include details about what you did, why you did it, why it matters & the benefits, how we can verify it works, and any risks.
Please include screenshots if possible if you have a visible change. (use something like the [agent-browser skill](https://github.com/vercel-labs/agent-browser/blob/main/skills/agent-browser/SKILL.md) or similar to take screenshots). Ideally, you include before and after screenshots.
Questions? Just ask in #dev — we're happy to help.
Happy hacking!

View File

@@ -1,8 +1,23 @@
FROM node:20-bookworm-slim AS base
FROM node:lts-trixie-slim AS base
ARG USER_UID=1000
ARG USER_GID=1000
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl git \
&& rm -rf /var/lib/apt/lists/*
RUN corepack enable
&& apt-get install -y --no-install-recommends ca-certificates gosu curl git wget ripgrep python3 \
&& mkdir -p -m 755 /etc/apt/keyrings \
&& wget -nv -O/etc/apt/keyrings/githubcli-archive-keyring.gpg https://cli.github.com/packages/githubcli-archive-keyring.gpg \
&& echo "20e0125d6f6e077a9ad46f03371bc26d90b04939fb95170f5a1905099cc6bcc0 /etc/apt/keyrings/githubcli-archive-keyring.gpg" | sha256sum -c - \
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& mkdir -p -m 755 /etc/apt/sources.list.d \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh \
&& rm -rf /var/lib/apt/lists/* \
&& corepack enable
# Modify the existing node user/group to have the specified UID/GID to match host user
RUN usermod -u $USER_UID --non-unique node \
&& groupmod -g $USER_GID --non-unique node \
&& usermod -g $USER_GID -d /paperclip node
FROM base AS deps
WORKDIR /app
@@ -13,21 +28,39 @@ COPY ui/package.json ui/
COPY packages/shared/package.json packages/shared/
COPY packages/db/package.json packages/db/
COPY packages/adapter-utils/package.json packages/adapter-utils/
COPY packages/mcp-server/package.json packages/mcp-server/
COPY packages/adapters/claude-local/package.json packages/adapters/claude-local/
COPY packages/adapters/codex-local/package.json packages/adapters/codex-local/
COPY packages/adapters/cursor-local/package.json packages/adapters/cursor-local/
COPY packages/adapters/gemini-local/package.json packages/adapters/gemini-local/
COPY packages/adapters/openclaw-gateway/package.json packages/adapters/openclaw-gateway/
COPY packages/adapters/opencode-local/package.json packages/adapters/opencode-local/
COPY packages/adapters/pi-local/package.json packages/adapters/pi-local/
COPY packages/plugins/sdk/package.json packages/plugins/sdk/
COPY patches/ patches/
RUN pnpm install --frozen-lockfile
FROM base AS build
WORKDIR /app
COPY --from=deps /app /app
COPY . .
RUN pnpm --filter @paperclip/ui build
RUN pnpm --filter @paperclip/server build
RUN pnpm --filter @paperclipai/ui build
RUN pnpm --filter @paperclipai/plugin-sdk build
RUN pnpm --filter @paperclipai/server build
RUN test -f server/dist/index.js || (echo "ERROR: server build output missing" && exit 1)
FROM base AS production
ARG USER_UID=1000
ARG USER_GID=1000
WORKDIR /app
COPY --from=build /app /app
RUN npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest
COPY --chown=node:node --from=build /app /app
RUN npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest opencode-ai \
&& mkdir -p /paperclip \
&& chown node:node /paperclip
COPY scripts/docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENV NODE_ENV=production \
HOME=/paperclip \
@@ -36,11 +69,15 @@ ENV NODE_ENV=production \
SERVE_UI=true \
PAPERCLIP_HOME=/paperclip \
PAPERCLIP_INSTANCE_ID=default \
USER_UID=${USER_UID} \
USER_GID=${USER_GID} \
PAPERCLIP_CONFIG=/paperclip/instances/default/config.json \
PAPERCLIP_DEPLOYMENT_MODE=local_trusted \
PAPERCLIP_DEPLOYMENT_EXPOSURE=private
PAPERCLIP_DEPLOYMENT_MODE=authenticated \
PAPERCLIP_DEPLOYMENT_EXPOSURE=private \
OPENCODE_ALLOW_ALL_MODELS=true
VOLUME ["/paperclip"]
EXPOSE 3100
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["node", "--import", "./server/node_modules/tsx/dist/loader.mjs", "server/dist/index.js"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Paperclip AI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -4,15 +4,15 @@
<p align="center">
<a href="#quickstart"><strong>Quickstart</strong></a> &middot;
<a href="https://paperclip.dev/docs"><strong>Docs</strong></a> &middot;
<a href="https://paperclip.ing/docs"><strong>Docs</strong></a> &middot;
<a href="https://github.com/paperclipai/paperclip"><strong>GitHub</strong></a> &middot;
<a href="https://discord.gg/paperclip"><strong>Discord</strong></a>
<a href="https://discord.gg/m4HZY7xNG3"><strong>Discord</strong></a>
</p>
<p align="center">
<a href="https://github.com/paperclipai/paperclip/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License" /></a>
<a href="https://github.com/paperclipai/paperclip/stargazers"><img src="https://img.shields.io/github/stars/paperclipai/paperclip?style=flat" alt="Stars" /></a>
<a href="https://discord.gg/paperclip"><img src="https://img.shields.io/discord/000000000?label=discord" alt="Discord" /></a>
<a href="https://discord.gg/m4HZY7xNG3"><img src="https://img.shields.io/discord/000000000?label=discord" alt="Discord" /></a>
</p>
<br/>
@@ -174,9 +174,19 @@ Paperclip handles the hard orchestration details correctly.
Open source. Self-hosted. No Paperclip account required.
```bash
npx paperclipai onboard
npx paperclipai onboard --yes
```
That quickstart path now defaults to trusted local loopback mode for the fastest first run. To start in authenticated/private mode instead, choose a bind preset explicitly:
```bash
npx paperclipai onboard --yes --bind lan
# or:
npx paperclipai onboard --yes --bind tailnet
```
If you already have Paperclip configured, rerunning `onboard` keeps the existing config in place. Use `paperclipai configure` to edit settings.
Or manually:
```bash
@@ -218,7 +228,8 @@ By default, agents run on scheduled heartbeats and event-based triggers (task as
## Development
```bash
pnpm dev # Full dev (API + UI)
pnpm dev # Full dev (API + UI, watch mode)
pnpm dev:once # Full dev without file watching
pnpm dev:server # Server only
pnpm build # Build all
pnpm typecheck # Type checking
@@ -233,23 +244,56 @@ See [doc/DEVELOPING.md](doc/DEVELOPING.md) for the full development guide.
## Roadmap
- 🛒 **Clipmart** — Download and share entire company architectures
- 🧩 **Plugin System** — Embed custom plugins (e.g. Reporting, Knowledge Base) into Paperclip
- ☁️ **Cloud Agent Adapters** — Add more adapters for cloud-hosted agents
- ✅ Plugin system (e.g. add a knowledge base, custom tracing, queues, etc)
- ✅ Get OpenClaw / claw-style agent employees
- ✅ companies.sh - import and export entire organizations
- ✅ Easy AGENTS.md configurations
- ✅ Skills Manager
- ✅ Scheduled Routines
- ✅ Better Budgeting
- ✅ Agent Reviews and Approvals
- ⚪ Multiple Human Users
- ⚪ Cloud / Sandbox agents (e.g. Cursor / e2b agents)
- ⚪ Artifacts & Work Products
- ⚪ Memory & Knowledge
- ⚪ Enforced Outcomes
- ⚪ MAXIMIZER MODE
- ⚪ Deep Planning
- ⚪ Work Queues
- ⚪ Self-Organization
- ⚪ Automatic Organizational Learning
- ⚪ CEO Chat
- ⚪ Cloud deployments
- ⚪ Desktop App
<br/>
## Community & Plugins
Find Plugins and more at [awesome-paperclip](https://github.com/gsxdsm/awesome-paperclip)
## Telemetry
Paperclip collects anonymous usage telemetry to help us understand how the product is used and improve it. No personal information, issue content, prompts, file paths, or secrets are ever collected. Private repository references are hashed with a per-install salt before being sent.
Telemetry is **enabled by default** and can be disabled with any of the following:
| Method | How |
| -------------------- | ------------------------------------------------------- |
| Environment variable | `PAPERCLIP_TELEMETRY_DISABLED=1` |
| Standard convention | `DO_NOT_TRACK=1` |
| CI environments | Automatically disabled when `CI=true` |
| Config file | Set `telemetry.enabled: false` in your Paperclip config |
## Contributing
We welcome contributions. See the [contributing guide](CONTRIBUTING.md) for details.
<!-- TODO: add CONTRIBUTING.md -->
<br/>
## Community
- [Discord](#) — Coming soon
- [Discord](https://discord.gg/m4HZY7xNG3) — Join the community
- [GitHub Issues](https://github.com/paperclipai/paperclip/issues) — bugs and feature requests
- [GitHub Discussions](https://github.com/paperclipai/paperclip/discussions) — ideas and RFC
@@ -259,6 +303,10 @@ We welcome contributions. See the [contributing guide](CONTRIBUTING.md) for deta
MIT &copy; 2026 Paperclip
## Star History
[![Star History Chart](https://api.star-history.com/image?repos=paperclipai/paperclip&type=date&legend=top-left)](https://www.star-history.com/?repos=paperclipai%2Fpaperclip&type=date&legend=top-left)
<br/>
---

8
SECURITY.md Normal file
View File

@@ -0,0 +1,8 @@
# Security Policy
## Reporting a Vulnerability
Please report security vulnerabilities through GitHub's Security Advisory feature:
[https://github.com/paperclipai/paperclip/security/advisories/new](https://github.com/paperclipai/paperclip/security/advisories/new)
Do not open public issues for security vulnerabilities.

143
adapter-plugin.md Normal file
View File

@@ -0,0 +1,143 @@
- Created branch: feat/external-adapter-phase1
I started phase 1 in the most merge-friendly way I could: small central changes, reusing existing registry patterns instead of inventing a whole new plugin system up front.
What I changed
1. Server adapter registry is now mutable
Files:
- server/src/adapters/registry.ts
- server/src/adapters/index.ts
Added:
- registerServerAdapter(adapter)
- unregisterServerAdapter(type)
- requireServerAdapter(type)
Kept the existing built-in registry shape, but changed initialization so built-ins are registered into a mutable map on startup.
Why this is merge-friendly:
- existing built-in adapter definitions stay where they already are
- existing lookup helpers still exist
- no big architectural rewrite yet
1. Runtime adapter validation moved to server routes
File:
- server/src/routes/agents.ts
Added:
- assertKnownAdapterType(...)
Used it in:
- /companies/:companyId/adapters/:type/models
- /companies/:companyId/adapters/:type/detect-model
- /companies/:companyId/adapters/:type/test-environment
- POST /companies/:companyId/agents
- POST /companies/:companyId/agent-hires
- PATCH /agents/:id when adapterType is touched
Why:
- shared schemas can now allow external adapter strings
- server becomes the real source of truth for “is this adapter actually registered?”
1. Shared adapterType validation is now open-ended for inputs
Files:
- packages/shared/src/adapter-type.ts
- packages/shared/src/validators/agent.ts
- packages/shared/src/validators/access.ts
- packages/shared/src/index.ts
Changed input validation from hardcoded z.enum(AGENT_ADAPTER_TYPES) to:
- agentAdapterTypeSchema
- optionalAgentAdapterTypeSchema
These accept any non-empty string.
Important:
- I did not remove AGENT_ADAPTER_TYPES from constants
- I did not change the built-in type unions yet
- this keeps the change minimal and limits blast radius
So:
- input payloads can carry external adapter types
- actual acceptance still depends on server registry validation
1. UI adapter registry is now mutable too
Files:
- ui/src/adapters/registry.ts
- ui/src/adapters/index.ts
Added:
- registerUIAdapter(adapter)
- unregisterUIAdapter(type)
- findUIAdapter(type)
Built-ins are still registered from the same file, same pattern as server.
1. Began switching UI adapter lists toward the registry
Files:
- ui/src/adapters/metadata.ts
- ui/src/components/AgentConfigForm.tsx
- ui/src/pages/NewAgent.tsx
Changed:
- AgentConfigForm adapter list now comes from registered UI adapters via metadata helper
- NewAgent supported adapter set now derives from listUIAdapters()
Why:
- phase 1 should start reducing hardcoded UI lists
- but without rewriting every page at once
1. Tests added first, then implementation
New tests:
- server/src/__tests__/adapter-registry.test.ts
- server/src/__tests__/agent-adapter-validation-routes.test.ts
- ui/src/adapters/registry.test.ts
- packages/shared/src/adapter-types.test.ts
Small test mock fix:
- server/src/__tests__/agent-skills-routes.test.ts
Current status
The branch is created and the phase-1 code is started.
I also followed TDD in spirit:
- wrote failing tests first for missing register/unregister APIs
- then implemented the registry changes
What is not finished yet
I have not finished the green/stabilization pass yet.
I still need to:
- run the targeted tests again after the implementation changes
- fix any TS / mocking fallout
- decide how far to take phase-1 UI wiring in this branch before stopping
Also, I intentionally did not yet touch the bigger hardcoded surfaces like:
- NewAgentDialog
- OnboardingWizard
- InviteLanding
- duplicated adapterLabels maps
- plugin manifest / plugin loader adapter contributions
That is deliberate.
For merge safety, I started with the smallest useful seam:
- dynamic server registry
- dynamic UI registry
- runtime validation based on registry
- first couple of UI consumers
One note
There was already an unrelated dirty change in:
- server/scripts/dev-watch.ts
I left that alone and did not make phase-1 changes there.
Recommended next move
I should keep going on this same branch and do the stabilization pass:
1. rerun server/ui/shared targeted tests
2. fix failures
3. run typechecks
4. then extend the same registry-driven approach to the next safest UI surfaces

View File

@@ -1,5 +1,128 @@
# paperclipai
## 0.3.1
### Patch Changes
- Stable release preparation for 0.3.1
- Updated dependencies
- @paperclipai/adapter-utils@0.3.1
- @paperclipai/adapter-claude-local@0.3.1
- @paperclipai/adapter-codex-local@0.3.1
- @paperclipai/adapter-cursor-local@0.3.1
- @paperclipai/adapter-gemini-local@0.3.1
- @paperclipai/adapter-openclaw-gateway@0.3.1
- @paperclipai/adapter-opencode-local@0.3.1
- @paperclipai/adapter-pi-local@0.3.1
- @paperclipai/db@0.3.1
- @paperclipai/shared@0.3.1
- @paperclipai/server@0.3.1
## 0.3.0
### Minor Changes
- Stable release preparation for 0.3.0
### Patch Changes
- Updated dependencies [6077ae6]
- Updated dependencies
- @paperclipai/shared@0.3.0
- @paperclipai/adapter-utils@0.3.0
- @paperclipai/adapter-claude-local@0.3.0
- @paperclipai/adapter-codex-local@0.3.0
- @paperclipai/adapter-cursor-local@0.3.0
- @paperclipai/adapter-openclaw-gateway@0.3.0
- @paperclipai/adapter-opencode-local@0.3.0
- @paperclipai/adapter-pi-local@0.3.0
- @paperclipai/db@0.3.0
- @paperclipai/server@0.3.0
## 0.2.7
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.7
- @paperclipai/adapter-utils@0.2.7
- @paperclipai/db@0.2.7
- @paperclipai/adapter-claude-local@0.2.7
- @paperclipai/adapter-codex-local@0.2.7
- @paperclipai/adapter-openclaw@0.2.7
- @paperclipai/server@0.2.7
## 0.2.6
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.6
- @paperclipai/adapter-utils@0.2.6
- @paperclipai/db@0.2.6
- @paperclipai/adapter-claude-local@0.2.6
- @paperclipai/adapter-codex-local@0.2.6
- @paperclipai/adapter-openclaw@0.2.6
- @paperclipai/server@0.2.6
## 0.2.5
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.5
- @paperclipai/adapter-utils@0.2.5
- @paperclipai/db@0.2.5
- @paperclipai/adapter-claude-local@0.2.5
- @paperclipai/adapter-codex-local@0.2.5
- @paperclipai/adapter-openclaw@0.2.5
- @paperclipai/server@0.2.5
## 0.2.4
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.4
- @paperclipai/adapter-utils@0.2.4
- @paperclipai/db@0.2.4
- @paperclipai/adapter-claude-local@0.2.4
- @paperclipai/adapter-codex-local@0.2.4
- @paperclipai/adapter-openclaw@0.2.4
- @paperclipai/server@0.2.4
## 0.2.3
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.3
- @paperclipai/adapter-utils@0.2.3
- @paperclipai/db@0.2.3
- @paperclipai/adapter-claude-local@0.2.3
- @paperclipai/adapter-codex-local@0.2.3
- @paperclipai/adapter-openclaw@0.2.3
- @paperclipai/server@0.2.3
## 0.2.2
### Patch Changes
- Version bump (patch)
- Updated dependencies
- @paperclipai/shared@0.2.2
- @paperclipai/adapter-utils@0.2.2
- @paperclipai/db@0.2.2
- @paperclipai/adapter-claude-local@0.2.2
- @paperclipai/adapter-codex-local@0.2.2
- @paperclipai/adapter-openclaw@0.2.2
- @paperclipai/server@0.2.2
## 0.2.1
### Patch Changes

300
cli/README.md Normal file
View File

@@ -0,0 +1,300 @@
<p align="center">
<img src="https://raw.githubusercontent.com/paperclipai/paperclip/master/doc/assets/header.png" alt="Paperclip — runs your business" width="720" />
</p>
<p align="center">
<a href="#quickstart"><strong>Quickstart</strong></a> &middot;
<a href="https://paperclip.ing/docs"><strong>Docs</strong></a> &middot;
<a href="https://github.com/paperclipai/paperclip"><strong>GitHub</strong></a> &middot;
<a href="https://discord.gg/m4HZY7xNG3"><strong>Discord</strong></a>
</p>
<p align="center">
<a href="https://github.com/paperclipai/paperclip/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License" /></a>
<a href="https://github.com/paperclipai/paperclip/stargazers"><img src="https://img.shields.io/github/stars/paperclipai/paperclip?style=flat" alt="Stars" /></a>
<a href="https://discord.gg/m4HZY7xNG3"><img src="https://img.shields.io/badge/discord-join%20chat-5865F2?logo=discord&logoColor=white" alt="Discord" /></a>
</p>
<br/>
<div align="center">
<video src="https://github.com/user-attachments/assets/773bdfb2-6d1e-4e30-8c5f-3487d5b70c8f" width="600" controls></video>
</div>
<br/>
## What is Paperclip?
# Open-source orchestration for zero-human companies
**If OpenClaw is an _employee_, Paperclip is the _company_**
Paperclip is a Node.js server and React UI that orchestrates a team of AI agents to run a business. Bring your own agents, assign goals, and track your agents' work and costs from one dashboard.
It looks like a task manager — but under the hood it has org charts, budgets, governance, goal alignment, and agent coordination.
**Manage business goals, not pull requests.**
| | Step | Example |
| ------ | --------------- | ------------------------------------------------------------------ |
| **01** | Define the goal | _"Build the #1 AI note-taking app to $1M MRR."_ |
| **02** | Hire the team | CEO, CTO, engineers, designers, marketers — any bot, any provider. |
| **03** | Approve and run | Review strategy. Set budgets. Hit go. Monitor from the dashboard. |
<br/>
> **COMING SOON: Clipmart** — Download and run entire companies with one click. Browse pre-built company templates — full org structures, agent configs, and skills — and import them into your Paperclip instance in seconds.
<br/>
<div align="center">
<table>
<tr>
<td align="center"><strong>Works<br/>with</strong></td>
<td align="center"><img src="https://raw.githubusercontent.com/paperclipai/paperclip/master/doc/assets/logos/openclaw.svg" width="32" alt="OpenClaw" /><br/><sub>OpenClaw</sub></td>
<td align="center"><img src="https://raw.githubusercontent.com/paperclipai/paperclip/master/doc/assets/logos/claude.svg" width="32" alt="Claude" /><br/><sub>Claude Code</sub></td>
<td align="center"><img src="https://raw.githubusercontent.com/paperclipai/paperclip/master/doc/assets/logos/codex.svg" width="32" alt="Codex" /><br/><sub>Codex</sub></td>
<td align="center"><img src="https://raw.githubusercontent.com/paperclipai/paperclip/master/doc/assets/logos/cursor.svg" width="32" alt="Cursor" /><br/><sub>Cursor</sub></td>
<td align="center"><img src="https://raw.githubusercontent.com/paperclipai/paperclip/master/doc/assets/logos/bash.svg" width="32" alt="Bash" /><br/><sub>Bash</sub></td>
<td align="center"><img src="https://raw.githubusercontent.com/paperclipai/paperclip/master/doc/assets/logos/http.svg" width="32" alt="HTTP" /><br/><sub>HTTP</sub></td>
</tr>
</table>
<em>If it can receive a heartbeat, it's hired.</em>
</div>
<br/>
## Paperclip is right for you if
- ✅ You want to build **autonomous AI companies**
- ✅ You **coordinate many different agents** (OpenClaw, Codex, Claude, Cursor) toward a common goal
- ✅ You have **20 simultaneous Claude Code terminals** open and lose track of what everyone is doing
- ✅ You want agents running **autonomously 24/7**, but still want to audit work and chime in when needed
- ✅ You want to **monitor costs** and enforce budgets
- ✅ You want a process for managing agents that **feels like using a task manager**
- ✅ You want to manage your autonomous businesses **from your phone**
<br/>
## Features
<table>
<tr>
<td align="center" width="33%">
<h3>🔌 Bring Your Own Agent</h3>
Any agent, any runtime, one org chart. If it can receive a heartbeat, it's hired.
</td>
<td align="center" width="33%">
<h3>🎯 Goal Alignment</h3>
Every task traces back to the company mission. Agents know <em>what</em> to do and <em>why</em>.
</td>
<td align="center" width="33%">
<h3>💓 Heartbeats</h3>
Agents wake on a schedule, check work, and act. Delegation flows up and down the org chart.
</td>
</tr>
<tr>
<td align="center">
<h3>💰 Cost Control</h3>
Monthly budgets per agent. When they hit the limit, they stop. No runaway costs.
</td>
<td align="center">
<h3>🏢 Multi-Company</h3>
One deployment, many companies. Complete data isolation. One control plane for your portfolio.
</td>
<td align="center">
<h3>🎫 Ticket System</h3>
Every conversation traced. Every decision explained. Full tool-call tracing and immutable audit log.
</td>
</tr>
<tr>
<td align="center">
<h3>🛡️ Governance</h3>
You're the board. Approve hires, override strategy, pause or terminate any agent — at any time.
</td>
<td align="center">
<h3>📊 Org Chart</h3>
Hierarchies, roles, reporting lines. Your agents have a boss, a title, and a job description.
</td>
<td align="center">
<h3>📱 Mobile Ready</h3>
Monitor and manage your autonomous businesses from anywhere.
</td>
</tr>
</table>
<br/>
## Problems Paperclip solves
| Without Paperclip | With Paperclip |
| ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| ❌ You have 20 Claude Code tabs open and can't track which one does what. On reboot you lose everything. | ✅ Tasks are ticket-based, conversations are threaded, sessions persist across reboots. |
| ❌ You manually gather context from several places to remind your bot what you're actually doing. | ✅ Context flows from the task up through the project and company goals — your agent always knows what to do and why. |
| ❌ Folders of agent configs are disorganized and you're re-inventing task management, communication, and coordination between agents. | ✅ Paperclip gives you org charts, ticketing, delegation, and governance out of the box — so you run a company, not a pile of scripts. |
| ❌ Runaway loops waste hundreds of dollars of tokens and max your quota before you even know what happened. | ✅ Cost tracking surfaces token budgets and throttles agents when they're out. Management prioritizes with budgets. |
| ❌ You have recurring jobs (customer support, social, reports) and have to remember to manually kick them off. | ✅ Heartbeats handle regular work on a schedule. Management supervises. |
| ❌ You have an idea, you have to find your repo, fire up Claude Code, keep a tab open, and babysit it. | ✅ Add a task in Paperclip. Your coding agent works on it until it's done. Management reviews their work. |
<br/>
## Why Paperclip is special
Paperclip handles the hard orchestration details correctly.
| | |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Atomic execution.** | Task checkout and budget enforcement are atomic, so no double-work and no runaway spend. |
| **Persistent agent state.** | Agents resume the same task context across heartbeats instead of restarting from scratch. |
| **Runtime skill injection.** | Agents can learn Paperclip workflows and project context at runtime, without retraining. |
| **Governance with rollback.** | Approval gates are enforced, config changes are revisioned, and bad changes can be rolled back safely. |
| **Goal-aware execution.** | Tasks carry full goal ancestry so agents consistently see the "why," not just a title. |
| **Portable company templates.** | Export/import orgs, agents, and skills with secret scrubbing and collision handling. |
| **True multi-company isolation.** | Every entity is company-scoped, so one deployment can run many companies with separate data and audit trails. |
<br/>
## What Paperclip is not
| | |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| **Not a chatbot.** | Agents have jobs, not chat windows. |
| **Not an agent framework.** | We don't tell you how to build agents. We tell you how to run a company made of them. |
| **Not a workflow builder.** | No drag-and-drop pipelines. Paperclip models companies — with org charts, goals, budgets, and governance. |
| **Not a prompt manager.** | Agents bring their own prompts, models, and runtimes. Paperclip manages the organization they work in. |
| **Not a single-agent tool.** | This is for teams. If you have one agent, you probably don't need Paperclip. If you have twenty — you definitely do. |
| **Not a code review tool.** | Paperclip orchestrates work, not pull requests. Bring your own review process. |
<br/>
## Quickstart
Open source. Self-hosted. No Paperclip account required.
```bash
npx paperclipai onboard --yes
```
That quickstart path now defaults to trusted local loopback mode for the fastest first run. To start in authenticated/private mode instead, choose a bind preset explicitly:
```bash
npx paperclipai onboard --yes --bind lan
# or:
npx paperclipai onboard --yes --bind tailnet
```
If you already have Paperclip configured, rerunning `onboard` keeps the existing config in place. Use `paperclipai configure` to edit settings.
Or manually:
```bash
git clone https://github.com/paperclipai/paperclip.git
cd paperclip
pnpm install
pnpm dev
```
This starts the API server at `http://localhost:3100`. An embedded PostgreSQL database is created automatically — no setup required.
> **Requirements:** Node.js 20+, pnpm 9.15+
<br/>
## FAQ
**What does a typical setup look like?**
Locally, a single Node.js process manages an embedded Postgres and local file storage. For production, point it at your own Postgres and deploy however you like. Configure projects, agents, and goals — the agents take care of the rest.
If you're a solo-entreprenuer you can use Tailscale to access Paperclip on the go. Then later you can deploy to e.g. Vercel when you need it.
**Can I run multiple companies?**
Yes. A single deployment can run an unlimited number of companies with complete data isolation.
**How is Paperclip different from agents like OpenClaw or Claude Code?**
Paperclip _uses_ those agents. It orchestrates them into a company — with org charts, budgets, goals, governance, and accountability.
**Why should I use Paperclip instead of just pointing my OpenClaw to Asana or Trello?**
Agent orchestration has subtleties in how you coordinate who has work checked out, how to maintain sessions, monitoring costs, establishing governance - Paperclip does this for you.
(Bring-your-own-ticket-system is on the Roadmap)
**Do agents run continuously?**
By default, agents run on scheduled heartbeats and event-based triggers (task assignment, @-mentions). You can also hook in continuous agents like OpenClaw. You bring your agent and Paperclip coordinates.
<br/>
## Development
```bash
pnpm dev # Full dev (API + UI, watch mode)
pnpm dev:once # Full dev without file watching
pnpm dev:server # Server only
pnpm build # Build all
pnpm typecheck # Type checking
pnpm test:run # Run tests
pnpm db:generate # Generate DB migration
pnpm db:migrate # Apply migrations
```
See [doc/DEVELOPING.md](https://github.com/paperclipai/paperclip/blob/master/doc/DEVELOPING.md) for the full development guide.
<br/>
## Roadmap
- ✅ Plugin system (e.g. add a knowledge base, custom tracing, queues, etc)
- ✅ Get OpenClaw / claw-style agent employees
- ✅ companies.sh - import and export entire organizations
- ✅ Easy AGENTS.md configurations
- ✅ Skills Manager
- ✅ Scheduled Routines
- ✅ Better Budgeting
- ⚪ Artifacts & Deployments
- ⚪ CEO Chat
- ⚪ MAXIMIZER MODE
- ⚪ Multiple Human Users
- ⚪ Cloud / Sandbox agents (e.g. Cursor / e2b agents)
- ⚪ Cloud deployments
- ⚪ Desktop App
<br/>
## Community & Plugins
Find Plugins and more at [awesome-paperclip](https://github.com/gsxdsm/awesome-paperclip)
## Contributing
We welcome contributions. See the [contributing guide](https://github.com/paperclipai/paperclip/blob/master/CONTRIBUTING.md) for details.
<br/>
## Community
- [Discord](https://discord.gg/m4HZY7xNG3) — Join the community
- [GitHub Issues](https://github.com/paperclipai/paperclip/issues) — bugs and feature requests
- [GitHub Discussions](https://github.com/paperclipai/paperclip/discussions) — ideas and RFC
<br/>
## License
MIT &copy; 2026 Paperclip
## Star History
[![Star History Chart](https://api.star-history.com/image?repos=paperclipai/paperclip&type=date&legend=top-left)](https://www.star-history.com/?repos=paperclipai%2Fpaperclip&type=date&legend=top-left)
<br/>
---
<p align="center">
<img src="https://raw.githubusercontent.com/paperclipai/paperclip/master/doc/assets/footer.jpg" alt="" width="720" />
</p>
<p align="center">
<sub>Open source under MIT. Built for people who want to run companies, not babysit agents.</sub>
</p>

View File

@@ -21,7 +21,7 @@ const workspacePaths = [
"packages/adapter-utils",
"packages/adapters/claude-local",
"packages/adapters/codex-local",
"packages/adapters/openclaw",
"packages/adapters/openclaw-gateway",
];
// Workspace packages that should NOT be bundled — they'll be published

View File

@@ -1,6 +1,6 @@
{
"name": "paperclipai",
"version": "0.2.1",
"version": "0.3.1",
"description": "Paperclip CLI — orchestrate AI agent teams to run a business",
"type": "module",
"bin": {
@@ -16,10 +16,13 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/paperclipai/paperclip.git",
"url": "https://github.com/paperclipai/paperclip",
"directory": "cli"
},
"homepage": "https://github.com/paperclipai/paperclip",
"bugs": {
"url": "https://github.com/paperclipai/paperclip/issues"
},
"files": [
"dist"
],
@@ -36,7 +39,11 @@
"@clack/prompts": "^0.10.0",
"@paperclipai/adapter-claude-local": "workspace:*",
"@paperclipai/adapter-codex-local": "workspace:*",
"@paperclipai/adapter-openclaw": "workspace:*",
"@paperclipai/adapter-cursor-local": "workspace:*",
"@paperclipai/adapter-gemini-local": "workspace:*",
"@paperclipai/adapter-opencode-local": "workspace:*",
"@paperclipai/adapter-pi-local": "workspace:*",
"@paperclipai/adapter-openclaw-gateway": "workspace:*",
"@paperclipai/adapter-utils": "workspace:*",
"@paperclipai/db": "workspace:*",
"@paperclipai/server": "workspace:*",
@@ -44,6 +51,7 @@
"drizzle-orm": "0.38.4",
"dotenv": "^17.0.1",
"commander": "^13.1.0",
"embedded-postgres": "^18.1.0-beta.16",
"picocolors": "^1.1.1"
},
"devDependencies": {

View File

@@ -4,7 +4,9 @@ import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
ensureAgentJwtSecret,
mergePaperclipEnvEntries,
readAgentJwtSecretFromEnv,
readPaperclipEnvEntries,
resolveAgentJwtEnvFile,
} from "../config/env.js";
import { agentJwtSecretCheck } from "../checks/agent-jwt-secret-check.js";
@@ -58,4 +60,20 @@ describe("agent jwt env helpers", () => {
const result = agentJwtSecretCheck(configPath);
expect(result.status).toBe("pass");
});
it("quotes hash-prefixed env values so dotenv round-trips them", () => {
const configPath = tempConfigPath();
const envPath = resolveAgentJwtEnvFile(configPath);
mergePaperclipEnvEntries(
{
PAPERCLIP_WORKTREE_COLOR: "#439edb",
},
envPath,
);
const contents = fs.readFileSync(envPath, "utf-8");
expect(contents).toContain('PAPERCLIP_WORKTREE_COLOR="#439edb"');
expect(readPaperclipEnvEntries(envPath).PAPERCLIP_WORKTREE_COLOR).toBe("#439edb");
});
});

View File

@@ -21,6 +21,12 @@ function writeBaseConfig(configPath: string) {
mode: "embedded-postgres",
embeddedPostgresDataDir: "/tmp/paperclip-db",
embeddedPostgresPort: 54329,
backup: {
enabled: true,
intervalMinutes: 60,
retentionDays: 30,
dir: "/tmp/paperclip-backups",
},
},
logging: {
mode: "file",
@@ -36,6 +42,10 @@ function writeBaseConfig(configPath: string) {
},
auth: {
baseUrlMode: "auto",
disableSignUp: false,
},
telemetry: {
enabled: true,
},
storage: {
provider: "local_disk",
@@ -68,4 +78,3 @@ describe("allowed-hostname command", () => {
expect(raw.server.allowedHostnames).toEqual(["dotta-macbook-pro"]);
});
});

View File

@@ -0,0 +1,16 @@
import { Command } from "commander";
import { describe, expect, it } from "vitest";
import { registerClientAuthCommands } from "../commands/client/auth.js";
describe("registerClientAuthCommands", () => {
it("registers auth commands without duplicate company-id flags", () => {
const program = new Command();
const auth = program.command("auth");
expect(() => registerClientAuthCommands(auth)).not.toThrow();
const login = auth.commands.find((command) => command.name() === "login");
expect(login).toBeDefined();
expect(login?.options.filter((option) => option.long === "--company-id")).toHaveLength(1);
});
});

View File

@@ -0,0 +1,53 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
getStoredBoardCredential,
readBoardAuthStore,
removeStoredBoardCredential,
setStoredBoardCredential,
} from "../client/board-auth.js";
function createTempAuthPath(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-cli-auth-"));
return path.join(dir, "auth.json");
}
describe("board auth store", () => {
it("returns an empty store when the file does not exist", () => {
const authPath = createTempAuthPath();
expect(readBoardAuthStore(authPath)).toEqual({
version: 1,
credentials: {},
});
});
it("stores and retrieves credentials by normalized api base", () => {
const authPath = createTempAuthPath();
setStoredBoardCredential({
apiBase: "http://localhost:3100/",
token: "token-123",
userId: "user-1",
storePath: authPath,
});
expect(getStoredBoardCredential("http://localhost:3100", authPath)).toMatchObject({
apiBase: "http://localhost:3100",
token: "token-123",
userId: "user-1",
});
});
it("removes stored credentials", () => {
const authPath = createTempAuthPath();
setStoredBoardCredential({
apiBase: "http://localhost:3100",
token: "token-123",
storePath: authPath,
});
expect(removeStoredBoardCredential("http://localhost:3100", authPath)).toBe(true);
expect(getStoredBoardCredential("http://localhost:3100", authPath)).toBeNull();
});
});

View File

@@ -8,12 +8,20 @@ function makeCompany(overrides: Partial<Company>): Company {
name: "Alpha",
description: null,
status: "active",
pauseReason: null,
pausedAt: null,
issuePrefix: "ALP",
issueCounter: 1,
budgetMonthlyCents: 0,
spentMonthlyCents: 0,
requireBoardApprovalForNewAgents: false,
feedbackDataSharingEnabled: false,
feedbackDataSharingConsentAt: null,
feedbackDataSharingConsentByUserId: null,
feedbackDataSharingTermsVersion: null,
brandColor: null,
logoAssetId: null,
logoUrl: null,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,

View File

@@ -0,0 +1,502 @@
import { execFile, spawn } from "node:child_process";
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
import { createStoredZipArchive } from "./helpers/zip.js";
const execFileAsync = promisify(execFile);
type ServerProcess = ReturnType<typeof spawn>;
async function getAvailablePort(): Promise<number> {
return await new Promise((resolve, reject) => {
const server = net.createServer();
server.unref();
server.on("error", reject);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (!address || typeof address === "string") {
server.close(() => reject(new Error("Failed to allocate test port")));
return;
}
const { port } = address;
server.close((error) => {
if (error) reject(error);
else resolve(port);
});
});
});
}
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
if (!embeddedPostgresSupport.supported) {
console.warn(
`Skipping embedded Postgres company import/export e2e tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
);
}
function writeTestConfig(configPath: string, tempRoot: string, port: number, connectionString: string) {
const config = {
$meta: {
version: 1,
updatedAt: new Date().toISOString(),
source: "doctor",
},
database: {
mode: "postgres",
connectionString,
embeddedPostgresDataDir: path.join(tempRoot, "embedded-db"),
embeddedPostgresPort: 54329,
backup: {
enabled: false,
intervalMinutes: 60,
retentionDays: 30,
dir: path.join(tempRoot, "backups"),
},
},
logging: {
mode: "file",
logDir: path.join(tempRoot, "logs"),
},
server: {
deploymentMode: "local_trusted",
exposure: "private",
host: "127.0.0.1",
port,
allowedHostnames: [],
serveUi: false,
},
auth: {
baseUrlMode: "auto",
disableSignUp: false,
},
storage: {
provider: "local_disk",
localDisk: {
baseDir: path.join(tempRoot, "storage"),
},
s3: {
bucket: "paperclip",
region: "us-east-1",
prefix: "",
forcePathStyle: false,
},
},
secrets: {
provider: "local_encrypted",
strictMode: false,
localEncrypted: {
keyFilePath: path.join(tempRoot, "secrets", "master.key"),
},
},
};
mkdirSync(path.dirname(configPath), { recursive: true });
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
}
function createServerEnv(configPath: string, port: number, connectionString: string) {
const env = { ...process.env };
for (const key of Object.keys(env)) {
if (key.startsWith("PAPERCLIP_")) {
delete env[key];
}
}
delete env.DATABASE_URL;
delete env.PORT;
delete env.HOST;
delete env.SERVE_UI;
delete env.HEARTBEAT_SCHEDULER_ENABLED;
env.PAPERCLIP_CONFIG = configPath;
env.DATABASE_URL = connectionString;
env.HOST = "127.0.0.1";
env.PORT = String(port);
env.SERVE_UI = "false";
env.PAPERCLIP_DB_BACKUP_ENABLED = "false";
env.HEARTBEAT_SCHEDULER_ENABLED = "false";
env.PAPERCLIP_MIGRATION_AUTO_APPLY = "true";
env.PAPERCLIP_UI_DEV_MIDDLEWARE = "false";
return env;
}
function createCliEnv() {
const env = { ...process.env };
for (const key of Object.keys(env)) {
if (key.startsWith("PAPERCLIP_")) {
delete env[key];
}
}
delete env.DATABASE_URL;
delete env.PORT;
delete env.HOST;
delete env.SERVE_UI;
delete env.PAPERCLIP_DB_BACKUP_ENABLED;
delete env.HEARTBEAT_SCHEDULER_ENABLED;
delete env.PAPERCLIP_MIGRATION_AUTO_APPLY;
delete env.PAPERCLIP_UI_DEV_MIDDLEWARE;
return env;
}
function collectTextFiles(root: string, current: string, files: Record<string, string>) {
for (const entry of readdirSync(current, { withFileTypes: true })) {
const absolutePath = path.join(current, entry.name);
if (entry.isDirectory()) {
collectTextFiles(root, absolutePath, files);
continue;
}
if (!entry.isFile()) continue;
const relativePath = path.relative(root, absolutePath).replace(/\\/g, "/");
files[relativePath] = readFileSync(absolutePath, "utf8");
}
}
async function stopServerProcess(child: ServerProcess | null) {
if (!child || child.exitCode !== null) return;
child.kill("SIGTERM");
await new Promise<void>((resolve) => {
child.once("exit", () => resolve());
setTimeout(() => {
if (child.exitCode === null) {
child.kill("SIGKILL");
}
}, 5_000);
});
}
async function api<T>(baseUrl: string, pathname: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${baseUrl}${pathname}`, init);
const text = await res.text();
if (!res.ok) {
throw new Error(`Request failed ${res.status} ${pathname}: ${text}`);
}
return text ? JSON.parse(text) as T : (null as T);
}
async function runCliJson<T>(args: string[], opts: { apiBase: string; configPath: string }) {
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const result = await execFileAsync(
"pnpm",
["--silent", "paperclipai", ...args, "--api-base", opts.apiBase, "--config", opts.configPath, "--json"],
{
cwd: repoRoot,
env: createCliEnv(),
maxBuffer: 10 * 1024 * 1024,
},
);
const stdout = result.stdout.trim();
const jsonStart = stdout.search(/[\[{]/);
if (jsonStart === -1) {
throw new Error(`CLI did not emit JSON.\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
}
return JSON.parse(stdout.slice(jsonStart)) as T;
}
async function waitForServer(
apiBase: string,
child: ServerProcess,
output: { stdout: string[]; stderr: string[] },
) {
const startedAt = Date.now();
while (Date.now() - startedAt < 30_000) {
if (child.exitCode !== null) {
throw new Error(
`paperclipai run exited before healthcheck succeeded.\nstdout:\n${output.stdout.join("")}\nstderr:\n${output.stderr.join("")}`,
);
}
try {
const res = await fetch(`${apiBase}/api/health`);
if (res.ok) return;
} catch {
// Server is still starting.
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
throw new Error(
`Timed out waiting for ${apiBase}/api/health.\nstdout:\n${output.stdout.join("")}\nstderr:\n${output.stderr.join("")}`,
);
}
describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
let tempRoot = "";
let configPath = "";
let exportDir = "";
let apiBase = "";
let serverProcess: ServerProcess | null = null;
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
beforeAll(async () => {
tempRoot = mkdtempSync(path.join(os.tmpdir(), "paperclip-company-cli-e2e-"));
configPath = path.join(tempRoot, "config", "config.json");
exportDir = path.join(tempRoot, "exported-company");
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-company-cli-db-");
const port = await getAvailablePort();
writeTestConfig(configPath, tempRoot, port, tempDb.connectionString);
apiBase = `http://127.0.0.1:${port}`;
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const output = { stdout: [] as string[], stderr: [] as string[] };
const child = spawn(
"pnpm",
["paperclipai", "run", "--config", configPath],
{
cwd: repoRoot,
env: createServerEnv(configPath, port, tempDb.connectionString),
stdio: ["ignore", "pipe", "pipe"],
},
);
serverProcess = child;
child.stdout?.on("data", (chunk) => {
output.stdout.push(String(chunk));
});
child.stderr?.on("data", (chunk) => {
output.stderr.push(String(chunk));
});
await waitForServer(apiBase, child, output);
}, 60_000);
afterAll(async () => {
await stopServerProcess(serverProcess);
await tempDb?.cleanup();
if (tempRoot) {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it("exports a company package and imports it into new and existing companies", async () => {
expect(serverProcess).not.toBeNull();
const sourceCompany = await api<{ id: string; name: string; issuePrefix: string }>(apiBase, "/api/companies", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ name: `CLI Export Source ${Date.now()}` }),
});
const sourceAgent = await api<{ id: string; name: string }>(
apiBase,
`/api/companies/${sourceCompany.id}/agents`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
name: "Export Engineer",
role: "engineer",
adapterType: "claude_local",
adapterConfig: {
promptTemplate: "You verify company portability.",
},
}),
},
);
const sourceProject = await api<{ id: string; name: string }>(
apiBase,
`/api/companies/${sourceCompany.id}/projects`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
name: "Portability Verification",
status: "in_progress",
}),
},
);
const largeIssueDescription = `Round-trip the company package through the CLI.\n\n${"portable-data ".repeat(12_000)}`;
const sourceIssue = await api<{ id: string; title: string; identifier: string }>(
apiBase,
`/api/companies/${sourceCompany.id}/issues`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
title: "Validate company import/export",
description: largeIssueDescription,
status: "todo",
projectId: sourceProject.id,
assigneeAgentId: sourceAgent.id,
}),
},
);
const exportResult = await runCliJson<{
ok: boolean;
out: string;
filesWritten: number;
}>(
[
"company",
"export",
sourceCompany.id,
"--out",
exportDir,
"--include",
"company,agents,projects,issues",
],
{ apiBase, configPath },
);
expect(exportResult.ok).toBe(true);
expect(exportResult.filesWritten).toBeGreaterThan(0);
expect(readFileSync(path.join(exportDir, "COMPANY.md"), "utf8")).toContain(sourceCompany.name);
expect(readFileSync(path.join(exportDir, ".paperclip.yaml"), "utf8")).toContain('schema: "paperclip/v1"');
const importedNew = await runCliJson<{
company: { id: string; name: string; action: string };
agents: Array<{ id: string | null; action: string; name: string }>;
}>(
[
"company",
"import",
exportDir,
"--target",
"new",
"--new-company-name",
`Imported ${sourceCompany.name}`,
"--include",
"company,agents,projects,issues",
"--yes",
],
{ apiBase, configPath },
);
expect(importedNew.company.action).toBe("created");
expect(importedNew.agents).toHaveLength(1);
expect(importedNew.agents[0]?.action).toBe("created");
const importedAgents = await api<Array<{ id: string; name: string }>>(
apiBase,
`/api/companies/${importedNew.company.id}/agents`,
);
const importedProjects = await api<Array<{ id: string; name: string }>>(
apiBase,
`/api/companies/${importedNew.company.id}/projects`,
);
const importedIssues = await api<Array<{ id: string; title: string; identifier: string }>>(
apiBase,
`/api/companies/${importedNew.company.id}/issues`,
);
expect(importedAgents.map((agent) => agent.name)).toContain(sourceAgent.name);
expect(importedProjects.map((project) => project.name)).toContain(sourceProject.name);
expect(importedIssues.map((issue) => issue.title)).toContain(sourceIssue.title);
const previewExisting = await runCliJson<{
errors: string[];
plan: {
companyAction: string;
agentPlans: Array<{ action: string }>;
projectPlans: Array<{ action: string }>;
issuePlans: Array<{ action: string }>;
};
}>(
[
"company",
"import",
exportDir,
"--target",
"existing",
"--company-id",
importedNew.company.id,
"--include",
"company,agents,projects,issues",
"--collision",
"rename",
"--dry-run",
],
{ apiBase, configPath },
);
expect(previewExisting.errors).toEqual([]);
expect(previewExisting.plan.companyAction).toBe("none");
expect(previewExisting.plan.agentPlans.some((plan) => plan.action === "create")).toBe(true);
expect(previewExisting.plan.projectPlans.some((plan) => plan.action === "create")).toBe(true);
expect(previewExisting.plan.issuePlans.some((plan) => plan.action === "create")).toBe(true);
const importedExisting = await runCliJson<{
company: { id: string; action: string };
agents: Array<{ id: string | null; action: string; name: string }>;
}>(
[
"company",
"import",
exportDir,
"--target",
"existing",
"--company-id",
importedNew.company.id,
"--include",
"company,agents,projects,issues",
"--collision",
"rename",
"--yes",
],
{ apiBase, configPath },
);
expect(importedExisting.company.action).toBe("unchanged");
expect(importedExisting.agents.some((agent) => agent.action === "created")).toBe(true);
const twiceImportedAgents = await api<Array<{ id: string; name: string }>>(
apiBase,
`/api/companies/${importedNew.company.id}/agents`,
);
const twiceImportedProjects = await api<Array<{ id: string; name: string }>>(
apiBase,
`/api/companies/${importedNew.company.id}/projects`,
);
const twiceImportedIssues = await api<Array<{ id: string; title: string; identifier: string }>>(
apiBase,
`/api/companies/${importedNew.company.id}/issues`,
);
expect(twiceImportedAgents).toHaveLength(2);
expect(new Set(twiceImportedAgents.map((agent) => agent.name)).size).toBe(2);
expect(twiceImportedProjects).toHaveLength(2);
expect(twiceImportedIssues).toHaveLength(2);
const zipPath = path.join(tempRoot, "exported-company.zip");
const portableFiles: Record<string, string> = {};
collectTextFiles(exportDir, exportDir, portableFiles);
writeFileSync(zipPath, createStoredZipArchive(portableFiles, "paperclip-demo"));
const importedFromZip = await runCliJson<{
company: { id: string; name: string; action: string };
agents: Array<{ id: string | null; action: string; name: string }>;
}>(
[
"company",
"import",
zipPath,
"--target",
"new",
"--new-company-name",
`Zip Imported ${sourceCompany.name}`,
"--include",
"company,agents,projects,issues",
"--yes",
],
{ apiBase, configPath },
);
expect(importedFromZip.company.action).toBe("created");
expect(importedFromZip.agents.some((agent) => agent.action === "created")).toBe(true);
}, 60_000);
});

View File

@@ -0,0 +1,74 @@
import { describe, expect, it } from "vitest";
import {
isGithubShorthand,
looksLikeRepoUrl,
isHttpUrl,
normalizeGithubImportSource,
} from "../commands/client/company.js";
describe("isHttpUrl", () => {
it("matches http URLs", () => {
expect(isHttpUrl("http://example.com/foo")).toBe(true);
});
it("matches https URLs", () => {
expect(isHttpUrl("https://example.com/foo")).toBe(true);
});
it("rejects local paths", () => {
expect(isHttpUrl("/tmp/my-company")).toBe(false);
expect(isHttpUrl("./relative")).toBe(false);
});
});
describe("looksLikeRepoUrl", () => {
it("matches GitHub URLs", () => {
expect(looksLikeRepoUrl("https://github.com/org/repo")).toBe(true);
});
it("rejects URLs without owner/repo path", () => {
expect(looksLikeRepoUrl("https://example.com/foo")).toBe(false);
});
it("rejects local paths", () => {
expect(looksLikeRepoUrl("/tmp/my-company")).toBe(false);
});
});
describe("isGithubShorthand", () => {
it("matches owner/repo/path shorthands", () => {
expect(isGithubShorthand("paperclipai/companies/gstack")).toBe(true);
expect(isGithubShorthand("paperclipai/companies")).toBe(true);
});
it("rejects local-looking paths", () => {
expect(isGithubShorthand("./exports/acme")).toBe(false);
expect(isGithubShorthand("/tmp/acme")).toBe(false);
expect(isGithubShorthand("C:\\temp\\acme")).toBe(false);
});
});
describe("normalizeGithubImportSource", () => {
it("normalizes shorthand imports to canonical GitHub sources", () => {
expect(normalizeGithubImportSource("paperclipai/companies/gstack")).toBe(
"https://github.com/paperclipai/companies?ref=main&path=gstack",
);
});
it("applies --ref to shorthand imports", () => {
expect(normalizeGithubImportSource("paperclipai/companies/gstack", "feature/demo")).toBe(
"https://github.com/paperclipai/companies?ref=feature%2Fdemo&path=gstack",
);
});
it("applies --ref to existing GitHub tree URLs without losing the package path", () => {
expect(
normalizeGithubImportSource(
"https://github.com/paperclipai/companies/tree/main/gstack",
"release/2026-03-23",
),
).toBe(
"https://github.com/paperclipai/companies?ref=release%2F2026-03-23&path=gstack",
);
});
});

View File

@@ -0,0 +1,44 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { resolveInlineSourceFromPath } from "../commands/client/company.js";
import { createStoredZipArchive } from "./helpers/zip.js";
const tempDirs: string[] = [];
afterEach(async () => {
for (const dir of tempDirs.splice(0)) {
await rm(dir, { recursive: true, force: true });
}
});
describe("resolveInlineSourceFromPath", () => {
it("imports portable files from a zip archive instead of scanning the parent directory", async () => {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-company-import-zip-"));
tempDirs.push(tempDir);
const archivePath = path.join(tempDir, "paperclip-demo.zip");
const archive = createStoredZipArchive(
{
"COMPANY.md": "# Company\n",
".paperclip.yaml": "schema: paperclip/v1\n",
"agents/ceo/AGENT.md": "# CEO\n",
"notes/todo.txt": "ignore me\n",
},
"paperclip-demo",
);
await writeFile(archivePath, archive);
const resolved = await resolveInlineSourceFromPath(archivePath);
expect(resolved).toEqual({
rootPath: "paperclip-demo",
files: {
"COMPANY.md": "# Company\n",
".paperclip.yaml": "schema: paperclip/v1\n",
"agents/ceo/AGENT.md": "# CEO\n",
},
});
});
});

View File

@@ -0,0 +1,599 @@
import { describe, expect, it } from "vitest";
import type { CompanyPortabilityPreviewResult } from "@paperclipai/shared";
import {
buildCompanyDashboardUrl,
buildDefaultImportAdapterOverrides,
buildDefaultImportSelectionState,
buildImportSelectionCatalog,
buildSelectedFilesFromImportSelection,
renderCompanyImportPreview,
renderCompanyImportResult,
resolveCompanyImportApplyConfirmationMode,
resolveCompanyImportApiPath,
} from "../commands/client/company.js";
describe("resolveCompanyImportApiPath", () => {
it("uses company-scoped preview route for existing-company dry runs", () => {
expect(
resolveCompanyImportApiPath({
dryRun: true,
targetMode: "existing_company",
companyId: "company-123",
}),
).toBe("/api/companies/company-123/imports/preview");
});
it("uses company-scoped apply route for existing-company imports", () => {
expect(
resolveCompanyImportApiPath({
dryRun: false,
targetMode: "existing_company",
companyId: "company-123",
}),
).toBe("/api/companies/company-123/imports/apply");
});
it("keeps global routes for new-company imports", () => {
expect(
resolveCompanyImportApiPath({
dryRun: true,
targetMode: "new_company",
}),
).toBe("/api/companies/import/preview");
expect(
resolveCompanyImportApiPath({
dryRun: false,
targetMode: "new_company",
}),
).toBe("/api/companies/import");
});
it("throws when an existing-company import is missing a company id", () => {
expect(() =>
resolveCompanyImportApiPath({
dryRun: true,
targetMode: "existing_company",
companyId: " ",
})
).toThrow(/require a companyId/i);
});
});
describe("resolveCompanyImportApplyConfirmationMode", () => {
it("skips confirmation when --yes is set", () => {
expect(
resolveCompanyImportApplyConfirmationMode({
yes: true,
interactive: false,
json: false,
}),
).toBe("skip");
});
it("prompts in interactive text mode when --yes is not set", () => {
expect(
resolveCompanyImportApplyConfirmationMode({
yes: false,
interactive: true,
json: false,
}),
).toBe("prompt");
});
it("requires --yes for non-interactive apply", () => {
expect(() =>
resolveCompanyImportApplyConfirmationMode({
yes: false,
interactive: false,
json: false,
})
).toThrow(/non-interactive terminal requires --yes/i);
});
it("requires --yes for json apply", () => {
expect(() =>
resolveCompanyImportApplyConfirmationMode({
yes: false,
interactive: false,
json: true,
})
).toThrow(/with --json requires --yes/i);
});
});
describe("buildCompanyDashboardUrl", () => {
it("preserves the configured base path when building a dashboard URL", () => {
expect(buildCompanyDashboardUrl("https://paperclip.example/app/", "PAP")).toBe(
"https://paperclip.example/app/PAP/dashboard",
);
});
});
describe("renderCompanyImportPreview", () => {
it("summarizes the preview with counts, selection info, and truncated examples", () => {
const preview: CompanyPortabilityPreviewResult = {
include: {
company: true,
agents: true,
projects: true,
issues: true,
skills: true,
},
targetCompanyId: "company-123",
targetCompanyName: "Imported Co",
collisionStrategy: "rename",
selectedAgentSlugs: ["ceo", "cto", "eng-1", "eng-2", "eng-3", "eng-4", "eng-5"],
plan: {
companyAction: "update",
agentPlans: [
{ slug: "ceo", action: "create", plannedName: "CEO", existingAgentId: null, reason: null },
{ slug: "cto", action: "update", plannedName: "CTO", existingAgentId: "agent-2", reason: "replace strategy" },
{ slug: "eng-1", action: "skip", plannedName: "Engineer 1", existingAgentId: "agent-3", reason: "skip strategy" },
{ slug: "eng-2", action: "create", plannedName: "Engineer 2", existingAgentId: null, reason: null },
{ slug: "eng-3", action: "create", plannedName: "Engineer 3", existingAgentId: null, reason: null },
{ slug: "eng-4", action: "create", plannedName: "Engineer 4", existingAgentId: null, reason: null },
{ slug: "eng-5", action: "create", plannedName: "Engineer 5", existingAgentId: null, reason: null },
],
projectPlans: [
{ slug: "alpha", action: "create", plannedName: "Alpha", existingProjectId: null, reason: null },
],
issuePlans: [
{ slug: "kickoff", action: "create", plannedTitle: "Kickoff", reason: null },
],
},
manifest: {
schemaVersion: 1,
generatedAt: "2026-03-23T17:00:00.000Z",
source: {
companyId: "company-src",
companyName: "Source Co",
},
includes: {
company: true,
agents: true,
projects: true,
issues: true,
skills: true,
},
company: {
path: "COMPANY.md",
name: "Source Co",
description: null,
brandColor: null,
logoPath: null,
requireBoardApprovalForNewAgents: false,
feedbackDataSharingEnabled: false,
feedbackDataSharingConsentAt: null,
feedbackDataSharingConsentByUserId: null,
feedbackDataSharingTermsVersion: null,
},
sidebar: {
agents: ["ceo"],
projects: ["alpha"],
},
agents: [
{
slug: "ceo",
name: "CEO",
path: "agents/ceo/AGENT.md",
skills: [],
role: "ceo",
title: null,
icon: null,
capabilities: null,
reportsToSlug: null,
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
budgetMonthlyCents: 0,
metadata: null,
},
],
skills: [
{
key: "skill-a",
slug: "skill-a",
name: "Skill A",
path: "skills/skill-a/SKILL.md",
description: null,
sourceType: "inline",
sourceLocator: null,
sourceRef: null,
trustLevel: null,
compatibility: null,
metadata: null,
fileInventory: [],
},
],
projects: [
{
slug: "alpha",
name: "Alpha",
path: "projects/alpha/PROJECT.md",
description: null,
ownerAgentSlug: null,
leadAgentSlug: null,
targetDate: null,
color: null,
status: null,
executionWorkspacePolicy: null,
workspaces: [],
env: null,
metadata: null,
},
],
issues: [
{
slug: "kickoff",
identifier: null,
title: "Kickoff",
path: "projects/alpha/issues/kickoff/TASK.md",
projectSlug: "alpha",
projectWorkspaceKey: null,
assigneeAgentSlug: "ceo",
description: null,
recurring: false,
routine: null,
legacyRecurrence: null,
status: null,
priority: null,
labelIds: [],
billingCode: null,
executionWorkspaceSettings: null,
assigneeAdapterOverrides: null,
metadata: null,
},
],
envInputs: [
{
key: "OPENAI_API_KEY",
description: null,
agentSlug: "ceo",
projectSlug: null,
kind: "secret",
requirement: "required",
defaultValue: null,
portability: "portable",
},
],
},
files: {
"COMPANY.md": "# Source Co",
},
envInputs: [
{
key: "OPENAI_API_KEY",
description: null,
agentSlug: "ceo",
projectSlug: null,
kind: "secret",
requirement: "required",
defaultValue: null,
portability: "portable",
},
],
warnings: ["One warning"],
errors: ["One error"],
};
const rendered = renderCompanyImportPreview(preview, {
sourceLabel: "GitHub: https://github.com/paperclipai/companies/demo",
targetLabel: "Imported Co (company-123)",
infoMessages: ["Using claude-local adapter"],
});
expect(rendered).toContain("Include");
expect(rendered).toContain("company, projects, tasks, agents, skills");
expect(rendered).toContain("7 agents total");
expect(rendered).toContain("1 project total");
expect(rendered).toContain("1 task total");
expect(rendered).toContain("skills: 1 skill packaged");
expect(rendered).toContain("+1 more");
expect(rendered).toContain("Using claude-local adapter");
expect(rendered).toContain("Warnings");
expect(rendered).toContain("Errors");
});
});
describe("renderCompanyImportResult", () => {
it("summarizes import results with created, updated, and skipped counts", () => {
const rendered = renderCompanyImportResult(
{
company: {
id: "company-123",
name: "Imported Co",
action: "updated",
},
agents: [
{ slug: "ceo", id: "agent-1", action: "created", name: "CEO", reason: null },
{ slug: "cto", id: "agent-2", action: "updated", name: "CTO", reason: "replace strategy" },
{ slug: "ops", id: null, action: "skipped", name: "Ops", reason: "skip strategy" },
],
projects: [
{ slug: "app", id: "project-1", action: "created", name: "App", reason: null },
{ slug: "ops", id: "project-2", action: "updated", name: "Operations", reason: "replace strategy" },
{ slug: "archive", id: null, action: "skipped", name: "Archive", reason: "skip strategy" },
],
envInputs: [],
warnings: ["Review API keys"],
},
{
targetLabel: "Imported Co (company-123)",
companyUrl: "https://paperclip.example/PAP/dashboard",
infoMessages: ["Using claude-local adapter"],
},
);
expect(rendered).toContain("Company");
expect(rendered).toContain("https://paperclip.example/PAP/dashboard");
expect(rendered).toContain("3 agents total (1 created, 1 updated, 1 skipped)");
expect(rendered).toContain("3 projects total (1 created, 1 updated, 1 skipped)");
expect(rendered).toContain("Agent results");
expect(rendered).toContain("Project results");
expect(rendered).toContain("Using claude-local adapter");
expect(rendered).toContain("Review API keys");
});
});
describe("import selection catalog", () => {
it("defaults to everything and keeps project selection separate from task selection", () => {
const preview: CompanyPortabilityPreviewResult = {
include: {
company: true,
agents: true,
projects: true,
issues: true,
skills: true,
},
targetCompanyId: "company-123",
targetCompanyName: "Imported Co",
collisionStrategy: "rename",
selectedAgentSlugs: ["ceo"],
plan: {
companyAction: "create",
agentPlans: [],
projectPlans: [],
issuePlans: [],
},
manifest: {
schemaVersion: 1,
generatedAt: "2026-03-23T18:00:00.000Z",
source: {
companyId: "company-src",
companyName: "Source Co",
},
includes: {
company: true,
agents: true,
projects: true,
issues: true,
skills: true,
},
company: {
path: "COMPANY.md",
name: "Source Co",
description: null,
brandColor: null,
logoPath: "images/company-logo.png",
requireBoardApprovalForNewAgents: false,
feedbackDataSharingEnabled: false,
feedbackDataSharingConsentAt: null,
feedbackDataSharingConsentByUserId: null,
feedbackDataSharingTermsVersion: null,
},
sidebar: {
agents: ["ceo"],
projects: ["alpha"],
},
agents: [
{
slug: "ceo",
name: "CEO",
path: "agents/ceo/AGENT.md",
skills: [],
role: "ceo",
title: null,
icon: null,
capabilities: null,
reportsToSlug: null,
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
budgetMonthlyCents: 0,
metadata: null,
},
],
skills: [
{
key: "skill-a",
slug: "skill-a",
name: "Skill A",
path: "skills/skill-a/SKILL.md",
description: null,
sourceType: "inline",
sourceLocator: null,
sourceRef: null,
trustLevel: null,
compatibility: null,
metadata: null,
fileInventory: [{ path: "skills/skill-a/helper.md", kind: "doc" }],
},
],
projects: [
{
slug: "alpha",
name: "Alpha",
path: "projects/alpha/PROJECT.md",
description: null,
ownerAgentSlug: null,
leadAgentSlug: null,
targetDate: null,
color: null,
status: null,
executionWorkspacePolicy: null,
workspaces: [],
env: null,
metadata: null,
},
],
issues: [
{
slug: "kickoff",
identifier: null,
title: "Kickoff",
path: "projects/alpha/issues/kickoff/TASK.md",
projectSlug: "alpha",
projectWorkspaceKey: null,
assigneeAgentSlug: "ceo",
description: null,
recurring: false,
routine: null,
legacyRecurrence: null,
status: null,
priority: null,
labelIds: [],
billingCode: null,
executionWorkspaceSettings: null,
assigneeAdapterOverrides: null,
metadata: null,
},
],
envInputs: [],
},
files: {
"COMPANY.md": "# Source Co",
"README.md": "# Readme",
".paperclip.yaml": "schema: paperclip/v1\n",
"images/company-logo.png": {
encoding: "base64",
data: "",
contentType: "image/png",
},
"projects/alpha/PROJECT.md": "# Alpha",
"projects/alpha/notes.md": "project notes",
"projects/alpha/issues/kickoff/TASK.md": "# Kickoff",
"projects/alpha/issues/kickoff/details.md": "task details",
"agents/ceo/AGENT.md": "# CEO",
"agents/ceo/prompt.md": "prompt",
"skills/skill-a/SKILL.md": "# Skill A",
"skills/skill-a/helper.md": "helper",
},
envInputs: [],
warnings: [],
errors: [],
};
const catalog = buildImportSelectionCatalog(preview);
const state = buildDefaultImportSelectionState(catalog);
expect(state.company).toBe(true);
expect(state.projects.has("alpha")).toBe(true);
expect(state.issues.has("kickoff")).toBe(true);
expect(state.agents.has("ceo")).toBe(true);
expect(state.skills.has("skill-a")).toBe(true);
state.company = false;
state.issues.clear();
state.agents.clear();
state.skills.clear();
const selectedFiles = buildSelectedFilesFromImportSelection(catalog, state);
expect(selectedFiles).toContain(".paperclip.yaml");
expect(selectedFiles).toContain("projects/alpha/PROJECT.md");
expect(selectedFiles).toContain("projects/alpha/notes.md");
expect(selectedFiles).not.toContain("projects/alpha/issues/kickoff/TASK.md");
expect(selectedFiles).not.toContain("projects/alpha/issues/kickoff/details.md");
});
});
describe("default adapter overrides", () => {
it("maps process-only imported agents to claude_local", () => {
const preview: CompanyPortabilityPreviewResult = {
include: {
company: false,
agents: true,
projects: false,
issues: false,
skills: false,
},
targetCompanyId: null,
targetCompanyName: null,
collisionStrategy: "rename",
selectedAgentSlugs: ["legacy-agent", "explicit-agent"],
plan: {
companyAction: "none",
agentPlans: [],
projectPlans: [],
issuePlans: [],
},
manifest: {
schemaVersion: 1,
generatedAt: "2026-03-23T18:20:00.000Z",
source: null,
includes: {
company: false,
agents: true,
projects: false,
issues: false,
skills: false,
},
company: null,
sidebar: null,
agents: [
{
slug: "legacy-agent",
name: "Legacy Agent",
path: "agents/legacy-agent/AGENT.md",
skills: [],
role: "agent",
title: null,
icon: null,
capabilities: null,
reportsToSlug: null,
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
budgetMonthlyCents: 0,
metadata: null,
},
{
slug: "explicit-agent",
name: "Explicit Agent",
path: "agents/explicit-agent/AGENT.md",
skills: [],
role: "agent",
title: null,
icon: null,
capabilities: null,
reportsToSlug: null,
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
budgetMonthlyCents: 0,
metadata: null,
},
],
skills: [],
projects: [],
issues: [],
envInputs: [],
},
files: {},
envInputs: [],
warnings: [],
errors: [],
};
expect(buildDefaultImportAdapterOverrides(preview)).toEqual({
"legacy-agent": {
adapterType: "claude_local",
},
});
});
});

View File

@@ -0,0 +1,102 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { doctor } from "../commands/doctor.js";
import { writeConfig } from "../config/store.js";
import type { PaperclipConfig } from "../config/schema.js";
const ORIGINAL_ENV = { ...process.env };
function createTempConfig(): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-doctor-"));
const configPath = path.join(root, ".paperclip", "config.json");
const runtimeRoot = path.join(root, "runtime");
const config: PaperclipConfig = {
$meta: {
version: 1,
updatedAt: "2026-03-10T00:00:00.000Z",
source: "configure",
},
database: {
mode: "embedded-postgres",
embeddedPostgresDataDir: path.join(runtimeRoot, "db"),
embeddedPostgresPort: 55432,
backup: {
enabled: true,
intervalMinutes: 60,
retentionDays: 30,
dir: path.join(runtimeRoot, "backups"),
},
},
logging: {
mode: "file",
logDir: path.join(runtimeRoot, "logs"),
},
server: {
deploymentMode: "local_trusted",
exposure: "private",
host: "127.0.0.1",
port: 3199,
allowedHostnames: [],
serveUi: true,
},
auth: {
baseUrlMode: "auto",
disableSignUp: false,
},
telemetry: {
enabled: true,
},
storage: {
provider: "local_disk",
localDisk: {
baseDir: path.join(runtimeRoot, "storage"),
},
s3: {
bucket: "paperclip",
region: "us-east-1",
prefix: "",
forcePathStyle: false,
},
},
secrets: {
provider: "local_encrypted",
strictMode: false,
localEncrypted: {
keyFilePath: path.join(runtimeRoot, "secrets", "master.key"),
},
},
};
writeConfig(config, configPath);
return configPath;
}
describe("doctor", () => {
beforeEach(() => {
process.env = { ...ORIGINAL_ENV };
delete process.env.PAPERCLIP_AGENT_JWT_SECRET;
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY;
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
});
afterEach(() => {
process.env = { ...ORIGINAL_ENV };
});
it("re-runs repairable checks so repaired failures do not remain blocking", async () => {
const configPath = createTempConfig();
const summary = await doctor({
config: configPath,
repair: true,
yes: true,
});
expect(summary.failed).toBe(0);
expect(summary.warned).toBe(0);
expect(process.env.PAPERCLIP_AGENT_JWT_SECRET).toBeTruthy();
});
});

View File

@@ -0,0 +1,177 @@
import os from "node:os";
import path from "node:path";
import { mkdtemp, readFile } from "node:fs/promises";
import { Command } from "commander";
import { describe, expect, it } from "vitest";
import type { FeedbackTrace } from "@paperclipai/shared";
import { readZipArchive } from "../commands/client/zip.js";
import {
buildFeedbackTraceQuery,
registerFeedbackCommands,
renderFeedbackReport,
summarizeFeedbackTraces,
writeFeedbackExportBundle,
} from "../commands/client/feedback.js";
function makeTrace(overrides: Partial<FeedbackTrace> = {}): FeedbackTrace {
return {
id: "trace-12345678",
companyId: "company-123",
feedbackVoteId: "vote-12345678",
issueId: "issue-123",
projectId: "project-123",
issueIdentifier: "PAP-123",
issueTitle: "Fix the feedback command",
authorUserId: "user-123",
targetType: "issue_comment",
targetId: "comment-123",
vote: "down",
status: "pending",
destination: "paperclip_labs_feedback_v1",
exportId: null,
consentVersion: "feedback-data-sharing-v1",
schemaVersion: "1",
bundleVersion: "1",
payloadVersion: "1",
payloadDigest: null,
payloadSnapshot: {
vote: {
value: "down",
reason: "Needed more detail",
},
},
targetSummary: {
label: "Comment",
excerpt: "The first answer was too vague.",
authorAgentId: "agent-123",
authorUserId: null,
createdAt: new Date("2026-03-31T12:00:00.000Z"),
documentKey: null,
documentTitle: null,
revisionNumber: null,
},
redactionSummary: null,
attemptCount: 0,
lastAttemptedAt: null,
exportedAt: null,
failureReason: null,
createdAt: new Date("2026-03-31T12:01:00.000Z"),
updatedAt: new Date("2026-03-31T12:02:00.000Z"),
...overrides,
};
}
describe("registerFeedbackCommands", () => {
it("registers the top-level feedback commands", () => {
const program = new Command();
expect(() => registerFeedbackCommands(program)).not.toThrow();
const feedback = program.commands.find((command) => command.name() === "feedback");
expect(feedback).toBeDefined();
expect(feedback?.commands.map((command) => command.name())).toEqual(["report", "export"]);
expect(feedback?.commands[0]?.options.filter((option) => option.long === "--company-id")).toHaveLength(1);
});
});
describe("buildFeedbackTraceQuery", () => {
it("encodes all supported filters", () => {
expect(
buildFeedbackTraceQuery({
targetType: "issue_comment",
vote: "down",
status: "pending",
projectId: "project-123",
issueId: "issue-123",
from: "2026-03-31T00:00:00.000Z",
to: "2026-03-31T23:59:59.999Z",
sharedOnly: true,
}),
).toBe(
"?targetType=issue_comment&vote=down&status=pending&projectId=project-123&issueId=issue-123&from=2026-03-31T00%3A00%3A00.000Z&to=2026-03-31T23%3A59%3A59.999Z&sharedOnly=true&includePayload=true",
);
});
});
describe("renderFeedbackReport", () => {
it("includes summary counts and the optional reason", () => {
const traces = [
makeTrace(),
makeTrace({
id: "trace-87654321",
feedbackVoteId: "vote-87654321",
vote: "up",
status: "local_only",
payloadSnapshot: {
vote: {
value: "up",
reason: null,
},
},
}),
];
const report = renderFeedbackReport({
apiBase: "http://127.0.0.1:3100",
companyId: "company-123",
traces,
summary: summarizeFeedbackTraces(traces),
includePayloads: false,
});
expect(report).toContain("Paperclip Feedback Report");
expect(report).toContain("thumbs up");
expect(report).toContain("thumbs down");
expect(report).toContain("Needed more detail");
});
});
describe("writeFeedbackExportBundle", () => {
it("writes votes, traces, a manifest, and a zip archive", async () => {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-feedback-export-"));
const outputDir = path.join(tempDir, "feedback-export");
const traces = [
makeTrace(),
makeTrace({
id: "trace-abcdef12",
feedbackVoteId: "vote-abcdef12",
issueIdentifier: "PAP-124",
issueId: "issue-124",
vote: "up",
status: "local_only",
payloadSnapshot: {
vote: {
value: "up",
reason: null,
},
},
}),
];
const exported = await writeFeedbackExportBundle({
apiBase: "http://127.0.0.1:3100",
companyId: "company-123",
traces,
outputDir,
});
expect(exported.manifest.summary.total).toBe(2);
expect(exported.manifest.summary.withReason).toBe(1);
const manifest = JSON.parse(await readFile(path.join(outputDir, "index.json"), "utf8")) as {
files: { votes: string[]; traces: string[]; zip: string };
};
expect(manifest.files.votes).toHaveLength(2);
expect(manifest.files.traces).toHaveLength(2);
const archive = await readFile(exported.zipPath);
const zip = await readZipArchive(archive);
expect(Object.keys(zip.files)).toEqual(
expect.arrayContaining([
"index.json",
`votes/${manifest.files.votes[0]}`,
`traces/${manifest.files.traces[0]}`,
]),
);
});
});

View File

@@ -0,0 +1,6 @@
export {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
type EmbeddedPostgresTestDatabase,
type EmbeddedPostgresTestSupport,
} from "@paperclipai/db";

View File

@@ -0,0 +1,87 @@
function writeUint16(target: Uint8Array, offset: number, value: number) {
target[offset] = value & 0xff;
target[offset + 1] = (value >>> 8) & 0xff;
}
function writeUint32(target: Uint8Array, offset: number, value: number) {
target[offset] = value & 0xff;
target[offset + 1] = (value >>> 8) & 0xff;
target[offset + 2] = (value >>> 16) & 0xff;
target[offset + 3] = (value >>> 24) & 0xff;
}
function crc32(bytes: Uint8Array) {
let crc = 0xffffffff;
for (const byte of bytes) {
crc ^= byte;
for (let bit = 0; bit < 8; bit += 1) {
crc = (crc & 1) === 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
}
}
return (crc ^ 0xffffffff) >>> 0;
}
export function createStoredZipArchive(files: Record<string, string>, rootPath: string) {
const encoder = new TextEncoder();
const localChunks: Uint8Array[] = [];
const centralChunks: Uint8Array[] = [];
let localOffset = 0;
let entryCount = 0;
for (const [relativePath, content] of Object.entries(files).sort(([left], [right]) => left.localeCompare(right))) {
const fileName = encoder.encode(`${rootPath}/${relativePath}`);
const body = encoder.encode(content);
const checksum = crc32(body);
const localHeader = new Uint8Array(30 + fileName.length);
writeUint32(localHeader, 0, 0x04034b50);
writeUint16(localHeader, 4, 20);
writeUint16(localHeader, 6, 0x0800);
writeUint16(localHeader, 8, 0);
writeUint32(localHeader, 14, checksum);
writeUint32(localHeader, 18, body.length);
writeUint32(localHeader, 22, body.length);
writeUint16(localHeader, 26, fileName.length);
localHeader.set(fileName, 30);
const centralHeader = new Uint8Array(46 + fileName.length);
writeUint32(centralHeader, 0, 0x02014b50);
writeUint16(centralHeader, 4, 20);
writeUint16(centralHeader, 6, 20);
writeUint16(centralHeader, 8, 0x0800);
writeUint16(centralHeader, 10, 0);
writeUint32(centralHeader, 16, checksum);
writeUint32(centralHeader, 20, body.length);
writeUint32(centralHeader, 24, body.length);
writeUint16(centralHeader, 28, fileName.length);
writeUint32(centralHeader, 42, localOffset);
centralHeader.set(fileName, 46);
localChunks.push(localHeader, body);
centralChunks.push(centralHeader);
localOffset += localHeader.length + body.length;
entryCount += 1;
}
const centralDirectoryLength = centralChunks.reduce((sum, chunk) => sum + chunk.length, 0);
const archive = new Uint8Array(
localChunks.reduce((sum, chunk) => sum + chunk.length, 0) + centralDirectoryLength + 22,
);
let offset = 0;
for (const chunk of localChunks) {
archive.set(chunk, offset);
offset += chunk.length;
}
const centralDirectoryOffset = offset;
for (const chunk of centralChunks) {
archive.set(chunk, offset);
offset += chunk.length;
}
writeUint32(archive, offset, 0x06054b50);
writeUint16(archive, offset + 8, entryCount);
writeUint16(archive, offset + 10, entryCount);
writeUint32(archive, offset + 12, centralDirectoryLength);
writeUint32(archive, offset + 16, centralDirectoryOffset);
return archive;
}

View File

@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { ApiRequestError, PaperclipApiClient } from "../client/http.js";
import { ApiConnectionError, ApiRequestError, PaperclipApiClient } from "../client/http.js";
describe("PaperclipApiClient", () => {
afterEach(() => {
@@ -58,4 +58,49 @@ describe("PaperclipApiClient", () => {
details: { issueId: "1" },
} satisfies Partial<ApiRequestError>);
});
it("throws ApiConnectionError with recovery guidance when fetch fails", async () => {
const fetchMock = vi.fn().mockRejectedValue(new TypeError("fetch failed"));
vi.stubGlobal("fetch", fetchMock);
const client = new PaperclipApiClient({ apiBase: "http://localhost:3100" });
await expect(client.post("/api/companies/import/preview", {})).rejects.toBeInstanceOf(ApiConnectionError);
await expect(client.post("/api/companies/import/preview", {})).rejects.toMatchObject({
url: "http://localhost:3100/api/companies/import/preview",
method: "POST",
causeMessage: "fetch failed",
} satisfies Partial<ApiConnectionError>);
await expect(client.post("/api/companies/import/preview", {})).rejects.toThrow(
/Could not reach the Paperclip API\./,
);
await expect(client.post("/api/companies/import/preview", {})).rejects.toThrow(
/curl http:\/\/localhost:3100\/api\/health/,
);
await expect(client.post("/api/companies/import/preview", {})).rejects.toThrow(
/pnpm dev|pnpm paperclipai run/,
);
});
it("retries once after interactive auth recovery", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response(JSON.stringify({ error: "Board access required" }), { status: 403 }))
.mockResolvedValueOnce(new Response(JSON.stringify({ ok: true }), { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
const recoverAuth = vi.fn().mockResolvedValue("board-token-123");
const client = new PaperclipApiClient({
apiBase: "http://localhost:3100",
recoverAuth,
});
const result = await client.post<{ ok: boolean }>("/api/test", { hello: "world" });
expect(result).toEqual({ ok: true });
expect(recoverAuth).toHaveBeenCalledOnce();
expect(fetchMock).toHaveBeenCalledTimes(2);
const retryHeaders = fetchMock.mock.calls[1]?.[1]?.headers as Record<string, string>;
expect(retryHeaders.authorization).toBe("Bearer board-token-123");
});
});

View File

@@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";
import { resolveRuntimeBind, validateConfiguredBindMode } from "@paperclipai/shared";
import { buildPresetServerConfig } from "../config/server-bind.js";
describe("network bind helpers", () => {
it("rejects non-loopback bind modes in local_trusted", () => {
expect(
validateConfiguredBindMode({
deploymentMode: "local_trusted",
deploymentExposure: "private",
bind: "lan",
host: "0.0.0.0",
}),
).toContain("local_trusted requires server.bind=loopback");
});
it("resolves tailnet bind using the detected tailscale address", () => {
const resolved = resolveRuntimeBind({
bind: "tailnet",
host: "127.0.0.1",
tailnetBindHost: "100.64.0.8",
});
expect(resolved.errors).toEqual([]);
expect(resolved.host).toBe("100.64.0.8");
});
it("requires a custom bind host when bind=custom", () => {
const resolved = resolveRuntimeBind({
bind: "custom",
host: "127.0.0.1",
});
expect(resolved.errors).toContain("server.customBindHost is required when server.bind=custom");
});
it("stores the detected tailscale address for tailnet presets", () => {
process.env.PAPERCLIP_TAILNET_BIND_HOST = "100.64.0.8";
const preset = buildPresetServerConfig("tailnet", {
port: 3100,
allowedHostnames: [],
serveUi: true,
});
expect(preset.server.host).toBe("100.64.0.8");
delete process.env.PAPERCLIP_TAILNET_BIND_HOST;
});
it("falls back to loopback when no tailscale address is available for tailnet presets", () => {
delete process.env.PAPERCLIP_TAILNET_BIND_HOST;
const preset = buildPresetServerConfig("tailnet", {
port: 3100,
allowedHostnames: [],
serveUi: true,
});
expect(preset.server.host).toBe("127.0.0.1");
});
});

View File

@@ -0,0 +1,166 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { onboard } from "../commands/onboard.js";
import type { PaperclipConfig } from "../config/schema.js";
const ORIGINAL_ENV = { ...process.env };
function createExistingConfigFixture() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-onboard-"));
const runtimeRoot = path.join(root, "runtime");
const configPath = path.join(root, ".paperclip", "config.json");
const config: PaperclipConfig = {
$meta: {
version: 1,
updatedAt: "2026-03-29T00:00:00.000Z",
source: "configure",
},
database: {
mode: "embedded-postgres",
embeddedPostgresDataDir: path.join(runtimeRoot, "db"),
embeddedPostgresPort: 54329,
backup: {
enabled: true,
intervalMinutes: 60,
retentionDays: 30,
dir: path.join(runtimeRoot, "backups"),
},
},
logging: {
mode: "file",
logDir: path.join(runtimeRoot, "logs"),
},
server: {
deploymentMode: "local_trusted",
exposure: "private",
host: "127.0.0.1",
port: 3100,
allowedHostnames: [],
serveUi: true,
},
auth: {
baseUrlMode: "auto",
disableSignUp: false,
},
telemetry: {
enabled: true,
},
storage: {
provider: "local_disk",
localDisk: {
baseDir: path.join(runtimeRoot, "storage"),
},
s3: {
bucket: "paperclip",
region: "us-east-1",
prefix: "",
forcePathStyle: false,
},
},
secrets: {
provider: "local_encrypted",
strictMode: false,
localEncrypted: {
keyFilePath: path.join(runtimeRoot, "secrets", "master.key"),
},
},
};
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
return { configPath, configText: fs.readFileSync(configPath, "utf8") };
}
function createFreshConfigPath() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-onboard-fresh-"));
return path.join(root, ".paperclip", "config.json");
}
describe("onboard", () => {
beforeEach(() => {
process.env = { ...ORIGINAL_ENV };
delete process.env.PAPERCLIP_AGENT_JWT_SECRET;
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY;
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
});
afterEach(() => {
process.env = { ...ORIGINAL_ENV };
});
it("preserves an existing config when rerun without flags", async () => {
const fixture = createExistingConfigFixture();
await onboard({ config: fixture.configPath });
expect(fs.readFileSync(fixture.configPath, "utf8")).toBe(fixture.configText);
expect(fs.existsSync(`${fixture.configPath}.backup`)).toBe(false);
expect(fs.existsSync(path.join(path.dirname(fixture.configPath), ".env"))).toBe(true);
});
it("preserves an existing config when rerun with --yes", async () => {
const fixture = createExistingConfigFixture();
await onboard({ config: fixture.configPath, yes: true, invokedByRun: true });
expect(fs.readFileSync(fixture.configPath, "utf8")).toBe(fixture.configText);
expect(fs.existsSync(`${fixture.configPath}.backup`)).toBe(false);
expect(fs.existsSync(path.join(path.dirname(fixture.configPath), ".env"))).toBe(true);
});
it("keeps --yes onboarding on local trusted loopback defaults", async () => {
const configPath = createFreshConfigPath();
process.env.HOST = "0.0.0.0";
process.env.PAPERCLIP_BIND = "lan";
await onboard({ config: configPath, yes: true, invokedByRun: true });
const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as PaperclipConfig;
expect(raw.server.deploymentMode).toBe("local_trusted");
expect(raw.server.exposure).toBe("private");
expect(raw.server.bind).toBe("loopback");
expect(raw.server.host).toBe("127.0.0.1");
});
it("supports authenticated/private quickstart bind presets", async () => {
const configPath = createFreshConfigPath();
process.env.PAPERCLIP_TAILNET_BIND_HOST = "100.64.0.8";
await onboard({ config: configPath, yes: true, invokedByRun: true, bind: "tailnet" });
const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as PaperclipConfig;
expect(raw.server.deploymentMode).toBe("authenticated");
expect(raw.server.exposure).toBe("private");
expect(raw.server.bind).toBe("tailnet");
expect(raw.server.host).toBe("100.64.0.8");
});
it("keeps tailnet quickstart on loopback until tailscale is available", async () => {
const configPath = createFreshConfigPath();
delete process.env.PAPERCLIP_TAILNET_BIND_HOST;
await onboard({ config: configPath, yes: true, invokedByRun: true, bind: "tailnet" });
const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as PaperclipConfig;
expect(raw.server.deploymentMode).toBe("authenticated");
expect(raw.server.exposure).toBe("private");
expect(raw.server.bind).toBe("tailnet");
expect(raw.server.host).toBe("127.0.0.1");
});
it("ignores deployment env overrides during --yes quickstart", async () => {
const configPath = createFreshConfigPath();
process.env.PAPERCLIP_DEPLOYMENT_MODE = "authenticated";
await onboard({ config: configPath, yes: true, invokedByRun: true });
const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as PaperclipConfig;
expect(raw.server.deploymentMode).toBe("local_trusted");
expect(raw.server.exposure).toBe("private");
expect(raw.server.bind).toBe("loopback");
expect(raw.server.host).toBe("127.0.0.1");
});
});

View File

@@ -0,0 +1,249 @@
import { randomUUID } from "node:crypto";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { eq } from "drizzle-orm";
import {
agents,
companies,
createDb,
projects,
routines,
} from "@paperclipai/db";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
import { disableAllRoutinesInConfig } from "../commands/routines.js";
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
if (!embeddedPostgresSupport.supported) {
console.warn(
`Skipping embedded Postgres routines CLI tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
);
}
function writeTestConfig(configPath: string, tempRoot: string, connectionString: string) {
const config = {
$meta: {
version: 1,
updatedAt: new Date().toISOString(),
source: "doctor" as const,
},
database: {
mode: "postgres" as const,
connectionString,
embeddedPostgresDataDir: path.join(tempRoot, "embedded-db"),
embeddedPostgresPort: 54329,
backup: {
enabled: false,
intervalMinutes: 60,
retentionDays: 30,
dir: path.join(tempRoot, "backups"),
},
},
logging: {
mode: "file" as const,
logDir: path.join(tempRoot, "logs"),
},
server: {
deploymentMode: "local_trusted" as const,
exposure: "private" as const,
host: "127.0.0.1",
port: 3100,
allowedHostnames: [],
serveUi: false,
},
auth: {
baseUrlMode: "auto" as const,
disableSignUp: false,
},
storage: {
provider: "local_disk" as const,
localDisk: {
baseDir: path.join(tempRoot, "storage"),
},
s3: {
bucket: "paperclip",
region: "us-east-1",
prefix: "",
forcePathStyle: false,
},
},
secrets: {
provider: "local_encrypted" as const,
strictMode: false,
localEncrypted: {
keyFilePath: path.join(tempRoot, "secrets", "master.key"),
},
},
};
mkdirSync(path.dirname(configPath), { recursive: true });
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
}
describeEmbeddedPostgres("disableAllRoutinesInConfig", () => {
let db!: ReturnType<typeof createDb>;
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
let tempRoot = "";
let configPath = "";
beforeAll(async () => {
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-routines-cli-db-");
db = createDb(tempDb.connectionString);
tempRoot = mkdtempSync(path.join(os.tmpdir(), "paperclip-routines-cli-config-"));
configPath = path.join(tempRoot, "config.json");
writeTestConfig(configPath, tempRoot, tempDb.connectionString);
}, 20_000);
afterEach(async () => {
await db.delete(routines);
await db.delete(projects);
await db.delete(agents);
await db.delete(companies);
});
afterAll(async () => {
await tempDb?.cleanup();
if (tempRoot) {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it("pauses only non-archived routines for the selected company", async () => {
const companyId = randomUUID();
const otherCompanyId = randomUUID();
const projectId = randomUUID();
const otherProjectId = randomUUID();
const agentId = randomUUID();
const otherAgentId = randomUUID();
const activeRoutineId = randomUUID();
const pausedRoutineId = randomUUID();
const archivedRoutineId = randomUUID();
const otherCompanyRoutineId = randomUUID();
await db.insert(companies).values([
{
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
},
{
id: otherCompanyId,
name: "Other company",
issuePrefix: `T${otherCompanyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
},
]);
await db.insert(agents).values([
{
id: agentId,
companyId,
name: "Coder",
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
{
id: otherAgentId,
companyId: otherCompanyId,
name: "Other coder",
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
]);
await db.insert(projects).values([
{
id: projectId,
companyId,
name: "Project",
status: "in_progress",
},
{
id: otherProjectId,
companyId: otherCompanyId,
name: "Other project",
status: "in_progress",
},
]);
await db.insert(routines).values([
{
id: activeRoutineId,
companyId,
projectId,
assigneeAgentId: agentId,
title: "Active routine",
status: "active",
},
{
id: pausedRoutineId,
companyId,
projectId,
assigneeAgentId: agentId,
title: "Paused routine",
status: "paused",
},
{
id: archivedRoutineId,
companyId,
projectId,
assigneeAgentId: agentId,
title: "Archived routine",
status: "archived",
},
{
id: otherCompanyRoutineId,
companyId: otherCompanyId,
projectId: otherProjectId,
assigneeAgentId: otherAgentId,
title: "Other company routine",
status: "active",
},
]);
const result = await disableAllRoutinesInConfig({
config: configPath,
companyId,
});
expect(result).toMatchObject({
companyId,
totalRoutines: 3,
pausedCount: 1,
alreadyPausedCount: 1,
archivedCount: 1,
});
const companyRoutines = await db
.select({
id: routines.id,
status: routines.status,
})
.from(routines)
.where(eq(routines.companyId, companyId));
const statusById = new Map(companyRoutines.map((routine) => [routine.id, routine.status]));
expect(statusById.get(activeRoutineId)).toBe("paused");
expect(statusById.get(pausedRoutineId)).toBe("paused");
expect(statusById.get(archivedRoutineId)).toBe("archived");
const otherCompanyRoutine = await db
.select({
status: routines.status,
})
.from(routines)
.where(eq(routines.id, otherCompanyRoutineId));
expect(otherCompanyRoutine[0]?.status).toBe("active");
});
});

View File

@@ -0,0 +1,117 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const ORIGINAL_ENV = { ...process.env };
const CI_ENV_VARS = ["CI", "CONTINUOUS_INTEGRATION", "BUILD_NUMBER", "GITHUB_ACTIONS", "GITLAB_CI"];
function makeConfigPath(root: string, enabled: boolean): string {
const configPath = path.join(root, ".paperclip", "config.json");
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, JSON.stringify({
$meta: {
version: 1,
updatedAt: "2026-03-31T00:00:00.000Z",
source: "configure",
},
database: {
mode: "embedded-postgres",
embeddedPostgresDataDir: path.join(root, "runtime", "db"),
embeddedPostgresPort: 54329,
backup: {
enabled: true,
intervalMinutes: 60,
retentionDays: 30,
dir: path.join(root, "runtime", "backups"),
},
},
logging: {
mode: "file",
logDir: path.join(root, "runtime", "logs"),
},
server: {
deploymentMode: "local_trusted",
exposure: "private",
host: "127.0.0.1",
port: 3100,
allowedHostnames: [],
serveUi: true,
},
auth: {
baseUrlMode: "auto",
disableSignUp: false,
},
telemetry: {
enabled,
},
storage: {
provider: "local_disk",
localDisk: {
baseDir: path.join(root, "runtime", "storage"),
},
s3: {
bucket: "paperclip",
region: "us-east-1",
prefix: "",
forcePathStyle: false,
},
},
secrets: {
provider: "local_encrypted",
strictMode: false,
localEncrypted: {
keyFilePath: path.join(root, "runtime", "secrets", "master.key"),
},
},
}, null, 2));
return configPath;
}
describe("cli telemetry", () => {
beforeEach(() => {
process.env = { ...ORIGINAL_ENV };
for (const key of CI_ENV_VARS) {
delete process.env[key];
}
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true })));
});
afterEach(() => {
process.env = { ...ORIGINAL_ENV };
vi.unstubAllGlobals();
vi.resetModules();
});
it("respects telemetry.enabled=false from the config file", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-cli-telemetry-"));
const configPath = makeConfigPath(root, false);
process.env.PAPERCLIP_HOME = path.join(root, "home");
process.env.PAPERCLIP_INSTANCE_ID = "telemetry-test";
const { initTelemetryFromConfigFile } = await import("../telemetry.js");
const client = initTelemetryFromConfigFile(configPath);
expect(client).toBeNull();
expect(fs.existsSync(path.join(root, "home", "instances", "telemetry-test", "telemetry", "state.json"))).toBe(false);
});
it("creates telemetry state only after the first event is tracked", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-cli-telemetry-"));
process.env.PAPERCLIP_HOME = path.join(root, "home");
process.env.PAPERCLIP_INSTANCE_ID = "telemetry-test";
const { initTelemetry, flushTelemetry } = await import("../telemetry.js");
const client = initTelemetry({ enabled: true });
const statePath = path.join(root, "home", "instances", "telemetry-test", "telemetry", "state.json");
expect(client).not.toBeNull();
expect(fs.existsSync(statePath)).toBe(false);
client!.track("install.started", { setupMode: "quickstart" });
expect(fs.existsSync(statePath)).toBe(true);
await flushTelemetry();
});
});

View File

@@ -0,0 +1,492 @@
import { describe, expect, it } from "vitest";
import { buildWorktreeMergePlan, parseWorktreeMergeScopes } from "../commands/worktree-merge-history-lib.js";
function makeIssue(overrides: Record<string, unknown> = {}) {
return {
id: "issue-1",
companyId: "company-1",
projectId: null,
projectWorkspaceId: null,
goalId: "goal-1",
parentId: null,
title: "Issue",
description: null,
status: "todo",
priority: "medium",
assigneeAgentId: null,
assigneeUserId: null,
checkoutRunId: null,
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
createdByAgentId: null,
createdByUserId: "local-board",
issueNumber: 1,
identifier: "PAP-1",
requestDepth: 0,
billingCode: null,
assigneeAdapterOverrides: null,
executionWorkspaceId: null,
executionWorkspacePreference: null,
executionWorkspaceSettings: null,
startedAt: null,
completedAt: null,
cancelledAt: null,
hiddenAt: null,
createdAt: new Date("2026-03-20T00:00:00.000Z"),
updatedAt: new Date("2026-03-20T00:00:00.000Z"),
...overrides,
} as any;
}
function makeComment(overrides: Record<string, unknown> = {}) {
return {
id: "comment-1",
companyId: "company-1",
issueId: "issue-1",
authorAgentId: null,
authorUserId: "local-board",
body: "hello",
createdAt: new Date("2026-03-20T00:00:00.000Z"),
updatedAt: new Date("2026-03-20T00:00:00.000Z"),
...overrides,
} as any;
}
function makeIssueDocument(overrides: Record<string, unknown> = {}) {
return {
id: "issue-document-1",
companyId: "company-1",
issueId: "issue-1",
documentId: "document-1",
key: "plan",
linkCreatedAt: new Date("2026-03-20T00:00:00.000Z"),
linkUpdatedAt: new Date("2026-03-20T00:00:00.000Z"),
title: "Plan",
format: "markdown",
latestBody: "# Plan",
latestRevisionId: "revision-1",
latestRevisionNumber: 1,
createdByAgentId: null,
createdByUserId: "local-board",
updatedByAgentId: null,
updatedByUserId: "local-board",
documentCreatedAt: new Date("2026-03-20T00:00:00.000Z"),
documentUpdatedAt: new Date("2026-03-20T00:00:00.000Z"),
...overrides,
} as any;
}
function makeDocumentRevision(overrides: Record<string, unknown> = {}) {
return {
id: "revision-1",
companyId: "company-1",
documentId: "document-1",
revisionNumber: 1,
body: "# Plan",
changeSummary: null,
createdByAgentId: null,
createdByUserId: "local-board",
createdAt: new Date("2026-03-20T00:00:00.000Z"),
...overrides,
} as any;
}
function makeAttachment(overrides: Record<string, unknown> = {}) {
return {
id: "attachment-1",
companyId: "company-1",
issueId: "issue-1",
issueCommentId: null,
assetId: "asset-1",
provider: "local_disk",
objectKey: "company-1/issues/issue-1/2026/03/20/asset.png",
contentType: "image/png",
byteSize: 12,
sha256: "deadbeef",
originalFilename: "asset.png",
createdByAgentId: null,
createdByUserId: "local-board",
assetCreatedAt: new Date("2026-03-20T00:00:00.000Z"),
assetUpdatedAt: new Date("2026-03-20T00:00:00.000Z"),
attachmentCreatedAt: new Date("2026-03-20T00:00:00.000Z"),
attachmentUpdatedAt: new Date("2026-03-20T00:00:00.000Z"),
...overrides,
} as any;
}
function makeProject(overrides: Record<string, unknown> = {}) {
return {
id: "project-1",
companyId: "company-1",
goalId: null,
name: "Project",
description: null,
status: "in_progress",
leadAgentId: null,
targetDate: null,
color: "#22c55e",
pauseReason: null,
pausedAt: null,
executionWorkspacePolicy: null,
archivedAt: null,
createdAt: new Date("2026-03-20T00:00:00.000Z"),
updatedAt: new Date("2026-03-20T00:00:00.000Z"),
...overrides,
} as any;
}
function makeProjectWorkspace(overrides: Record<string, unknown> = {}) {
return {
id: "workspace-1",
companyId: "company-1",
projectId: "project-1",
name: "Workspace",
sourceType: "local_path",
cwd: "/tmp/project",
repoUrl: "https://github.com/example/project.git",
repoRef: "main",
defaultRef: "main",
visibility: "default",
setupCommand: null,
cleanupCommand: null,
remoteProvider: null,
remoteWorkspaceRef: null,
sharedWorkspaceKey: null,
metadata: null,
isPrimary: true,
createdAt: new Date("2026-03-20T00:00:00.000Z"),
updatedAt: new Date("2026-03-20T00:00:00.000Z"),
...overrides,
} as any;
}
describe("worktree merge history planner", () => {
it("parses default scopes", () => {
expect(parseWorktreeMergeScopes(undefined)).toEqual(["issues", "comments"]);
expect(parseWorktreeMergeScopes("issues")).toEqual(["issues"]);
});
it("dedupes nested worktree issues by preserved source uuid", () => {
const sharedIssue = makeIssue({ id: "issue-a", identifier: "PAP-10", title: "Shared" });
const branchOneIssue = makeIssue({
id: "issue-b",
identifier: "PAP-22",
title: "Branch one issue",
createdAt: new Date("2026-03-20T01:00:00.000Z"),
});
const branchTwoIssue = makeIssue({
id: "issue-c",
identifier: "PAP-23",
title: "Branch two issue",
createdAt: new Date("2026-03-20T02:00:00.000Z"),
});
const plan = buildWorktreeMergePlan({
companyId: "company-1",
companyName: "Paperclip",
issuePrefix: "PAP",
previewIssueCounterStart: 500,
scopes: ["issues", "comments"],
sourceIssues: [sharedIssue, branchOneIssue, branchTwoIssue],
targetIssues: [sharedIssue, branchOneIssue],
sourceComments: [],
targetComments: [],
targetAgents: [],
targetProjects: [],
targetProjectWorkspaces: [],
targetGoals: [{ id: "goal-1" }] as any,
});
expect(plan.counts.issuesToInsert).toBe(1);
expect(plan.issuePlans.filter((item) => item.action === "insert").map((item) => item.source.id)).toEqual(["issue-c"]);
expect(plan.issuePlans.find((item) => item.source.id === "issue-c" && item.action === "insert")).toMatchObject({
previewIdentifier: "PAP-501",
});
});
it("clears missing references and coerces in_progress without an assignee", () => {
const plan = buildWorktreeMergePlan({
companyId: "company-1",
companyName: "Paperclip",
issuePrefix: "PAP",
previewIssueCounterStart: 10,
scopes: ["issues"],
sourceIssues: [
makeIssue({
id: "issue-x",
identifier: "PAP-99",
status: "in_progress",
assigneeAgentId: "agent-missing",
projectId: "project-missing",
projectWorkspaceId: "workspace-missing",
goalId: "goal-missing",
}),
],
targetIssues: [],
sourceComments: [],
targetComments: [],
targetAgents: [],
targetProjects: [],
targetProjectWorkspaces: [],
targetGoals: [],
});
const insert = plan.issuePlans[0] as any;
expect(insert.targetStatus).toBe("todo");
expect(insert.targetAssigneeAgentId).toBeNull();
expect(insert.targetProjectId).toBeNull();
expect(insert.targetProjectWorkspaceId).toBeNull();
expect(insert.targetGoalId).toBeNull();
expect(insert.adjustments).toEqual([
"clear_assignee_agent",
"clear_project",
"clear_project_workspace",
"clear_goal",
"coerce_in_progress_to_todo",
]);
});
it("applies an explicit project mapping override instead of clearing the project", () => {
const plan = buildWorktreeMergePlan({
companyId: "company-1",
companyName: "Paperclip",
issuePrefix: "PAP",
previewIssueCounterStart: 10,
scopes: ["issues"],
sourceIssues: [
makeIssue({
id: "issue-project-map",
identifier: "PAP-77",
projectId: "source-project-1",
projectWorkspaceId: "source-workspace-1",
}),
],
targetIssues: [],
sourceComments: [],
targetComments: [],
targetAgents: [],
targetProjects: [{ id: "target-project-1", name: "Mapped project", status: "in_progress" }] as any,
targetProjectWorkspaces: [],
targetGoals: [{ id: "goal-1" }] as any,
projectIdOverrides: {
"source-project-1": "target-project-1",
},
});
const insert = plan.issuePlans[0] as any;
expect(insert.targetProjectId).toBe("target-project-1");
expect(insert.projectResolution).toBe("mapped");
expect(insert.mappedProjectName).toBe("Mapped project");
expect(insert.targetProjectWorkspaceId).toBeNull();
expect(insert.adjustments).toEqual(["clear_project_workspace"]);
});
it("plans selected project imports and preserves project workspace links", () => {
const sourceProject = makeProject({
id: "source-project-1",
name: "Paperclip Evals",
goalId: "goal-1",
});
const sourceWorkspace = makeProjectWorkspace({
id: "source-workspace-1",
projectId: "source-project-1",
cwd: "/Users/dotta/paperclip-evals",
repoUrl: "https://github.com/paperclipai/paperclip-evals.git",
});
const plan = buildWorktreeMergePlan({
companyId: "company-1",
companyName: "Paperclip",
issuePrefix: "PAP",
previewIssueCounterStart: 10,
scopes: ["issues"],
sourceIssues: [
makeIssue({
id: "issue-project-import",
identifier: "PAP-88",
projectId: "source-project-1",
projectWorkspaceId: "source-workspace-1",
}),
],
targetIssues: [],
sourceComments: [],
targetComments: [],
sourceProjects: [sourceProject],
sourceProjectWorkspaces: [sourceWorkspace],
targetAgents: [],
targetProjects: [],
targetProjectWorkspaces: [],
targetGoals: [{ id: "goal-1" }] as any,
importProjectIds: ["source-project-1"],
});
expect(plan.counts.projectsToImport).toBe(1);
expect(plan.projectImports[0]).toMatchObject({
source: { id: "source-project-1", name: "Paperclip Evals" },
targetGoalId: "goal-1",
workspaces: [{ id: "source-workspace-1" }],
});
const insert = plan.issuePlans[0] as any;
expect(insert.targetProjectId).toBe("source-project-1");
expect(insert.targetProjectWorkspaceId).toBe("source-workspace-1");
expect(insert.projectResolution).toBe("imported");
expect(insert.mappedProjectName).toBe("Paperclip Evals");
expect(insert.adjustments).toEqual([]);
});
it("imports comments onto shared or newly imported issues while skipping existing comments", () => {
const sharedIssue = makeIssue({ id: "issue-a", identifier: "PAP-10" });
const newIssue = makeIssue({
id: "issue-b",
identifier: "PAP-11",
createdAt: new Date("2026-03-20T01:00:00.000Z"),
});
const existingComment = makeComment({ id: "comment-existing", issueId: "issue-a" });
const sharedIssueComment = makeComment({ id: "comment-shared", issueId: "issue-a" });
const newIssueComment = makeComment({
id: "comment-new-issue",
issueId: "issue-b",
authorAgentId: "missing-agent",
createdAt: new Date("2026-03-20T01:05:00.000Z"),
});
const plan = buildWorktreeMergePlan({
companyId: "company-1",
companyName: "Paperclip",
issuePrefix: "PAP",
previewIssueCounterStart: 10,
scopes: ["issues", "comments"],
sourceIssues: [sharedIssue, newIssue],
targetIssues: [sharedIssue],
sourceComments: [existingComment, sharedIssueComment, newIssueComment],
targetComments: [existingComment],
targetAgents: [],
targetProjects: [],
targetProjectWorkspaces: [],
targetGoals: [{ id: "goal-1" }] as any,
});
expect(plan.counts.commentsToInsert).toBe(2);
expect(plan.counts.commentsExisting).toBe(1);
expect(plan.commentPlans.filter((item) => item.action === "insert").map((item) => item.source.id)).toEqual([
"comment-shared",
"comment-new-issue",
]);
expect(plan.adjustments.clear_author_agent).toBe(1);
});
it("merges document revisions onto an existing shared document and renumbers conflicts", () => {
const sharedIssue = makeIssue({ id: "issue-a", identifier: "PAP-10" });
const sourceDocument = makeIssueDocument({
issueId: "issue-a",
documentId: "document-a",
latestBody: "# Branch plan",
latestRevisionId: "revision-branch-2",
latestRevisionNumber: 2,
documentUpdatedAt: new Date("2026-03-20T02:00:00.000Z"),
linkUpdatedAt: new Date("2026-03-20T02:00:00.000Z"),
});
const targetDocument = makeIssueDocument({
issueId: "issue-a",
documentId: "document-a",
latestBody: "# Main plan",
latestRevisionId: "revision-main-2",
latestRevisionNumber: 2,
documentUpdatedAt: new Date("2026-03-20T01:00:00.000Z"),
linkUpdatedAt: new Date("2026-03-20T01:00:00.000Z"),
});
const sourceRevisionOne = makeDocumentRevision({ documentId: "document-a", id: "revision-1" });
const sourceRevisionTwo = makeDocumentRevision({
documentId: "document-a",
id: "revision-branch-2",
revisionNumber: 2,
body: "# Branch plan",
createdAt: new Date("2026-03-20T02:00:00.000Z"),
});
const targetRevisionOne = makeDocumentRevision({ documentId: "document-a", id: "revision-1" });
const targetRevisionTwo = makeDocumentRevision({
documentId: "document-a",
id: "revision-main-2",
revisionNumber: 2,
body: "# Main plan",
createdAt: new Date("2026-03-20T01:00:00.000Z"),
});
const plan = buildWorktreeMergePlan({
companyId: "company-1",
companyName: "Paperclip",
issuePrefix: "PAP",
previewIssueCounterStart: 10,
scopes: ["issues", "comments"],
sourceIssues: [sharedIssue],
targetIssues: [sharedIssue],
sourceComments: [],
targetComments: [],
sourceDocuments: [sourceDocument],
targetDocuments: [targetDocument],
sourceDocumentRevisions: [sourceRevisionOne, sourceRevisionTwo],
targetDocumentRevisions: [targetRevisionOne, targetRevisionTwo],
sourceAttachments: [],
targetAttachments: [],
targetAgents: [],
targetProjects: [],
targetProjectWorkspaces: [],
targetGoals: [{ id: "goal-1" }] as any,
});
expect(plan.counts.documentsToMerge).toBe(1);
expect(plan.counts.documentRevisionsToInsert).toBe(1);
expect(plan.documentPlans[0]).toMatchObject({
action: "merge_existing",
latestRevisionId: "revision-branch-2",
latestRevisionNumber: 3,
});
const mergePlan = plan.documentPlans[0] as any;
expect(mergePlan.revisionsToInsert).toHaveLength(1);
expect(mergePlan.revisionsToInsert[0]).toMatchObject({
source: { id: "revision-branch-2" },
targetRevisionNumber: 3,
});
});
it("imports attachments while clearing missing comment and author references", () => {
const sharedIssue = makeIssue({ id: "issue-a", identifier: "PAP-10" });
const attachment = makeAttachment({
issueId: "issue-a",
issueCommentId: "comment-missing",
createdByAgentId: "agent-missing",
});
const plan = buildWorktreeMergePlan({
companyId: "company-1",
companyName: "Paperclip",
issuePrefix: "PAP",
previewIssueCounterStart: 10,
scopes: ["issues"],
sourceIssues: [sharedIssue],
targetIssues: [sharedIssue],
sourceComments: [],
targetComments: [],
sourceDocuments: [],
targetDocuments: [],
sourceDocumentRevisions: [],
targetDocumentRevisions: [],
sourceAttachments: [attachment],
targetAttachments: [],
targetAgents: [],
targetProjects: [],
targetProjectWorkspaces: [],
targetGoals: [{ id: "goal-1" }] as any,
});
expect(plan.counts.attachmentsToInsert).toBe(1);
expect(plan.adjustments.clear_attachment_agent).toBe(1);
expect(plan.attachmentPlans[0]).toMatchObject({
action: "insert",
targetIssueCommentId: null,
targetCreatedByAgentId: null,
});
});
});

View File

@@ -0,0 +1,982 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
agents,
companies,
createDb,
projects,
routines,
routineTriggers,
} from "@paperclipai/db";
import {
copyGitHooksToWorktreeGitDir,
copySeededSecretsKey,
pauseSeededScheduledRoutines,
readSourceAttachmentBody,
rebindWorkspaceCwd,
resolveSourceConfigPath,
resolveWorktreeReseedSource,
resolveWorktreeReseedTargetPaths,
resolveGitWorktreeAddArgs,
resolveWorktreeMakeTargetPath,
worktreeInitCommand,
worktreeMakeCommand,
worktreeReseedCommand,
} from "../commands/worktree.js";
import {
buildWorktreeConfig,
buildWorktreeEnvEntries,
formatShellExports,
generateWorktreeColor,
resolveWorktreeSeedPlan,
resolveWorktreeLocalPaths,
rewriteLocalUrlPort,
sanitizeWorktreeInstanceId,
} from "../commands/worktree-lib.js";
import type { PaperclipConfig } from "../config/schema.js";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
const ORIGINAL_CWD = process.cwd();
const ORIGINAL_ENV = { ...process.env };
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
if (!embeddedPostgresSupport.supported) {
console.warn(
`Skipping embedded Postgres worktree CLI tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
);
}
afterEach(() => {
process.chdir(ORIGINAL_CWD);
for (const key of Object.keys(process.env)) {
if (!(key in ORIGINAL_ENV)) delete process.env[key];
}
for (const [key, value] of Object.entries(ORIGINAL_ENV)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
});
function buildSourceConfig(): PaperclipConfig {
return {
$meta: {
version: 1,
updatedAt: "2026-03-09T00:00:00.000Z",
source: "configure",
},
database: {
mode: "embedded-postgres",
embeddedPostgresDataDir: "/tmp/main/db",
embeddedPostgresPort: 54329,
backup: {
enabled: true,
intervalMinutes: 60,
retentionDays: 30,
dir: "/tmp/main/backups",
},
},
logging: {
mode: "file",
logDir: "/tmp/main/logs",
},
server: {
deploymentMode: "authenticated",
exposure: "private",
host: "127.0.0.1",
port: 3100,
allowedHostnames: ["localhost"],
serveUi: true,
},
auth: {
baseUrlMode: "explicit",
publicBaseUrl: "http://127.0.0.1:3100",
disableSignUp: false,
},
telemetry: {
enabled: true,
},
storage: {
provider: "local_disk",
localDisk: {
baseDir: "/tmp/main/storage",
},
s3: {
bucket: "paperclip",
region: "us-east-1",
prefix: "",
forcePathStyle: false,
},
},
secrets: {
provider: "local_encrypted",
strictMode: false,
localEncrypted: {
keyFilePath: "/tmp/main/secrets/master.key",
},
},
};
}
describe("worktree helpers", () => {
it("sanitizes instance ids", () => {
expect(sanitizeWorktreeInstanceId("feature/worktree-support")).toBe("feature-worktree-support");
expect(sanitizeWorktreeInstanceId(" ")).toBe("worktree");
});
it("resolves worktree:make target paths under the user home directory", () => {
expect(resolveWorktreeMakeTargetPath("paperclip-pr-432")).toBe(
path.resolve(os.homedir(), "paperclip-pr-432"),
);
});
it("rejects worktree:make names that are not safe directory/branch names", () => {
expect(() => resolveWorktreeMakeTargetPath("paperclip/pr-432")).toThrow(
"Worktree name must contain only letters, numbers, dots, underscores, or dashes.",
);
});
it("builds git worktree add args for new and existing branches", () => {
expect(
resolveGitWorktreeAddArgs({
branchName: "feature-branch",
targetPath: "/tmp/feature-branch",
branchExists: false,
}),
).toEqual(["worktree", "add", "-b", "feature-branch", "/tmp/feature-branch", "HEAD"]);
expect(
resolveGitWorktreeAddArgs({
branchName: "feature-branch",
targetPath: "/tmp/feature-branch",
branchExists: true,
}),
).toEqual(["worktree", "add", "/tmp/feature-branch", "feature-branch"]);
});
it("builds git worktree add args with a start point", () => {
expect(
resolveGitWorktreeAddArgs({
branchName: "my-worktree",
targetPath: "/tmp/my-worktree",
branchExists: false,
startPoint: "public-gh/master",
}),
).toEqual(["worktree", "add", "-b", "my-worktree", "/tmp/my-worktree", "public-gh/master"]);
});
it("uses start point even when a local branch with the same name exists", () => {
expect(
resolveGitWorktreeAddArgs({
branchName: "my-worktree",
targetPath: "/tmp/my-worktree",
branchExists: true,
startPoint: "origin/main",
}),
).toEqual(["worktree", "add", "-b", "my-worktree", "/tmp/my-worktree", "origin/main"]);
});
it("rewrites loopback auth URLs to the new port only", () => {
expect(rewriteLocalUrlPort("http://127.0.0.1:3100", 3110)).toBe("http://127.0.0.1:3110/");
expect(rewriteLocalUrlPort("https://paperclip.example", 3110)).toBe("https://paperclip.example");
});
it("builds isolated config and env paths for a worktree", () => {
const paths = resolveWorktreeLocalPaths({
cwd: "/tmp/paperclip-feature",
homeDir: "/tmp/paperclip-worktrees",
instanceId: "feature-worktree-support",
});
const config = buildWorktreeConfig({
sourceConfig: buildSourceConfig(),
paths,
serverPort: 3110,
databasePort: 54339,
now: new Date("2026-03-09T12:00:00.000Z"),
});
expect(config.database.embeddedPostgresDataDir).toBe(
path.resolve("/tmp/paperclip-worktrees", "instances", "feature-worktree-support", "db"),
);
expect(config.database.embeddedPostgresPort).toBe(54339);
expect(config.server.port).toBe(3110);
expect(config.auth.publicBaseUrl).toBe("http://127.0.0.1:3110/");
expect(config.storage.localDisk.baseDir).toBe(
path.resolve("/tmp/paperclip-worktrees", "instances", "feature-worktree-support", "data", "storage"),
);
const env = buildWorktreeEnvEntries(paths, {
name: "feature-worktree-support",
color: "#3abf7a",
});
expect(env.PAPERCLIP_HOME).toBe(path.resolve("/tmp/paperclip-worktrees"));
expect(env.PAPERCLIP_INSTANCE_ID).toBe("feature-worktree-support");
expect(env.PAPERCLIP_IN_WORKTREE).toBe("true");
expect(env.PAPERCLIP_WORKTREE_NAME).toBe("feature-worktree-support");
expect(env.PAPERCLIP_WORKTREE_COLOR).toBe("#3abf7a");
expect(formatShellExports(env)).toContain("export PAPERCLIP_INSTANCE_ID='feature-worktree-support'");
});
it("falls back across storage roots before skipping a missing attachment object", async () => {
const missingErr = Object.assign(new Error("missing"), { code: "ENOENT" });
const expected = Buffer.from("image-bytes");
await expect(
readSourceAttachmentBody(
[
{
getObject: vi.fn().mockRejectedValue(missingErr),
},
{
getObject: vi.fn().mockResolvedValue(expected),
},
],
"company-1",
"company-1/issues/issue-1/missing.png",
),
).resolves.toEqual(expected);
});
it("returns null when an attachment object is missing from every lookup storage", async () => {
const missingErr = Object.assign(new Error("missing"), { code: "ENOENT" });
await expect(
readSourceAttachmentBody(
[
{
getObject: vi.fn().mockRejectedValue(missingErr),
},
{
getObject: vi.fn().mockRejectedValue(Object.assign(new Error("missing"), { status: 404 })),
},
],
"company-1",
"company-1/issues/issue-1/missing.png",
),
).resolves.toBeNull();
});
it("generates vivid worktree colors as hex", () => {
expect(generateWorktreeColor()).toMatch(/^#[0-9a-f]{6}$/);
});
it("uses minimal seed mode to keep app state but drop heavy runtime history", () => {
const minimal = resolveWorktreeSeedPlan("minimal");
const full = resolveWorktreeSeedPlan("full");
expect(minimal.excludedTables).toContain("heartbeat_runs");
expect(minimal.excludedTables).toContain("heartbeat_run_events");
expect(minimal.excludedTables).toContain("workspace_runtime_services");
expect(minimal.excludedTables).toContain("agent_task_sessions");
expect(minimal.nullifyColumns.issues).toEqual(["checkout_run_id", "execution_run_id"]);
expect(full.excludedTables).toEqual([]);
expect(full.nullifyColumns).toEqual({});
});
it("copies the source local_encrypted secrets key into the seeded worktree instance", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-"));
const originalInlineMasterKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY;
const originalKeyFile = process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
try {
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY;
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
const sourceConfigPath = path.join(tempRoot, "source", "config.json");
const sourceKeyPath = path.join(tempRoot, "source", "secrets", "master.key");
const targetKeyPath = path.join(tempRoot, "target", "secrets", "master.key");
fs.mkdirSync(path.dirname(sourceKeyPath), { recursive: true });
fs.writeFileSync(sourceKeyPath, "source-master-key", "utf8");
const sourceConfig = buildSourceConfig();
sourceConfig.secrets.localEncrypted.keyFilePath = sourceKeyPath;
copySeededSecretsKey({
sourceConfigPath,
sourceConfig,
sourceEnvEntries: {},
targetKeyFilePath: targetKeyPath,
});
expect(fs.readFileSync(targetKeyPath, "utf8")).toBe("source-master-key");
} finally {
if (originalInlineMasterKey === undefined) {
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY;
} else {
process.env.PAPERCLIP_SECRETS_MASTER_KEY = originalInlineMasterKey;
}
if (originalKeyFile === undefined) {
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
} else {
process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE = originalKeyFile;
}
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
it("writes the source inline secrets master key into the seeded worktree instance", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-"));
try {
const sourceConfigPath = path.join(tempRoot, "source", "config.json");
const targetKeyPath = path.join(tempRoot, "target", "secrets", "master.key");
copySeededSecretsKey({
sourceConfigPath,
sourceConfig: buildSourceConfig(),
sourceEnvEntries: {
PAPERCLIP_SECRETS_MASTER_KEY: "inline-source-master-key",
},
targetKeyFilePath: targetKeyPath,
});
expect(fs.readFileSync(targetKeyPath, "utf8")).toBe("inline-source-master-key");
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
it("persists the current agent jwt secret into the worktree env file", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-jwt-"));
const repoRoot = path.join(tempRoot, "repo");
const originalCwd = process.cwd();
const originalJwtSecret = process.env.PAPERCLIP_AGENT_JWT_SECRET;
try {
fs.mkdirSync(repoRoot, { recursive: true });
process.env.PAPERCLIP_AGENT_JWT_SECRET = "worktree-shared-secret";
process.chdir(repoRoot);
await worktreeInitCommand({
seed: false,
fromConfig: path.join(tempRoot, "missing", "config.json"),
home: path.join(tempRoot, ".paperclip-worktrees"),
});
const envPath = path.join(repoRoot, ".paperclip", ".env");
const envContents = fs.readFileSync(envPath, "utf8");
expect(envContents).toContain("PAPERCLIP_AGENT_JWT_SECRET=worktree-shared-secret");
expect(envContents).toContain("PAPERCLIP_WORKTREE_NAME=repo");
expect(envContents).toMatch(/PAPERCLIP_WORKTREE_COLOR=\"#[0-9a-f]{6}\"/);
} finally {
process.chdir(originalCwd);
if (originalJwtSecret === undefined) {
delete process.env.PAPERCLIP_AGENT_JWT_SECRET;
} else {
process.env.PAPERCLIP_AGENT_JWT_SECRET = originalJwtSecret;
}
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
it("avoids ports already claimed by sibling worktree instance configs", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-claimed-ports-"));
const repoRoot = path.join(tempRoot, "repo");
const homeDir = path.join(tempRoot, ".paperclip-worktrees");
const siblingInstanceRoot = path.join(homeDir, "instances", "existing-worktree");
const originalCwd = process.cwd();
try {
fs.mkdirSync(repoRoot, { recursive: true });
fs.mkdirSync(siblingInstanceRoot, { recursive: true });
fs.writeFileSync(
path.join(siblingInstanceRoot, "config.json"),
JSON.stringify(
{
...buildSourceConfig(),
database: {
mode: "embedded-postgres",
embeddedPostgresDataDir: path.join(siblingInstanceRoot, "db"),
embeddedPostgresPort: 54330,
backup: {
enabled: true,
intervalMinutes: 60,
retentionDays: 30,
dir: path.join(siblingInstanceRoot, "backups"),
},
},
logging: {
mode: "file",
logDir: path.join(siblingInstanceRoot, "logs"),
},
server: {
deploymentMode: "authenticated",
exposure: "private",
host: "127.0.0.1",
port: 3101,
allowedHostnames: ["localhost"],
serveUi: true,
},
storage: {
provider: "local_disk",
localDisk: {
baseDir: path.join(siblingInstanceRoot, "storage"),
},
s3: {
bucket: "paperclip",
region: "us-east-1",
prefix: "",
forcePathStyle: false,
},
},
secrets: {
provider: "local_encrypted",
strictMode: false,
localEncrypted: {
keyFilePath: path.join(siblingInstanceRoot, "secrets", "master.key"),
},
},
},
null,
2,
) + "\n",
);
process.chdir(repoRoot);
await worktreeInitCommand({
seed: false,
fromConfig: path.join(tempRoot, "missing", "config.json"),
home: homeDir,
});
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, ".paperclip", "config.json"), "utf8"));
expect(config.server.port).toBeGreaterThan(3101);
expect(config.database.embeddedPostgresPort).not.toBe(54330);
expect(config.database.embeddedPostgresPort).not.toBe(config.server.port);
expect(config.database.embeddedPostgresPort).toBeGreaterThan(54330);
} finally {
process.chdir(originalCwd);
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
it("defaults the seed source config to the current repo-local Paperclip config", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-source-config-"));
const repoRoot = path.join(tempRoot, "repo");
const localConfigPath = path.join(repoRoot, ".paperclip", "config.json");
const originalCwd = process.cwd();
const originalPaperclipConfig = process.env.PAPERCLIP_CONFIG;
try {
fs.mkdirSync(path.dirname(localConfigPath), { recursive: true });
fs.writeFileSync(localConfigPath, JSON.stringify(buildSourceConfig()), "utf8");
delete process.env.PAPERCLIP_CONFIG;
process.chdir(repoRoot);
expect(fs.realpathSync(resolveSourceConfigPath({}))).toBe(fs.realpathSync(localConfigPath));
} finally {
process.chdir(originalCwd);
if (originalPaperclipConfig === undefined) {
delete process.env.PAPERCLIP_CONFIG;
} else {
process.env.PAPERCLIP_CONFIG = originalPaperclipConfig;
}
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
it("preserves the source config path across worktree:make cwd changes", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-source-override-"));
const sourceConfigPath = path.join(tempRoot, "source", "config.json");
const targetRoot = path.join(tempRoot, "target");
const originalCwd = process.cwd();
const originalPaperclipConfig = process.env.PAPERCLIP_CONFIG;
try {
fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true });
fs.mkdirSync(targetRoot, { recursive: true });
fs.writeFileSync(sourceConfigPath, JSON.stringify(buildSourceConfig()), "utf8");
delete process.env.PAPERCLIP_CONFIG;
process.chdir(targetRoot);
expect(resolveSourceConfigPath({ sourceConfigPathOverride: sourceConfigPath })).toBe(
path.resolve(sourceConfigPath),
);
} finally {
process.chdir(originalCwd);
if (originalPaperclipConfig === undefined) {
delete process.env.PAPERCLIP_CONFIG;
} else {
process.env.PAPERCLIP_CONFIG = originalPaperclipConfig;
}
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
it("requires an explicit reseed source", () => {
expect(() => resolveWorktreeReseedSource({})).toThrow(
"Pass --from <worktree> or --from-config/--from-instance explicitly so the reseed source is unambiguous.",
);
});
it("rejects mixed reseed source selectors", () => {
expect(() => resolveWorktreeReseedSource({
from: "current",
fromInstance: "default",
})).toThrow(
"Use either --from <worktree> or --from-config/--from-data-dir/--from-instance, not both.",
);
});
it("derives worktree reseed target paths from the adjacent env file", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-target-"));
const worktreeRoot = path.join(tempRoot, "repo");
const configPath = path.join(worktreeRoot, ".paperclip", "config.json");
const envPath = path.join(worktreeRoot, ".paperclip", ".env");
try {
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, JSON.stringify(buildSourceConfig()), "utf8");
fs.writeFileSync(
envPath,
[
"PAPERCLIP_HOME=/tmp/paperclip-worktrees",
"PAPERCLIP_INSTANCE_ID=pap-1132-chat",
].join("\n"),
"utf8",
);
expect(
resolveWorktreeReseedTargetPaths({
configPath,
rootPath: worktreeRoot,
}),
).toMatchObject({
cwd: worktreeRoot,
homeDir: "/tmp/paperclip-worktrees",
instanceId: "pap-1132-chat",
});
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
it("rejects reseed targets without worktree env metadata", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-target-missing-"));
const worktreeRoot = path.join(tempRoot, "repo");
const configPath = path.join(worktreeRoot, ".paperclip", "config.json");
try {
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, JSON.stringify(buildSourceConfig()), "utf8");
fs.writeFileSync(path.join(worktreeRoot, ".paperclip", ".env"), "", "utf8");
expect(() =>
resolveWorktreeReseedTargetPaths({
configPath,
rootPath: worktreeRoot,
})).toThrow("does not look like a worktree-local Paperclip instance");
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
it("reseed preserves the current worktree ports, instance id, and branding", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-"));
const repoRoot = path.join(tempRoot, "repo");
const sourceRoot = path.join(tempRoot, "source");
const homeDir = path.join(tempRoot, ".paperclip-worktrees");
const currentInstanceId = "existing-worktree";
const currentPaths = resolveWorktreeLocalPaths({
cwd: repoRoot,
homeDir,
instanceId: currentInstanceId,
});
const sourcePaths = resolveWorktreeLocalPaths({
cwd: sourceRoot,
homeDir: path.join(tempRoot, ".paperclip-source"),
instanceId: "default",
});
const originalCwd = process.cwd();
const originalPaperclipConfig = process.env.PAPERCLIP_CONFIG;
try {
fs.mkdirSync(path.dirname(currentPaths.configPath), { recursive: true });
fs.mkdirSync(path.dirname(sourcePaths.configPath), { recursive: true });
fs.mkdirSync(path.dirname(sourcePaths.secretsKeyFilePath), { recursive: true });
fs.mkdirSync(repoRoot, { recursive: true });
fs.mkdirSync(sourceRoot, { recursive: true });
const currentConfig = buildWorktreeConfig({
sourceConfig: buildSourceConfig(),
paths: currentPaths,
serverPort: 3114,
databasePort: 54341,
});
const sourceConfig = buildWorktreeConfig({
sourceConfig: buildSourceConfig(),
paths: sourcePaths,
serverPort: 3200,
databasePort: 54400,
});
fs.writeFileSync(currentPaths.configPath, JSON.stringify(currentConfig, null, 2), "utf8");
fs.writeFileSync(sourcePaths.configPath, JSON.stringify(sourceConfig, null, 2), "utf8");
fs.writeFileSync(sourcePaths.secretsKeyFilePath, "source-secret", "utf8");
fs.writeFileSync(
currentPaths.envPath,
[
`PAPERCLIP_HOME=${homeDir}`,
`PAPERCLIP_INSTANCE_ID=${currentInstanceId}`,
"PAPERCLIP_WORKTREE_NAME=existing-name",
"PAPERCLIP_WORKTREE_COLOR=\"#112233\"",
].join("\n"),
"utf8",
);
delete process.env.PAPERCLIP_CONFIG;
process.chdir(repoRoot);
await worktreeReseedCommand({
fromConfig: sourcePaths.configPath,
yes: true,
});
const rewrittenConfig = JSON.parse(fs.readFileSync(currentPaths.configPath, "utf8"));
const rewrittenEnv = fs.readFileSync(currentPaths.envPath, "utf8");
expect(rewrittenConfig.server.port).toBe(3114);
expect(rewrittenConfig.database.embeddedPostgresPort).toBe(54341);
expect(rewrittenConfig.database.embeddedPostgresDataDir).toBe(currentPaths.embeddedPostgresDataDir);
expect(rewrittenEnv).toContain(`PAPERCLIP_INSTANCE_ID=${currentInstanceId}`);
expect(rewrittenEnv).toContain("PAPERCLIP_WORKTREE_NAME=existing-name");
expect(rewrittenEnv).toContain("PAPERCLIP_WORKTREE_COLOR=\"#112233\"");
} finally {
process.chdir(originalCwd);
if (originalPaperclipConfig === undefined) {
delete process.env.PAPERCLIP_CONFIG;
} else {
process.env.PAPERCLIP_CONFIG = originalPaperclipConfig;
}
fs.rmSync(tempRoot, { recursive: true, force: true });
}
}, 20_000);
it("restores the current worktree config and instance data if reseed fails", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-rollback-"));
const repoRoot = path.join(tempRoot, "repo");
const sourceRoot = path.join(tempRoot, "source");
const homeDir = path.join(tempRoot, ".paperclip-worktrees");
const currentInstanceId = "rollback-worktree";
const currentPaths = resolveWorktreeLocalPaths({
cwd: repoRoot,
homeDir,
instanceId: currentInstanceId,
});
const sourcePaths = resolveWorktreeLocalPaths({
cwd: sourceRoot,
homeDir: path.join(tempRoot, ".paperclip-source"),
instanceId: "default",
});
const originalCwd = process.cwd();
const originalPaperclipConfig = process.env.PAPERCLIP_CONFIG;
try {
fs.mkdirSync(path.dirname(currentPaths.configPath), { recursive: true });
fs.mkdirSync(path.dirname(sourcePaths.configPath), { recursive: true });
fs.mkdirSync(currentPaths.instanceRoot, { recursive: true });
fs.mkdirSync(path.dirname(sourcePaths.secretsKeyFilePath), { recursive: true });
fs.mkdirSync(repoRoot, { recursive: true });
fs.mkdirSync(sourceRoot, { recursive: true });
const currentConfig = buildWorktreeConfig({
sourceConfig: buildSourceConfig(),
paths: currentPaths,
serverPort: 3114,
databasePort: 54341,
});
const sourceConfig = {
...buildSourceConfig(),
database: {
mode: "postgres",
connectionString: "",
},
secrets: {
provider: "local_encrypted",
strictMode: false,
localEncrypted: {
keyFilePath: sourcePaths.secretsKeyFilePath,
},
},
} as PaperclipConfig;
fs.writeFileSync(currentPaths.configPath, JSON.stringify(currentConfig, null, 2), "utf8");
fs.writeFileSync(currentPaths.envPath, `PAPERCLIP_HOME=${homeDir}\nPAPERCLIP_INSTANCE_ID=${currentInstanceId}\n`, "utf8");
fs.writeFileSync(path.join(currentPaths.instanceRoot, "marker.txt"), "keep me", "utf8");
fs.writeFileSync(sourcePaths.configPath, JSON.stringify(sourceConfig, null, 2), "utf8");
fs.writeFileSync(sourcePaths.secretsKeyFilePath, "source-secret", "utf8");
delete process.env.PAPERCLIP_CONFIG;
process.chdir(repoRoot);
await expect(worktreeReseedCommand({
fromConfig: sourcePaths.configPath,
yes: true,
})).rejects.toThrow("Source instance uses postgres mode but has no connection string");
const restoredConfig = JSON.parse(fs.readFileSync(currentPaths.configPath, "utf8"));
const restoredEnv = fs.readFileSync(currentPaths.envPath, "utf8");
const restoredMarker = fs.readFileSync(path.join(currentPaths.instanceRoot, "marker.txt"), "utf8");
expect(restoredConfig.server.port).toBe(3114);
expect(restoredConfig.database.embeddedPostgresPort).toBe(54341);
expect(restoredEnv).toContain(`PAPERCLIP_INSTANCE_ID=${currentInstanceId}`);
expect(restoredMarker).toBe("keep me");
} finally {
process.chdir(originalCwd);
if (originalPaperclipConfig === undefined) {
delete process.env.PAPERCLIP_CONFIG;
} else {
process.env.PAPERCLIP_CONFIG = originalPaperclipConfig;
}
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
it("rebinds same-repo workspace paths onto the current worktree root", () => {
expect(
rebindWorkspaceCwd({
sourceRepoRoot: "/Users/example/paperclip",
targetRepoRoot: "/Users/example/paperclip-pr-432",
workspaceCwd: "/Users/example/paperclip",
}),
).toBe("/Users/example/paperclip-pr-432");
expect(
rebindWorkspaceCwd({
sourceRepoRoot: "/Users/example/paperclip",
targetRepoRoot: "/Users/example/paperclip-pr-432",
workspaceCwd: "/Users/example/paperclip/packages/db",
}),
).toBe("/Users/example/paperclip-pr-432/packages/db");
});
it("does not rebind paths outside the source repo root", () => {
expect(
rebindWorkspaceCwd({
sourceRepoRoot: "/Users/example/paperclip",
targetRepoRoot: "/Users/example/paperclip-pr-432",
workspaceCwd: "/Users/example/other-project",
}),
).toBeNull();
});
it("copies shared git hooks into a linked worktree git dir", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-hooks-"));
const repoRoot = path.join(tempRoot, "repo");
const worktreePath = path.join(tempRoot, "repo-feature");
try {
fs.mkdirSync(repoRoot, { recursive: true });
execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" });
fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8");
execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" });
const sourceHooksDir = path.join(repoRoot, ".git", "hooks");
const sourceHookPath = path.join(sourceHooksDir, "pre-commit");
const sourceTokensPath = path.join(sourceHooksDir, "forbidden-tokens.txt");
fs.writeFileSync(sourceHookPath, "#!/usr/bin/env bash\nexit 0\n", { encoding: "utf8", mode: 0o755 });
fs.chmodSync(sourceHookPath, 0o755);
fs.writeFileSync(sourceTokensPath, "secret-token\n", "utf8");
execFileSync("git", ["worktree", "add", "--detach", worktreePath], { cwd: repoRoot, stdio: "ignore" });
const copied = copyGitHooksToWorktreeGitDir(worktreePath);
const worktreeGitDir = execFileSync("git", ["rev-parse", "--git-dir"], {
cwd: worktreePath,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
const resolvedSourceHooksDir = fs.realpathSync(sourceHooksDir);
const resolvedTargetHooksDir = fs.realpathSync(path.resolve(worktreePath, worktreeGitDir, "hooks"));
const targetHookPath = path.join(resolvedTargetHooksDir, "pre-commit");
const targetTokensPath = path.join(resolvedTargetHooksDir, "forbidden-tokens.txt");
expect(copied).toMatchObject({
sourceHooksPath: resolvedSourceHooksDir,
targetHooksPath: resolvedTargetHooksDir,
copied: true,
});
expect(fs.readFileSync(targetHookPath, "utf8")).toBe("#!/usr/bin/env bash\nexit 0\n");
expect(fs.statSync(targetHookPath).mode & 0o111).not.toBe(0);
expect(fs.readFileSync(targetTokensPath, "utf8")).toBe("secret-token\n");
} finally {
execFileSync("git", ["worktree", "remove", "--force", worktreePath], { cwd: repoRoot, stdio: "ignore" });
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
it("creates and initializes a worktree from the top-level worktree:make command", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-make-"));
const repoRoot = path.join(tempRoot, "repo");
const fakeHome = path.join(tempRoot, "home");
const worktreePath = path.join(fakeHome, "paperclip-make-test");
const originalCwd = process.cwd();
const homedirSpy = vi.spyOn(os, "homedir").mockReturnValue(fakeHome);
try {
fs.mkdirSync(repoRoot, { recursive: true });
fs.mkdirSync(fakeHome, { recursive: true });
execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" });
fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8");
execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" });
process.chdir(repoRoot);
await worktreeMakeCommand("paperclip-make-test", {
seed: false,
home: path.join(tempRoot, ".paperclip-worktrees"),
});
expect(fs.existsSync(path.join(worktreePath, ".git"))).toBe(true);
expect(fs.existsSync(path.join(worktreePath, ".paperclip", "config.json"))).toBe(true);
expect(fs.existsSync(path.join(worktreePath, ".paperclip", ".env"))).toBe(true);
} finally {
process.chdir(originalCwd);
homedirSpy.mockRestore();
fs.rmSync(tempRoot, { recursive: true, force: true });
}
}, 20_000);
});
describeEmbeddedPostgres("pauseSeededScheduledRoutines", () => {
it("pauses only routines with enabled schedule triggers", async () => {
const tempDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-routines-");
const db = createDb(tempDb.connectionString);
const companyId = randomUUID();
const projectId = randomUUID();
const agentId = randomUUID();
const activeScheduledRoutineId = randomUUID();
const activeApiRoutineId = randomUUID();
const pausedScheduledRoutineId = randomUUID();
const archivedScheduledRoutineId = randomUUID();
const disabledScheduleRoutineId = randomUUID();
try {
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(agents).values({
id: agentId,
companyId,
name: "Coder",
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(projects).values({
id: projectId,
companyId,
name: "Project",
status: "in_progress",
});
await db.insert(routines).values([
{
id: activeScheduledRoutineId,
companyId,
projectId,
assigneeAgentId: agentId,
title: "Active scheduled",
status: "active",
},
{
id: activeApiRoutineId,
companyId,
projectId,
assigneeAgentId: agentId,
title: "Active API",
status: "active",
},
{
id: pausedScheduledRoutineId,
companyId,
projectId,
assigneeAgentId: agentId,
title: "Paused scheduled",
status: "paused",
},
{
id: archivedScheduledRoutineId,
companyId,
projectId,
assigneeAgentId: agentId,
title: "Archived scheduled",
status: "archived",
},
{
id: disabledScheduleRoutineId,
companyId,
projectId,
assigneeAgentId: agentId,
title: "Disabled schedule",
status: "active",
},
]);
await db.insert(routineTriggers).values([
{
companyId,
routineId: activeScheduledRoutineId,
kind: "schedule",
enabled: true,
cronExpression: "0 9 * * *",
timezone: "UTC",
},
{
companyId,
routineId: activeApiRoutineId,
kind: "api",
enabled: true,
},
{
companyId,
routineId: pausedScheduledRoutineId,
kind: "schedule",
enabled: true,
cronExpression: "0 10 * * *",
timezone: "UTC",
},
{
companyId,
routineId: archivedScheduledRoutineId,
kind: "schedule",
enabled: true,
cronExpression: "0 11 * * *",
timezone: "UTC",
},
{
companyId,
routineId: disabledScheduleRoutineId,
kind: "schedule",
enabled: false,
cronExpression: "0 12 * * *",
timezone: "UTC",
},
]);
const pausedCount = await pauseSeededScheduledRoutines(tempDb.connectionString);
expect(pausedCount).toBe(1);
const rows = await db.select({ id: routines.id, status: routines.status }).from(routines);
const statusById = new Map(rows.map((row) => [row.id, row.status]));
expect(statusById.get(activeScheduledRoutineId)).toBe("paused");
expect(statusById.get(activeApiRoutineId)).toBe("active");
expect(statusById.get(pausedScheduledRoutineId)).toBe("paused");
expect(statusById.get(archivedScheduledRoutineId)).toBe("archived");
expect(statusById.get(disabledScheduleRoutineId)).toBe("active");
} finally {
await db.$client?.end?.({ timeout: 5 }).catch(() => undefined);
await tempDb.cleanup();
}
}, 20_000);
});

View File

@@ -1,7 +1,11 @@
import type { CLIAdapterModule } from "@paperclipai/adapter-utils";
import { printClaudeStreamEvent } from "@paperclipai/adapter-claude-local/cli";
import { printCodexStreamEvent } from "@paperclipai/adapter-codex-local/cli";
import { printOpenClawStreamEvent } from "@paperclipai/adapter-openclaw/cli";
import { printCursorStreamEvent } from "@paperclipai/adapter-cursor-local/cli";
import { printGeminiStreamEvent } from "@paperclipai/adapter-gemini-local/cli";
import { printOpenCodeStreamEvent } from "@paperclipai/adapter-opencode-local/cli";
import { printPiStreamEvent } from "@paperclipai/adapter-pi-local/cli";
import { printOpenClawGatewayStreamEvent } from "@paperclipai/adapter-openclaw-gateway/cli";
import { processCLIAdapter } from "./process/index.js";
import { httpCLIAdapter } from "./http/index.js";
@@ -15,13 +19,43 @@ const codexLocalCLIAdapter: CLIAdapterModule = {
formatStdoutEvent: printCodexStreamEvent,
};
const openclawCLIAdapter: CLIAdapterModule = {
type: "openclaw",
formatStdoutEvent: printOpenClawStreamEvent,
const openCodeLocalCLIAdapter: CLIAdapterModule = {
type: "opencode_local",
formatStdoutEvent: printOpenCodeStreamEvent,
};
const piLocalCLIAdapter: CLIAdapterModule = {
type: "pi_local",
formatStdoutEvent: printPiStreamEvent,
};
const cursorLocalCLIAdapter: CLIAdapterModule = {
type: "cursor",
formatStdoutEvent: printCursorStreamEvent,
};
const geminiLocalCLIAdapter: CLIAdapterModule = {
type: "gemini_local",
formatStdoutEvent: printGeminiStreamEvent,
};
const openclawGatewayCLIAdapter: CLIAdapterModule = {
type: "openclaw_gateway",
formatStdoutEvent: printOpenClawGatewayStreamEvent,
};
const adaptersByType = new Map<string, CLIAdapterModule>(
[claudeLocalCLIAdapter, codexLocalCLIAdapter, openclawCLIAdapter, processCLIAdapter, httpCLIAdapter].map((a) => [a.type, a]),
[
claudeLocalCLIAdapter,
codexLocalCLIAdapter,
openCodeLocalCLIAdapter,
piLocalCLIAdapter,
cursorLocalCLIAdapter,
geminiLocalCLIAdapter,
openclawGatewayCLIAdapter,
processCLIAdapter,
httpCLIAdapter,
].map((a) => [a.type, a]),
);
export function getCLIAdapter(type: string): CLIAdapterModule {

View File

@@ -39,15 +39,7 @@ export async function databaseCheck(config: PaperclipConfig, configPath?: string
const dataDir = resolveRuntimeLikePath(config.database.embeddedPostgresDataDir, configPath);
const reportedPath = dataDir;
if (!fs.existsSync(dataDir)) {
return {
name: "Database",
status: "warn",
message: `Embedded PostgreSQL data directory does not exist: ${reportedPath}`,
canRepair: true,
repair: () => {
fs.mkdirSync(reportedPath, { recursive: true });
},
};
fs.mkdirSync(reportedPath, { recursive: true });
}
return {

View File

@@ -1,24 +1,21 @@
import { inferBindModeFromHost } from "@paperclipai/shared";
import type { PaperclipConfig } from "../config/schema.js";
import type { CheckResult } from "./index.js";
function isLoopbackHost(host: string) {
const normalized = host.trim().toLowerCase();
return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
}
export function deploymentAuthCheck(config: PaperclipConfig): CheckResult {
const mode = config.server.deploymentMode;
const exposure = config.server.exposure;
const auth = config.auth;
const bind = config.server.bind ?? inferBindModeFromHost(config.server.host);
if (mode === "local_trusted") {
if (!isLoopbackHost(config.server.host)) {
if (bind !== "loopback") {
return {
name: "Deployment/auth mode",
status: "fail",
message: `local_trusted requires loopback host binding (found ${config.server.host})`,
message: `local_trusted requires loopback binding (found ${bind})`,
canRepair: false,
repairHint: "Run `paperclipai configure --section server` and set host to 127.0.0.1",
repairHint: "Run `paperclipai configure --section server` and choose Local trusted / loopback reachability",
};
}
return {
@@ -86,6 +83,6 @@ export function deploymentAuthCheck(config: PaperclipConfig): CheckResult {
return {
name: "Deployment/auth mode",
status: "pass",
message: `Mode ${mode}/${exposure} with auth URL mode ${auth.baseUrlMode}`,
message: `Mode ${mode}/${exposure} with bind ${bind} and auth URL mode ${auth.baseUrlMode}`,
};
}

View File

@@ -5,20 +5,16 @@ export async function llmCheck(config: PaperclipConfig): Promise<CheckResult> {
if (!config.llm) {
return {
name: "LLM provider",
status: "warn",
message: "No LLM provider configured",
canRepair: false,
repairHint: "Run `paperclipai configure --section llm` to set one up",
status: "pass",
message: "No LLM provider configured (optional)",
};
}
if (!config.llm.apiKey) {
return {
name: "LLM provider",
status: "warn",
message: `${config.llm.provider} configured but no API key set`,
canRepair: false,
repairHint: "Run `paperclipai configure --section llm`",
status: "pass",
message: `${config.llm.provider} configured but no API key set (optional)`,
};
}

View File

@@ -8,15 +8,7 @@ export function logCheck(config: PaperclipConfig, configPath?: string): CheckRes
const reportedDir = logDir;
if (!fs.existsSync(logDir)) {
return {
name: "Log directory",
status: "warn",
message: `Log directory does not exist: ${reportedDir}`,
canRepair: true,
repair: () => {
fs.mkdirSync(reportedDir, { recursive: true });
},
};
fs.mkdirSync(reportedDir, { recursive: true });
}
try {

View File

@@ -7,16 +7,7 @@ export function storageCheck(config: PaperclipConfig, configPath?: string): Chec
if (config.storage.provider === "local_disk") {
const baseDir = resolveRuntimeLikePath(config.storage.localDisk.baseDir, configPath);
if (!fs.existsSync(baseDir)) {
return {
name: "Storage",
status: "warn",
message: `Local storage directory does not exist: ${baseDir}`,
canRepair: true,
repair: () => {
fs.mkdirSync(baseDir, { recursive: true });
},
repairHint: "Run with --repair to create local storage directory",
};
fs.mkdirSync(baseDir, { recursive: true });
}
try {

View File

@@ -0,0 +1,282 @@
import { spawn } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import pc from "picocolors";
import { buildCliCommandLabel } from "./command-label.js";
import { resolveDefaultCliAuthPath } from "../config/home.js";
type RequestedAccess = "board" | "instance_admin_required";
interface BoardAuthCredential {
apiBase: string;
token: string;
createdAt: string;
updatedAt: string;
userId?: string | null;
}
interface BoardAuthStore {
version: 1;
credentials: Record<string, BoardAuthCredential>;
}
interface CreateChallengeResponse {
id: string;
token: string;
boardApiToken: string;
approvalPath: string;
approvalUrl: string | null;
pollPath: string;
expiresAt: string;
suggestedPollIntervalMs: number;
}
interface ChallengeStatusResponse {
id: string;
status: "pending" | "approved" | "cancelled" | "expired";
command: string;
clientName: string | null;
requestedAccess: RequestedAccess;
requestedCompanyId: string | null;
requestedCompanyName: string | null;
approvedAt: string | null;
cancelledAt: string | null;
expiresAt: string;
approvedByUser: { id: string; name: string; email: string } | null;
}
function defaultBoardAuthStore(): BoardAuthStore {
return {
version: 1,
credentials: {},
};
}
function toStringOrNull(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function normalizeApiBase(apiBase: string): string {
return apiBase.trim().replace(/\/+$/, "");
}
export function resolveBoardAuthStorePath(overridePath?: string): string {
if (overridePath?.trim()) return path.resolve(overridePath.trim());
if (process.env.PAPERCLIP_AUTH_STORE?.trim()) return path.resolve(process.env.PAPERCLIP_AUTH_STORE.trim());
return resolveDefaultCliAuthPath();
}
export function readBoardAuthStore(storePath?: string): BoardAuthStore {
const filePath = resolveBoardAuthStorePath(storePath);
if (!fs.existsSync(filePath)) return defaultBoardAuthStore();
const raw = JSON.parse(fs.readFileSync(filePath, "utf8")) as Partial<BoardAuthStore> | null;
const credentials = raw?.credentials && typeof raw.credentials === "object" ? raw.credentials : {};
const normalized: Record<string, BoardAuthCredential> = {};
for (const [key, value] of Object.entries(credentials)) {
if (typeof value !== "object" || value === null) continue;
const record = value as unknown as Record<string, unknown>;
const apiBase = toStringOrNull(record.apiBase);
const token = toStringOrNull(record.token);
const createdAt = toStringOrNull(record.createdAt);
const updatedAt = toStringOrNull(record.updatedAt);
if (!apiBase || !token || !createdAt || !updatedAt) continue;
normalized[normalizeApiBase(key)] = {
apiBase,
token,
createdAt,
updatedAt,
userId: toStringOrNull(record.userId),
};
}
return {
version: 1,
credentials: normalized,
};
}
export function writeBoardAuthStore(store: BoardAuthStore, storePath?: string): void {
const filePath = resolveBoardAuthStorePath(storePath);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
}
export function getStoredBoardCredential(apiBase: string, storePath?: string): BoardAuthCredential | null {
const store = readBoardAuthStore(storePath);
return store.credentials[normalizeApiBase(apiBase)] ?? null;
}
export function setStoredBoardCredential(input: {
apiBase: string;
token: string;
userId?: string | null;
storePath?: string;
}): BoardAuthCredential {
const normalizedApiBase = normalizeApiBase(input.apiBase);
const store = readBoardAuthStore(input.storePath);
const now = new Date().toISOString();
const existing = store.credentials[normalizedApiBase];
const credential: BoardAuthCredential = {
apiBase: normalizedApiBase,
token: input.token.trim(),
createdAt: existing?.createdAt ?? now,
updatedAt: now,
userId: input.userId ?? existing?.userId ?? null,
};
store.credentials[normalizedApiBase] = credential;
writeBoardAuthStore(store, input.storePath);
return credential;
}
export function removeStoredBoardCredential(apiBase: string, storePath?: string): boolean {
const normalizedApiBase = normalizeApiBase(apiBase);
const store = readBoardAuthStore(storePath);
if (!store.credentials[normalizedApiBase]) return false;
delete store.credentials[normalizedApiBase];
writeBoardAuthStore(store, storePath);
return true;
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function requestJson<T>(url: string, init?: RequestInit): Promise<T> {
const headers = new Headers(init?.headers ?? undefined);
if (init?.body !== undefined && !headers.has("content-type")) {
headers.set("content-type", "application/json");
}
if (!headers.has("accept")) {
headers.set("accept", "application/json");
}
const response = await fetch(url, {
...init,
headers,
});
if (!response.ok) {
const body = await response.json().catch(() => null);
const message =
body && typeof body === "object" && typeof (body as { error?: unknown }).error === "string"
? (body as { error: string }).error
: `Request failed: ${response.status}`;
throw new Error(message);
}
return response.json() as Promise<T>;
}
export function openUrl(url: string): boolean {
const platform = process.platform;
try {
if (platform === "darwin") {
const child = spawn("open", [url], { detached: true, stdio: "ignore" });
child.unref();
return true;
}
if (platform === "win32") {
const child = spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" });
child.unref();
return true;
}
const child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
child.unref();
return true;
} catch {
return false;
}
}
export async function loginBoardCli(params: {
apiBase: string;
requestedAccess: RequestedAccess;
requestedCompanyId?: string | null;
clientName?: string | null;
command?: string;
storePath?: string;
print?: boolean;
}): Promise<{ token: string; approvalUrl: string; userId?: string | null }> {
const apiBase = normalizeApiBase(params.apiBase);
const createUrl = `${apiBase}/api/cli-auth/challenges`;
const command = params.command?.trim() || buildCliCommandLabel();
const challenge = await requestJson<CreateChallengeResponse>(createUrl, {
method: "POST",
body: JSON.stringify({
command,
clientName: params.clientName?.trim() || "paperclipai cli",
requestedAccess: params.requestedAccess,
requestedCompanyId: params.requestedCompanyId?.trim() || null,
}),
});
const approvalUrl = challenge.approvalUrl ?? `${apiBase}${challenge.approvalPath}`;
if (params.print !== false) {
console.error(pc.bold("Board authentication required"));
console.error(`Open this URL in your browser to approve CLI access:\n${approvalUrl}`);
}
const opened = openUrl(approvalUrl);
if (params.print !== false && opened) {
console.error(pc.dim("Opened the approval page in your browser."));
}
const expiresAtMs = Date.parse(challenge.expiresAt);
const pollMs = Math.max(500, challenge.suggestedPollIntervalMs || 1000);
while (Number.isFinite(expiresAtMs) ? Date.now() < expiresAtMs : true) {
const status = await requestJson<ChallengeStatusResponse>(
`${apiBase}/api${challenge.pollPath}?token=${encodeURIComponent(challenge.token)}`,
);
if (status.status === "approved") {
const me = await requestJson<{ userId: string; user?: { id: string } | null }>(
`${apiBase}/api/cli-auth/me`,
{
headers: {
authorization: `Bearer ${challenge.boardApiToken}`,
},
},
);
setStoredBoardCredential({
apiBase,
token: challenge.boardApiToken,
userId: me.userId ?? me.user?.id ?? null,
storePath: params.storePath,
});
return {
token: challenge.boardApiToken,
approvalUrl,
userId: me.userId ?? me.user?.id ?? null,
};
}
if (status.status === "cancelled") {
throw new Error("CLI auth challenge was cancelled.");
}
if (status.status === "expired") {
throw new Error("CLI auth challenge expired before approval.");
}
await sleep(pollMs);
}
throw new Error("CLI auth challenge expired before approval.");
}
export async function revokeStoredBoardCredential(params: {
apiBase: string;
token: string;
}): Promise<void> {
const apiBase = normalizeApiBase(params.apiBase);
await requestJson<{ revoked: boolean }>(`${apiBase}/api/cli-auth/revoke-current`, {
method: "POST",
headers: {
authorization: `Bearer ${params.token}`,
},
body: JSON.stringify({}),
});
}

View File

@@ -0,0 +1,4 @@
export function buildCliCommandLabel(): string {
const args = process.argv.slice(2);
return args.length > 0 ? `paperclipai ${args.join(" ")}` : "paperclipai";
}

View File

@@ -13,25 +13,54 @@ export class ApiRequestError extends Error {
}
}
export class ApiConnectionError extends Error {
url: string;
method: string;
causeMessage?: string;
constructor(input: {
apiBase: string;
path: string;
method: string;
cause?: unknown;
}) {
const url = buildUrl(input.apiBase, input.path);
const causeMessage = formatConnectionCause(input.cause);
super(buildConnectionErrorMessage({ apiBase: input.apiBase, url, method: input.method, causeMessage }));
this.url = url;
this.method = input.method;
this.causeMessage = causeMessage;
}
}
interface RequestOptions {
ignoreNotFound?: boolean;
}
interface RecoverAuthInput {
path: string;
method: string;
error: ApiRequestError;
}
interface ApiClientOptions {
apiBase: string;
apiKey?: string;
runId?: string;
recoverAuth?: (input: RecoverAuthInput) => Promise<string | null>;
}
export class PaperclipApiClient {
readonly apiBase: string;
readonly apiKey?: string;
apiKey?: string;
readonly runId?: string;
readonly recoverAuth?: (input: RecoverAuthInput) => Promise<string | null>;
constructor(opts: ApiClientOptions) {
this.apiBase = opts.apiBase.replace(/\/+$/, "");
this.apiKey = opts.apiKey?.trim() || undefined;
this.runId = opts.runId?.trim() || undefined;
this.recoverAuth = opts.recoverAuth;
}
get<T>(path: string, opts?: RequestOptions): Promise<T | null> {
@@ -56,8 +85,18 @@ export class PaperclipApiClient {
return this.request<T>(path, { method: "DELETE" }, opts);
}
private async request<T>(path: string, init: RequestInit, opts?: RequestOptions): Promise<T | null> {
setApiKey(apiKey: string | undefined) {
this.apiKey = apiKey?.trim() || undefined;
}
private async request<T>(
path: string,
init: RequestInit,
opts?: RequestOptions,
hasRetriedAuth = false,
): Promise<T | null> {
const url = buildUrl(this.apiBase, path);
const method = String(init.method ?? "GET").toUpperCase();
const headers: Record<string, string> = {
accept: "application/json",
@@ -76,17 +115,39 @@ export class PaperclipApiClient {
headers["x-paperclip-run-id"] = this.runId;
}
const response = await fetch(url, {
...init,
headers,
});
let response: Response;
try {
response = await fetch(url, {
...init,
headers,
});
} catch (error) {
throw new ApiConnectionError({
apiBase: this.apiBase,
path,
method,
cause: error,
});
}
if (opts?.ignoreNotFound && response.status === 404) {
return null;
}
if (!response.ok) {
throw await toApiError(response);
const apiError = await toApiError(response);
if (!hasRetriedAuth && this.recoverAuth) {
const recoveredToken = await this.recoverAuth({
path,
method,
error: apiError,
});
if (recoveredToken) {
this.setApiKey(recoveredToken);
return this.request<T>(path, init, opts, true);
}
}
throw apiError;
}
if (response.status === 204) {
@@ -104,8 +165,10 @@ export class PaperclipApiClient {
function buildUrl(apiBase: string, path: string): string {
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
const [pathname, query] = normalizedPath.split("?");
const url = new URL(apiBase);
url.pathname = `${url.pathname.replace(/\/+$/, "")}${normalizedPath}`;
url.pathname = `${url.pathname.replace(/\/+$/, "")}${pathname}`;
if (query) url.search = query;
return url.toString();
}
@@ -134,6 +197,50 @@ async function toApiError(response: Response): Promise<ApiRequestError> {
return new ApiRequestError(response.status, `Request failed with status ${response.status}`, undefined, parsed);
}
function buildConnectionErrorMessage(input: {
apiBase: string;
url: string;
method: string;
causeMessage?: string;
}): string {
const healthUrl = buildHealthCheckUrl(input.url);
const lines = [
"Could not reach the Paperclip API.",
"",
`Request: ${input.method} ${input.url}`,
];
if (input.causeMessage) {
lines.push(`Cause: ${input.causeMessage}`);
}
lines.push(
"",
"This usually means the Paperclip server is not running, the configured URL is wrong, or the request is being blocked before it reaches Paperclip.",
"",
"Try:",
"- Start Paperclip with `pnpm dev` or `pnpm paperclipai run`.",
`- Verify the server is reachable with \`curl ${healthUrl}\`.`,
`- If Paperclip is running elsewhere, pass \`--api-base ${input.apiBase.replace(/\/+$/, "")}\` or set \`PAPERCLIP_API_URL\`.`,
);
return lines.join("\n");
}
function buildHealthCheckUrl(requestUrl: string): string {
const url = new URL(requestUrl);
url.pathname = `${url.pathname.replace(/\/+$/, "").replace(/\/api(?:\/.*)?$/, "")}/api/health`;
url.search = "";
url.hash = "";
return url.toString();
}
function formatConnectionCause(error: unknown): string | undefined {
if (!error) return undefined;
if (error instanceof Error) {
return error.message.trim() || error.name;
}
const message = String(error).trim();
return message || undefined;
}
function toStringRecord(headers: HeadersInit | undefined): Record<string, string> {
if (!headers) return {};
if (Array.isArray(headers)) {

View File

@@ -26,6 +26,9 @@ export async function addAllowedHostname(host: string, opts: { config?: string }
p.log.info(`Hostname ${pc.cyan(normalized)} is already allowed.`);
} else {
p.log.success(`Added allowed hostname: ${pc.cyan(normalized)}`);
p.log.message(
pc.dim("Restart the Paperclip server for this change to take effect."),
);
}
if (!(config.server.deploymentMode === "authenticated" && config.server.exposure === "private")) {

View File

@@ -3,6 +3,8 @@ import * as p from "@clack/prompts";
import pc from "picocolors";
import { and, eq, gt, isNull } from "drizzle-orm";
import { createDb, instanceUserRoles, invites } from "@paperclipai/db";
import { inferBindModeFromHost } from "@paperclipai/shared";
import { loadPaperclipEnvFile } from "../config/env.js";
import { readConfig, resolveConfigPath } from "../config/store.js";
function hashToken(token: string) {
@@ -13,7 +15,8 @@ function createInviteToken() {
return `pcp_bootstrap_${randomBytes(24).toString("hex")}`;
}
function resolveDbUrl(configPath?: string) {
function resolveDbUrl(configPath?: string, explicitDbUrl?: string) {
if (explicitDbUrl) return explicitDbUrl;
const config = readConfig(configPath);
if (process.env.DATABASE_URL) return process.env.DATABASE_URL;
if (config?.database.mode === "postgres" && config.database.connectionString) {
@@ -28,13 +31,23 @@ function resolveDbUrl(configPath?: string) {
function resolveBaseUrl(configPath?: string, explicitBaseUrl?: string) {
if (explicitBaseUrl) return explicitBaseUrl.replace(/\/+$/, "");
const fromEnv =
process.env.PAPERCLIP_PUBLIC_URL ??
process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL ??
process.env.BETTER_AUTH_URL ??
process.env.BETTER_AUTH_BASE_URL;
if (fromEnv?.trim()) return fromEnv.trim().replace(/\/+$/, "");
const config = readConfig(configPath);
if (config?.auth.baseUrlMode === "explicit" && config.auth.publicBaseUrl) {
return config.auth.publicBaseUrl.replace(/\/+$/, "");
}
const host = config?.server.host ?? "localhost";
const bind = config?.server.bind ?? inferBindModeFromHost(config?.server.host);
const host =
bind === "custom"
? config?.server.customBindHost ?? config?.server.host ?? "localhost"
: config?.server.host ?? "localhost";
const port = config?.server.port ?? 3100;
const publicHost = host === "0.0.0.0" ? "localhost" : host;
const publicHost = host === "0.0.0.0" || bind === "lan" ? "localhost" : host;
return `http://${publicHost}:${port}`;
}
@@ -43,8 +56,10 @@ export async function bootstrapCeoInvite(opts: {
force?: boolean;
expiresHours?: number;
baseUrl?: string;
dbUrl?: string;
}) {
const configPath = resolveConfigPath(opts.config);
loadPaperclipEnvFile(configPath);
const config = readConfig(configPath);
if (!config) {
p.log.error(`No config found at ${configPath}. Run ${pc.cyan("paperclip onboard")} first.`);
@@ -56,7 +71,7 @@ export async function bootstrapCeoInvite(opts: {
return;
}
const dbUrl = resolveDbUrl(configPath);
const dbUrl = resolveDbUrl(configPath, opts.dbUrl);
if (!dbUrl) {
p.log.error(
"Could not resolve database connection for bootstrap.",
@@ -65,6 +80,11 @@ export async function bootstrapCeoInvite(opts: {
}
const db = createDb(dbUrl);
const closableDb = db as typeof db & {
$client?: {
end?: (options?: { timeout?: number }) => Promise<void>;
};
};
try {
const existingAdminCount = await db
.select()
@@ -112,5 +132,7 @@ export async function bootstrapCeoInvite(opts: {
} catch (err) {
p.log.error(`Could not create bootstrap invite: ${err instanceof Error ? err.message : String(err)}`);
p.log.info("If using embedded-postgres, start the Paperclip server and run this command again.");
} finally {
await closableDb.$client?.end?.({ timeout: 5 }).catch(() => undefined);
}
}

View File

@@ -1,5 +1,13 @@
import { Command } from "commander";
import type { Agent } from "@paperclipai/shared";
import {
removeMaintainerOnlySkillSymlinks,
resolvePaperclipSkillsDir,
} from "@paperclipai/adapter-utils/server-utils";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
addCommonClientOptions,
formatInlineRecord,
@@ -13,6 +21,141 @@ interface AgentListOptions extends BaseClientOptions {
companyId?: string;
}
interface AgentLocalCliOptions extends BaseClientOptions {
companyId?: string;
keyName?: string;
installSkills?: boolean;
}
interface CreatedAgentKey {
id: string;
name: string;
token: string;
createdAt: string;
}
interface SkillsInstallSummary {
tool: "codex" | "claude";
target: string;
linked: string[];
removed: string[];
skipped: string[];
failed: Array<{ name: string; error: string }>;
}
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
function codexSkillsHome(): string {
const fromEnv = process.env.CODEX_HOME?.trim();
const base = fromEnv && fromEnv.length > 0 ? fromEnv : path.join(os.homedir(), ".codex");
return path.join(base, "skills");
}
function claudeSkillsHome(): string {
const fromEnv = process.env.CLAUDE_HOME?.trim();
const base = fromEnv && fromEnv.length > 0 ? fromEnv : path.join(os.homedir(), ".claude");
return path.join(base, "skills");
}
async function installSkillsForTarget(
sourceSkillsDir: string,
targetSkillsDir: string,
tool: "codex" | "claude",
): Promise<SkillsInstallSummary> {
const summary: SkillsInstallSummary = {
tool,
target: targetSkillsDir,
linked: [],
removed: [],
skipped: [],
failed: [],
};
await fs.mkdir(targetSkillsDir, { recursive: true });
const entries = await fs.readdir(sourceSkillsDir, { withFileTypes: true });
summary.removed = await removeMaintainerOnlySkillSymlinks(
targetSkillsDir,
entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name),
);
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const source = path.join(sourceSkillsDir, entry.name);
const target = path.join(targetSkillsDir, entry.name);
const existing = await fs.lstat(target).catch(() => null);
if (existing) {
if (existing.isSymbolicLink()) {
let linkedPath: string | null = null;
try {
linkedPath = await fs.readlink(target);
} catch (err) {
await fs.unlink(target);
try {
await fs.symlink(source, target);
summary.linked.push(entry.name);
continue;
} catch (linkErr) {
summary.failed.push({
name: entry.name,
error:
err instanceof Error && linkErr instanceof Error
? `${err.message}; then ${linkErr.message}`
: err instanceof Error
? err.message
: `Failed to recover broken symlink: ${String(err)}`,
});
continue;
}
}
const resolvedLinkedPath = path.isAbsolute(linkedPath)
? linkedPath
: path.resolve(path.dirname(target), linkedPath);
const linkedTargetExists = await fs
.stat(resolvedLinkedPath)
.then(() => true)
.catch(() => false);
if (!linkedTargetExists) {
await fs.unlink(target);
} else {
summary.skipped.push(entry.name);
continue;
}
} else {
summary.skipped.push(entry.name);
continue;
}
}
try {
await fs.symlink(source, target);
summary.linked.push(entry.name);
} catch (err) {
summary.failed.push({
name: entry.name,
error: err instanceof Error ? err.message : String(err),
});
}
}
return summary;
}
function buildAgentEnvExports(input: {
apiBase: string;
companyId: string;
agentId: string;
apiKey: string;
}): string {
const escaped = (value: string) => value.replace(/'/g, "'\"'\"'");
return [
`export PAPERCLIP_API_URL='${escaped(input.apiBase)}'`,
`export PAPERCLIP_COMPANY_ID='${escaped(input.companyId)}'`,
`export PAPERCLIP_AGENT_ID='${escaped(input.agentId)}'`,
`export PAPERCLIP_API_KEY='${escaped(input.apiKey)}'`,
].join("\n");
}
export function registerAgentCommands(program: Command): void {
const agent = program.command("agent").description("Agent operations");
@@ -71,4 +214,102 @@ export function registerAgentCommands(program: Command): void {
}
}),
);
addCommonClientOptions(
agent
.command("local-cli")
.description(
"Create an agent API key, install local Paperclip skills for Codex/Claude, and print shell exports",
)
.argument("<agentRef>", "Agent ID or shortname/url-key")
.requiredOption("-C, --company-id <id>", "Company ID")
.option("--key-name <name>", "API key label", "local-cli")
.option(
"--no-install-skills",
"Skip installing Paperclip skills into ~/.codex/skills and ~/.claude/skills",
)
.action(async (agentRef: string, opts: AgentLocalCliOptions) => {
try {
const ctx = resolveCommandContext(opts, { requireCompany: true });
const query = new URLSearchParams({ companyId: ctx.companyId ?? "" });
const agentRow = await ctx.api.get<Agent>(
`/api/agents/${encodeURIComponent(agentRef)}?${query.toString()}`,
);
if (!agentRow) {
throw new Error(`Agent not found: ${agentRef}`);
}
const now = new Date().toISOString().replaceAll(":", "-");
const keyName = opts.keyName?.trim() ? opts.keyName.trim() : `local-cli-${now}`;
const key = await ctx.api.post<CreatedAgentKey>(`/api/agents/${agentRow.id}/keys`, { name: keyName });
if (!key) {
throw new Error("Failed to create API key");
}
const installSummaries: SkillsInstallSummary[] = [];
if (opts.installSkills !== false) {
const skillsDir = await resolvePaperclipSkillsDir(__moduleDir, [path.resolve(process.cwd(), "skills")]);
if (!skillsDir) {
throw new Error(
"Could not locate local Paperclip skills directory. Expected ./skills in the repo checkout.",
);
}
installSummaries.push(
await installSkillsForTarget(skillsDir, codexSkillsHome(), "codex"),
await installSkillsForTarget(skillsDir, claudeSkillsHome(), "claude"),
);
}
const exportsText = buildAgentEnvExports({
apiBase: ctx.api.apiBase,
companyId: agentRow.companyId,
agentId: agentRow.id,
apiKey: key.token,
});
if (ctx.json) {
printOutput(
{
agent: {
id: agentRow.id,
name: agentRow.name,
urlKey: agentRow.urlKey,
companyId: agentRow.companyId,
},
key: {
id: key.id,
name: key.name,
createdAt: key.createdAt,
token: key.token,
},
skills: installSummaries,
exports: exportsText,
},
{ json: true },
);
return;
}
console.log(`Agent: ${agentRow.name} (${agentRow.id})`);
console.log(`API key created: ${key.name} (${key.id})`);
if (installSummaries.length > 0) {
for (const summary of installSummaries) {
console.log(
`${summary.tool}: linked=${summary.linked.length} removed=${summary.removed.length} skipped=${summary.skipped.length} failed=${summary.failed.length} target=${summary.target}`,
);
for (const failed of summary.failed) {
console.log(` failed ${failed.name}: ${failed.error}`);
}
}
}
console.log("");
console.log("# Run this in your shell before launching codex/claude:");
console.log(exportsText);
} catch (err) {
handleCommandError(err);
}
}),
{ includeCompany: false },
);
}

View File

@@ -0,0 +1,113 @@
import type { Command } from "commander";
import {
getStoredBoardCredential,
loginBoardCli,
removeStoredBoardCredential,
revokeStoredBoardCredential,
} from "../../client/board-auth.js";
import {
addCommonClientOptions,
handleCommandError,
printOutput,
resolveCommandContext,
type BaseClientOptions,
} from "./common.js";
interface AuthLoginOptions extends BaseClientOptions {
instanceAdmin?: boolean;
}
interface AuthLogoutOptions extends BaseClientOptions {}
interface AuthWhoamiOptions extends BaseClientOptions {}
export function registerClientAuthCommands(auth: Command): void {
addCommonClientOptions(
auth
.command("login")
.description("Authenticate the CLI for board-user access")
.option("--instance-admin", "Request instance-admin approval instead of plain board access", false)
.action(async (opts: AuthLoginOptions) => {
try {
const ctx = resolveCommandContext(opts);
const login = await loginBoardCli({
apiBase: ctx.api.apiBase,
requestedAccess: opts.instanceAdmin ? "instance_admin_required" : "board",
requestedCompanyId: ctx.companyId ?? null,
command: "paperclipai auth login",
});
printOutput(
{
ok: true,
apiBase: ctx.api.apiBase,
userId: login.userId ?? null,
approvalUrl: login.approvalUrl,
},
{ json: ctx.json },
);
} catch (err) {
handleCommandError(err);
}
}),
{ includeCompany: true },
);
addCommonClientOptions(
auth
.command("logout")
.description("Remove the stored board-user credential for this API base")
.action(async (opts: AuthLogoutOptions) => {
try {
const ctx = resolveCommandContext(opts);
const credential = getStoredBoardCredential(ctx.api.apiBase);
if (!credential) {
printOutput({ ok: true, apiBase: ctx.api.apiBase, revoked: false, removedLocalCredential: false }, { json: ctx.json });
return;
}
let revoked = false;
try {
await revokeStoredBoardCredential({
apiBase: ctx.api.apiBase,
token: credential.token,
});
revoked = true;
} catch {
// Remove the local credential even if the server-side revoke fails.
}
const removedLocalCredential = removeStoredBoardCredential(ctx.api.apiBase);
printOutput(
{
ok: true,
apiBase: ctx.api.apiBase,
revoked,
removedLocalCredential,
},
{ json: ctx.json },
);
} catch (err) {
handleCommandError(err);
}
}),
);
addCommonClientOptions(
auth
.command("whoami")
.description("Show the current board-user identity for this API base")
.action(async (opts: AuthWhoamiOptions) => {
try {
const ctx = resolveCommandContext(opts);
const me = await ctx.api.get<{
user: { id: string; name: string; email: string } | null;
userId: string;
isInstanceAdmin: boolean;
companyIds: string[];
source: string;
keyId: string | null;
}>("/api/cli-auth/me");
printOutput(me, { json: ctx.json });
} catch (err) {
handleCommandError(err);
}
}),
);
}

View File

@@ -1,5 +1,7 @@
import pc from "picocolors";
import type { Command } from "commander";
import { getStoredBoardCredential, loginBoardCli } from "../../client/board-auth.js";
import { buildCliCommandLabel } from "../../client/command-label.js";
import { readConfig } from "../../config/store.js";
import { readContext, resolveProfile, type ClientContextProfile } from "../../client/context.js";
import { ApiRequestError, PaperclipApiClient } from "../../client/http.js";
@@ -53,10 +55,12 @@ export function resolveCommandContext(
profile.apiBase ||
inferApiBaseFromConfig(options.config);
const apiKey =
const explicitApiKey =
options.apiKey?.trim() ||
process.env.PAPERCLIP_API_KEY?.trim() ||
readKeyFromProfileEnv(profile);
const storedBoardCredential = explicitApiKey ? null : getStoredBoardCredential(apiBase);
const apiKey = explicitApiKey || storedBoardCredential?.token;
const companyId =
options.companyId?.trim() ||
@@ -69,7 +73,27 @@ export function resolveCommandContext(
);
}
const api = new PaperclipApiClient({ apiBase, apiKey });
const api = new PaperclipApiClient({
apiBase,
apiKey,
recoverAuth: explicitApiKey || !canAttemptInteractiveBoardAuth()
? undefined
: async ({ error }) => {
const requestedAccess = error.message.includes("Instance admin required")
? "instance_admin_required"
: "board";
if (!shouldRecoverBoardAuth(error)) {
return null;
}
const login = await loginBoardCli({
apiBase,
requestedAccess,
requestedCompanyId: companyId ?? null,
command: buildCliCommandLabel(),
});
return login.token;
},
});
return {
api,
companyId,
@@ -79,6 +103,16 @@ export function resolveCommandContext(
};
}
function shouldRecoverBoardAuth(error: ApiRequestError): boolean {
if (error.status === 401) return true;
if (error.status !== 403) return false;
return error.message.includes("Board access required") || error.message.includes("Instance admin required");
}
function canAttemptInteractiveBoardAuth(): boolean {
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
}
export function printOutput(data: unknown, opts: { json?: boolean; label?: string } = {}): void {
if (opts.json) {
console.log(JSON.stringify(data, null, 2));

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,645 @@
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import pc from "picocolors";
import { Command } from "commander";
import type { Company, FeedbackTrace, FeedbackTraceBundle } from "@paperclipai/shared";
import {
addCommonClientOptions,
handleCommandError,
printOutput,
resolveCommandContext,
type BaseClientOptions,
type ResolvedClientContext,
} from "./common.js";
interface FeedbackFilterOptions extends BaseClientOptions {
targetType?: string;
vote?: string;
status?: string;
projectId?: string;
issueId?: string;
from?: string;
to?: string;
sharedOnly?: boolean;
}
export interface FeedbackTraceQueryOptions {
targetType?: string;
vote?: string;
status?: string;
projectId?: string;
issueId?: string;
from?: string;
to?: string;
sharedOnly?: boolean;
}
interface FeedbackReportOptions extends FeedbackFilterOptions {
payloads?: boolean;
}
interface FeedbackExportOptions extends FeedbackFilterOptions {
out?: string;
}
interface FeedbackSummary {
total: number;
thumbsUp: number;
thumbsDown: number;
withReason: number;
statuses: Record<string, number>;
}
interface FeedbackExportManifest {
exportedAt: string;
serverUrl: string;
companyId: string;
summary: FeedbackSummary & {
uniqueIssues: number;
issues: string[];
};
files: {
votes: string[];
traces: string[];
fullTraces: string[];
zip: string;
};
}
interface FeedbackExportResult {
outputDir: string;
zipPath: string;
manifest: FeedbackExportManifest;
}
export function registerFeedbackCommands(program: Command): void {
const feedback = program.command("feedback").description("Inspect and export local feedback traces");
addCommonClientOptions(
feedback
.command("report")
.description("Render a terminal report for company feedback traces")
.option("-C, --company-id <id>", "Company ID (overrides context default)")
.option("--target-type <type>", "Filter by target type")
.option("--vote <vote>", "Filter by vote value")
.option("--status <status>", "Filter by trace status")
.option("--project-id <id>", "Filter by project ID")
.option("--issue-id <id>", "Filter by issue ID")
.option("--from <iso8601>", "Only include traces created at or after this timestamp")
.option("--to <iso8601>", "Only include traces created at or before this timestamp")
.option("--shared-only", "Only include traces eligible for sharing/export")
.option("--payloads", "Include raw payload dumps in the terminal report", false)
.action(async (opts: FeedbackReportOptions) => {
try {
const ctx = resolveCommandContext(opts);
const companyId = await resolveFeedbackCompanyId(ctx, opts.companyId);
const traces = await fetchCompanyFeedbackTraces(ctx, companyId, opts);
const summary = summarizeFeedbackTraces(traces);
if (ctx.json) {
printOutput(
{
apiBase: ctx.api.apiBase,
companyId,
summary,
traces,
},
{ json: true },
);
return;
}
console.log(renderFeedbackReport({
apiBase: ctx.api.apiBase,
companyId,
traces,
summary,
includePayloads: Boolean(opts.payloads),
}));
} catch (err) {
handleCommandError(err);
}
}),
{ includeCompany: false },
);
addCommonClientOptions(
feedback
.command("export")
.description("Export feedback votes and raw trace bundles into a folder plus zip archive")
.option("-C, --company-id <id>", "Company ID (overrides context default)")
.option("--target-type <type>", "Filter by target type")
.option("--vote <vote>", "Filter by vote value")
.option("--status <status>", "Filter by trace status")
.option("--project-id <id>", "Filter by project ID")
.option("--issue-id <id>", "Filter by issue ID")
.option("--from <iso8601>", "Only include traces created at or after this timestamp")
.option("--to <iso8601>", "Only include traces created at or before this timestamp")
.option("--shared-only", "Only include traces eligible for sharing/export")
.option("--out <path>", "Output directory (default: ./feedback-export-<timestamp>)")
.action(async (opts: FeedbackExportOptions) => {
try {
const ctx = resolveCommandContext(opts);
const companyId = await resolveFeedbackCompanyId(ctx, opts.companyId);
const traces = await fetchCompanyFeedbackTraces(ctx, companyId, opts);
const outputDir = path.resolve(opts.out?.trim() || defaultFeedbackExportDirName());
const exported = await writeFeedbackExportBundle({
apiBase: ctx.api.apiBase,
companyId,
traces,
outputDir,
traceBundleFetcher: (trace) => fetchFeedbackTraceBundle(ctx, trace.id),
});
if (ctx.json) {
printOutput(
{
companyId,
outputDir: exported.outputDir,
zipPath: exported.zipPath,
summary: exported.manifest.summary,
},
{ json: true },
);
return;
}
console.log(renderFeedbackExportSummary(exported));
} catch (err) {
handleCommandError(err);
}
}),
{ includeCompany: false },
);
}
export async function resolveFeedbackCompanyId(
ctx: ResolvedClientContext,
explicitCompanyId?: string,
): Promise<string> {
const direct = explicitCompanyId?.trim() || ctx.companyId?.trim();
if (direct) return direct;
const companies = (await ctx.api.get<Company[]>("/api/companies")) ?? [];
const companyId = companies[0]?.id?.trim();
if (!companyId) {
throw new Error(
"Company ID is required. Pass --company-id, set PAPERCLIP_COMPANY_ID, or configure a CLI context default.",
);
}
return companyId;
}
export function buildFeedbackTraceQuery(opts: FeedbackTraceQueryOptions, includePayload = true): string {
const params = new URLSearchParams();
if (opts.targetType) params.set("targetType", opts.targetType);
if (opts.vote) params.set("vote", opts.vote);
if (opts.status) params.set("status", opts.status);
if (opts.projectId) params.set("projectId", opts.projectId);
if (opts.issueId) params.set("issueId", opts.issueId);
if (opts.from) params.set("from", opts.from);
if (opts.to) params.set("to", opts.to);
if (opts.sharedOnly) params.set("sharedOnly", "true");
if (includePayload) params.set("includePayload", "true");
const query = params.toString();
return query ? `?${query}` : "";
}
export function normalizeFeedbackTraceExportFormat(value: string | undefined): "json" | "ndjson" {
if (!value || value === "ndjson") return "ndjson";
if (value === "json") return "json";
throw new Error(`Unsupported export format: ${value}`);
}
export function serializeFeedbackTraces(traces: FeedbackTrace[], format: string | undefined): string {
if (normalizeFeedbackTraceExportFormat(format) === "json") {
return JSON.stringify(traces, null, 2);
}
return traces.map((trace) => JSON.stringify(trace)).join("\n");
}
export async function fetchCompanyFeedbackTraces(
ctx: ResolvedClientContext,
companyId: string,
opts: FeedbackFilterOptions,
): Promise<FeedbackTrace[]> {
return (
(await ctx.api.get<FeedbackTrace[]>(
`/api/companies/${companyId}/feedback-traces${buildFeedbackTraceQuery(opts, true)}`,
)) ?? []
);
}
export async function fetchFeedbackTraceBundle(
ctx: ResolvedClientContext,
traceId: string,
): Promise<FeedbackTraceBundle> {
const bundle = await ctx.api.get<FeedbackTraceBundle>(`/api/feedback-traces/${traceId}/bundle`);
if (!bundle) {
throw new Error(`Feedback trace bundle ${traceId} not found`);
}
return bundle;
}
export function summarizeFeedbackTraces(traces: FeedbackTrace[]): FeedbackSummary {
const statuses: Record<string, number> = {};
let thumbsUp = 0;
let thumbsDown = 0;
let withReason = 0;
for (const trace of traces) {
if (trace.vote === "up") thumbsUp += 1;
if (trace.vote === "down") thumbsDown += 1;
if (readFeedbackReason(trace)) withReason += 1;
statuses[trace.status] = (statuses[trace.status] ?? 0) + 1;
}
return {
total: traces.length,
thumbsUp,
thumbsDown,
withReason,
statuses,
};
}
export function renderFeedbackReport(input: {
apiBase: string;
companyId: string;
traces: FeedbackTrace[];
summary: FeedbackSummary;
includePayloads: boolean;
}): string {
const lines: string[] = [];
lines.push("");
lines.push(pc.bold(pc.magenta("Paperclip Feedback Report")));
lines.push(pc.dim(new Date().toISOString()));
lines.push(horizontalRule());
lines.push(`${pc.dim("Server:")} ${input.apiBase}`);
lines.push(`${pc.dim("Company:")} ${input.companyId}`);
lines.push("");
if (input.traces.length === 0) {
lines.push(pc.yellow("[!!] No feedback traces found."));
lines.push("");
return lines.join("\n");
}
lines.push(pc.bold(pc.cyan("Summary")));
lines.push(horizontalRule());
lines.push(` ${pc.green(pc.bold(String(input.summary.thumbsUp)))} thumbs up`);
lines.push(` ${pc.red(pc.bold(String(input.summary.thumbsDown)))} thumbs down`);
lines.push(` ${pc.yellow(pc.bold(String(input.summary.withReason)))} downvotes with a reason`);
lines.push(` ${pc.bold(String(input.summary.total))} total traces`);
lines.push("");
lines.push(pc.dim("Export status:"));
for (const status of ["pending", "sent", "local_only", "failed"]) {
lines.push(` ${padRight(status, 10)} ${input.summary.statuses[status] ?? 0}`);
}
lines.push("");
lines.push(pc.bold(pc.cyan("Trace Details")));
lines.push(horizontalRule());
for (const trace of input.traces) {
const voteColor = trace.vote === "up" ? pc.green : pc.red;
const voteIcon = trace.vote === "up" ? "^" : "v";
const issueRef = trace.issueIdentifier ?? trace.issueId;
const label = trace.targetSummary.label?.trim() || trace.targetType;
const excerpt = compactText(trace.targetSummary.excerpt);
const reason = readFeedbackReason(trace);
lines.push(
` ${voteColor(voteIcon)} ${pc.bold(issueRef)} ${pc.dim(compactText(trace.issueTitle, 64))}`,
);
lines.push(
` ${pc.dim("Trace:")} ${trace.id.slice(0, 8)} ${pc.dim("Status:")} ${trace.status} ${pc.dim("Date:")} ${formatTimestamp(trace.createdAt)}`,
);
lines.push(` ${pc.dim("Target:")} ${label}`);
if (excerpt) {
lines.push(` ${pc.dim("Excerpt:")} ${excerpt}`);
}
if (reason) {
lines.push(` ${pc.yellow(pc.bold("Reason:"))} ${pc.yellow(reason)}`);
}
lines.push("");
}
if (input.includePayloads) {
lines.push(pc.bold(pc.cyan("Raw Payloads")));
lines.push(horizontalRule());
for (const trace of input.traces) {
if (!trace.payloadSnapshot) continue;
const issueRef = trace.issueIdentifier ?? trace.issueId;
lines.push(` ${pc.bold(`${issueRef} (${trace.id.slice(0, 8)})`)}`);
const body = JSON.stringify(trace.payloadSnapshot, null, 2)?.split("\n") ?? [];
for (const line of body) {
lines.push(` ${pc.dim(line)}`);
}
lines.push("");
}
}
lines.push(horizontalRule());
lines.push(pc.dim(`Report complete. ${input.traces.length} trace(s) displayed.`));
lines.push("");
return lines.join("\n");
}
export async function writeFeedbackExportBundle(input: {
apiBase: string;
companyId: string;
traces: FeedbackTrace[];
outputDir: string;
traceBundleFetcher?: (trace: FeedbackTrace) => Promise<FeedbackTraceBundle>;
}): Promise<FeedbackExportResult> {
await ensureEmptyOutputDirectory(input.outputDir);
await mkdir(path.join(input.outputDir, "votes"), { recursive: true });
await mkdir(path.join(input.outputDir, "traces"), { recursive: true });
await mkdir(path.join(input.outputDir, "full-traces"), { recursive: true });
const summary = summarizeFeedbackTraces(input.traces);
const voteFiles: string[] = [];
const traceFiles: string[] = [];
const fullTraceDirs: string[] = [];
const fullTraceFiles: string[] = [];
const issueSet = new Set<string>();
for (const trace of input.traces) {
const issueRef = sanitizeFileSegment(trace.issueIdentifier ?? trace.issueId);
const voteRecord = buildFeedbackVoteRecord(trace);
const voteFileName = `${issueRef}-${trace.feedbackVoteId.slice(0, 8)}.json`;
const traceFileName = `${issueRef}-${trace.id.slice(0, 8)}.json`;
voteFiles.push(voteFileName);
traceFiles.push(traceFileName);
issueSet.add(trace.issueIdentifier ?? trace.issueId);
await writeFile(
path.join(input.outputDir, "votes", voteFileName),
`${JSON.stringify(voteRecord, null, 2)}\n`,
"utf8",
);
await writeFile(
path.join(input.outputDir, "traces", traceFileName),
`${JSON.stringify(trace, null, 2)}\n`,
"utf8",
);
if (input.traceBundleFetcher) {
const bundle = await input.traceBundleFetcher(trace);
const bundleDirName = `${issueRef}-${trace.id.slice(0, 8)}`;
const bundleDir = path.join(input.outputDir, "full-traces", bundleDirName);
await mkdir(bundleDir, { recursive: true });
fullTraceDirs.push(bundleDirName);
await writeFile(
path.join(bundleDir, "bundle.json"),
`${JSON.stringify(bundle, null, 2)}\n`,
"utf8",
);
fullTraceFiles.push(path.posix.join("full-traces", bundleDirName, "bundle.json"));
for (const file of bundle.files) {
const targetPath = path.join(bundleDir, file.path);
await mkdir(path.dirname(targetPath), { recursive: true });
await writeFile(targetPath, file.contents, "utf8");
fullTraceFiles.push(path.posix.join("full-traces", bundleDirName, file.path.replace(/\\/g, "/")));
}
}
}
const zipPath = `${input.outputDir}.zip`;
const manifest: FeedbackExportManifest = {
exportedAt: new Date().toISOString(),
serverUrl: input.apiBase,
companyId: input.companyId,
summary: {
...summary,
uniqueIssues: issueSet.size,
issues: Array.from(issueSet).sort((left, right) => left.localeCompare(right)),
},
files: {
votes: voteFiles.slice().sort((left, right) => left.localeCompare(right)),
traces: traceFiles.slice().sort((left, right) => left.localeCompare(right)),
fullTraces: fullTraceDirs.slice().sort((left, right) => left.localeCompare(right)),
zip: path.basename(zipPath),
},
};
await writeFile(
path.join(input.outputDir, "index.json"),
`${JSON.stringify(manifest, null, 2)}\n`,
"utf8",
);
const archiveFiles = await collectJsonFilesForArchive(input.outputDir, [
"index.json",
...manifest.files.votes.map((file) => path.posix.join("votes", file)),
...manifest.files.traces.map((file) => path.posix.join("traces", file)),
...fullTraceFiles,
]);
await writeFile(zipPath, createStoredZipArchive(archiveFiles, path.basename(input.outputDir)));
return {
outputDir: input.outputDir,
zipPath,
manifest,
};
}
export function renderFeedbackExportSummary(exported: FeedbackExportResult): string {
const lines: string[] = [];
lines.push("");
lines.push(pc.bold(pc.magenta("Paperclip Feedback Export")));
lines.push(pc.dim(exported.manifest.exportedAt));
lines.push(horizontalRule());
lines.push(`${pc.dim("Company:")} ${exported.manifest.companyId}`);
lines.push(`${pc.dim("Output:")} ${exported.outputDir}`);
lines.push(`${pc.dim("Archive:")} ${exported.zipPath}`);
lines.push("");
lines.push(pc.bold("Export Summary"));
lines.push(horizontalRule());
lines.push(` ${pc.green(pc.bold(String(exported.manifest.summary.thumbsUp)))} thumbs up`);
lines.push(` ${pc.red(pc.bold(String(exported.manifest.summary.thumbsDown)))} thumbs down`);
lines.push(` ${pc.yellow(pc.bold(String(exported.manifest.summary.withReason)))} with reason`);
lines.push(` ${pc.bold(String(exported.manifest.summary.uniqueIssues))} unique issues`);
lines.push("");
lines.push(pc.dim("Files:"));
lines.push(` ${path.join(exported.outputDir, "index.json")}`);
lines.push(` ${path.join(exported.outputDir, "votes")} (${exported.manifest.files.votes.length} files)`);
lines.push(` ${path.join(exported.outputDir, "traces")} (${exported.manifest.files.traces.length} files)`);
lines.push(` ${path.join(exported.outputDir, "full-traces")} (${exported.manifest.files.fullTraces.length} bundles)`);
lines.push(` ${exported.zipPath}`);
lines.push("");
return lines.join("\n");
}
function readFeedbackReason(trace: FeedbackTrace): string | null {
const payload = asRecord(trace.payloadSnapshot);
const vote = asRecord(payload?.vote);
const reason = vote?.reason;
return typeof reason === "string" && reason.trim() ? reason.trim() : null;
}
function buildFeedbackVoteRecord(trace: FeedbackTrace) {
return {
voteId: trace.feedbackVoteId,
traceId: trace.id,
issueId: trace.issueId,
issueIdentifier: trace.issueIdentifier,
issueTitle: trace.issueTitle,
vote: trace.vote,
targetType: trace.targetType,
targetId: trace.targetId,
targetSummary: trace.targetSummary,
status: trace.status,
consentVersion: trace.consentVersion,
createdAt: trace.createdAt,
updatedAt: trace.updatedAt,
reason: readFeedbackReason(trace),
};
}
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}
function compactText(value: string | null | undefined, maxLength = 88): string | null {
if (!value) return null;
const compact = value.replace(/\s+/g, " ").trim();
if (!compact) return null;
if (compact.length <= maxLength) return compact;
return `${compact.slice(0, maxLength - 3)}...`;
}
function formatTimestamp(value: unknown): string {
if (value instanceof Date) return value.toISOString().slice(0, 19).replace("T", " ");
if (typeof value === "string") return value.slice(0, 19).replace("T", " ");
return "-";
}
function horizontalRule(): string {
return pc.dim("-".repeat(72));
}
function padRight(value: string, width: number): string {
return `${value}${" ".repeat(Math.max(0, width - value.length))}`;
}
function defaultFeedbackExportDirName(): string {
const iso = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
return `feedback-export-${iso}`;
}
async function ensureEmptyOutputDirectory(outputDir: string): Promise<void> {
try {
const info = await stat(outputDir);
if (!info.isDirectory()) {
throw new Error(`Output path already exists and is not a directory: ${outputDir}`);
}
const entries = await readdir(outputDir);
if (entries.length > 0) {
throw new Error(`Output directory already exists and is not empty: ${outputDir}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : "";
if (/ENOENT/.test(message)) {
await mkdir(outputDir, { recursive: true });
return;
}
throw error;
}
}
async function collectJsonFilesForArchive(
outputDir: string,
relativePaths: string[],
): Promise<Record<string, string>> {
const files: Record<string, string> = {};
for (const relativePath of relativePaths) {
const normalized = relativePath.replace(/\\/g, "/");
files[normalized] = await readFile(path.join(outputDir, normalized), "utf8");
}
return files;
}
function sanitizeFileSegment(value: string): string {
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "feedback";
}
function writeUint16(target: Uint8Array, offset: number, value: number) {
target[offset] = value & 0xff;
target[offset + 1] = (value >>> 8) & 0xff;
}
function writeUint32(target: Uint8Array, offset: number, value: number) {
target[offset] = value & 0xff;
target[offset + 1] = (value >>> 8) & 0xff;
target[offset + 2] = (value >>> 16) & 0xff;
target[offset + 3] = (value >>> 24) & 0xff;
}
function crc32(bytes: Uint8Array) {
let crc = 0xffffffff;
for (const byte of bytes) {
crc ^= byte;
for (let bit = 0; bit < 8; bit += 1) {
crc = (crc & 1) === 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
}
}
return (crc ^ 0xffffffff) >>> 0;
}
function createStoredZipArchive(files: Record<string, string>, rootPath: string): Uint8Array {
const encoder = new TextEncoder();
const localChunks: Uint8Array[] = [];
const centralChunks: Uint8Array[] = [];
let localOffset = 0;
let entryCount = 0;
for (const [relativePath, content] of Object.entries(files).sort(([left], [right]) => left.localeCompare(right))) {
const fileName = encoder.encode(`${rootPath}/${relativePath}`);
const body = encoder.encode(content);
const checksum = crc32(body);
const localHeader = new Uint8Array(30 + fileName.length);
writeUint32(localHeader, 0, 0x04034b50);
writeUint16(localHeader, 4, 20);
writeUint16(localHeader, 6, 0x0800);
writeUint16(localHeader, 8, 0);
writeUint32(localHeader, 14, checksum);
writeUint32(localHeader, 18, body.length);
writeUint32(localHeader, 22, body.length);
writeUint16(localHeader, 26, fileName.length);
localHeader.set(fileName, 30);
const centralHeader = new Uint8Array(46 + fileName.length);
writeUint32(centralHeader, 0, 0x02014b50);
writeUint16(centralHeader, 4, 20);
writeUint16(centralHeader, 6, 20);
writeUint16(centralHeader, 8, 0x0800);
writeUint16(centralHeader, 10, 0);
writeUint32(centralHeader, 16, checksum);
writeUint32(centralHeader, 20, body.length);
writeUint32(centralHeader, 24, body.length);
writeUint16(centralHeader, 28, fileName.length);
writeUint32(centralHeader, 42, localOffset);
centralHeader.set(fileName, 46);
localChunks.push(localHeader, body);
centralChunks.push(centralHeader);
localOffset += localHeader.length + body.length;
entryCount += 1;
}
const centralDirectoryLength = centralChunks.reduce((sum, chunk) => sum + chunk.length, 0);
const archive = new Uint8Array(
localChunks.reduce((sum, chunk) => sum + chunk.length, 0) + centralDirectoryLength + 22,
);
let offset = 0;
for (const chunk of localChunks) {
archive.set(chunk, offset);
offset += chunk.length;
}
const centralDirectoryOffset = offset;
for (const chunk of centralChunks) {
archive.set(chunk, offset);
offset += chunk.length;
}
writeUint32(archive, offset, 0x06054b50);
writeUint16(archive, offset + 8, entryCount);
writeUint16(archive, offset + 10, entryCount);
writeUint32(archive, offset + 12, centralDirectoryLength);
writeUint32(archive, offset + 16, centralDirectoryOffset);
return archive;
}

View File

@@ -1,8 +1,10 @@
import { Command } from "commander";
import { writeFile } from "node:fs/promises";
import {
addIssueCommentSchema,
checkoutIssueSchema,
createIssueSchema,
type FeedbackTrace,
updateIssueSchema,
type Issue,
type IssueComment,
@@ -15,6 +17,11 @@ import {
resolveCommandContext,
type BaseClientOptions,
} from "./common.js";
import {
buildFeedbackTraceQuery,
normalizeFeedbackTraceExportFormat,
serializeFeedbackTraces,
} from "./feedback.js";
interface IssueBaseOptions extends BaseClientOptions {
status?: string;
@@ -61,6 +68,18 @@ interface IssueCheckoutOptions extends BaseClientOptions {
expectedStatuses?: string;
}
interface IssueFeedbackOptions extends BaseClientOptions {
targetType?: string;
vote?: string;
status?: string;
from?: string;
to?: string;
sharedOnly?: boolean;
includePayload?: boolean;
out?: string;
format?: string;
}
export function registerIssueCommands(program: Command): void {
const issue = program.command("issue").description("Issue operations");
@@ -237,6 +256,85 @@ export function registerIssueCommands(program: Command): void {
}),
);
addCommonClientOptions(
issue
.command("feedback:list")
.description("List feedback traces for an issue")
.argument("<issueId>", "Issue ID")
.option("--target-type <type>", "Filter by target type")
.option("--vote <vote>", "Filter by vote value")
.option("--status <status>", "Filter by trace status")
.option("--from <iso8601>", "Only include traces created at or after this timestamp")
.option("--to <iso8601>", "Only include traces created at or before this timestamp")
.option("--shared-only", "Only include traces eligible for sharing/export")
.option("--include-payload", "Include stored payload snapshots in the response")
.action(async (issueId: string, opts: IssueFeedbackOptions) => {
try {
const ctx = resolveCommandContext(opts);
const traces = (await ctx.api.get<FeedbackTrace[]>(
`/api/issues/${issueId}/feedback-traces${buildFeedbackTraceQuery(opts)}`,
)) ?? [];
if (ctx.json) {
printOutput(traces, { json: true });
return;
}
printOutput(
traces.map((trace) => ({
id: trace.id,
issue: trace.issueIdentifier ?? trace.issueId,
vote: trace.vote,
status: trace.status,
targetType: trace.targetType,
target: trace.targetSummary.label,
})),
{ json: false },
);
} catch (err) {
handleCommandError(err);
}
}),
);
addCommonClientOptions(
issue
.command("feedback:export")
.description("Export feedback traces for an issue")
.argument("<issueId>", "Issue ID")
.option("--target-type <type>", "Filter by target type")
.option("--vote <vote>", "Filter by vote value")
.option("--status <status>", "Filter by trace status")
.option("--from <iso8601>", "Only include traces created at or after this timestamp")
.option("--to <iso8601>", "Only include traces created at or before this timestamp")
.option("--shared-only", "Only include traces eligible for sharing/export")
.option("--include-payload", "Include stored payload snapshots in the export")
.option("--out <path>", "Write export to a file path instead of stdout")
.option("--format <format>", "Export format: json or ndjson", "ndjson")
.action(async (issueId: string, opts: IssueFeedbackOptions) => {
try {
const ctx = resolveCommandContext(opts);
const traces = (await ctx.api.get<FeedbackTrace[]>(
`/api/issues/${issueId}/feedback-traces${buildFeedbackTraceQuery(opts, opts.includePayload ?? true)}`,
)) ?? [];
const serialized = serializeFeedbackTraces(traces, opts.format);
if (opts.out?.trim()) {
await writeFile(opts.out, serialized, "utf8");
if (ctx.json) {
printOutput(
{ out: opts.out, count: traces.length, format: normalizeFeedbackTraceExportFormat(opts.format) },
{ json: true },
);
return;
}
console.log(`Wrote ${traces.length} feedback trace(s) to ${opts.out}`);
return;
}
process.stdout.write(`${serialized}${serialized.endsWith("\n") ? "" : "\n"}`);
} catch (err) {
handleCommandError(err);
}
}),
);
addCommonClientOptions(
issue
.command("checkout")

View File

@@ -0,0 +1,374 @@
import path from "node:path";
import { Command } from "commander";
import pc from "picocolors";
import {
addCommonClientOptions,
handleCommandError,
printOutput,
resolveCommandContext,
type BaseClientOptions,
} from "./common.js";
// ---------------------------------------------------------------------------
// Types mirroring server-side shapes
// ---------------------------------------------------------------------------
interface PluginRecord {
id: string;
pluginKey: string;
packageName: string;
version: string;
status: string;
displayName?: string;
lastError?: string | null;
installedAt: string;
updatedAt: string;
}
// ---------------------------------------------------------------------------
// Option types
// ---------------------------------------------------------------------------
interface PluginListOptions extends BaseClientOptions {
status?: string;
}
interface PluginInstallOptions extends BaseClientOptions {
local?: boolean;
version?: string;
}
interface PluginUninstallOptions extends BaseClientOptions {
force?: boolean;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Resolve a local path argument to an absolute path so the server can find the
* plugin on disk regardless of where the user ran the CLI.
*/
function resolvePackageArg(packageArg: string, isLocal: boolean): string {
if (!isLocal) return packageArg;
// Already absolute
if (path.isAbsolute(packageArg)) return packageArg;
// Expand leading ~ to home directory
if (packageArg.startsWith("~")) {
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
return path.resolve(home, packageArg.slice(1).replace(/^[\\/]/, ""));
}
return path.resolve(process.cwd(), packageArg);
}
function formatPlugin(p: PluginRecord): string {
const statusColor =
p.status === "ready"
? pc.green(p.status)
: p.status === "error"
? pc.red(p.status)
: p.status === "disabled"
? pc.dim(p.status)
: pc.yellow(p.status);
const parts = [
`key=${pc.bold(p.pluginKey)}`,
`status=${statusColor}`,
`version=${p.version}`,
`id=${pc.dim(p.id)}`,
];
if (p.lastError) {
parts.push(`error=${pc.red(p.lastError.slice(0, 80))}`);
}
return parts.join(" ");
}
// ---------------------------------------------------------------------------
// Command registration
// ---------------------------------------------------------------------------
export function registerPluginCommands(program: Command): void {
const plugin = program.command("plugin").description("Plugin lifecycle management");
// -------------------------------------------------------------------------
// plugin list
// -------------------------------------------------------------------------
addCommonClientOptions(
plugin
.command("list")
.description("List installed plugins")
.option("--status <status>", "Filter by status (ready, error, disabled, installed, upgrade_pending)")
.action(async (opts: PluginListOptions) => {
try {
const ctx = resolveCommandContext(opts);
const qs = opts.status ? `?status=${encodeURIComponent(opts.status)}` : "";
const plugins = await ctx.api.get<PluginRecord[]>(`/api/plugins${qs}`);
if (ctx.json) {
printOutput(plugins, { json: true });
return;
}
const rows = plugins ?? [];
if (rows.length === 0) {
console.log(pc.dim("No plugins installed."));
return;
}
for (const p of rows) {
console.log(formatPlugin(p));
}
} catch (err) {
handleCommandError(err);
}
}),
);
// -------------------------------------------------------------------------
// plugin install <package-or-path>
// -------------------------------------------------------------------------
addCommonClientOptions(
plugin
.command("install <package>")
.description(
"Install a plugin from a local path or npm package.\n" +
" Examples:\n" +
" paperclipai plugin install ./my-plugin # local path\n" +
" paperclipai plugin install @acme/plugin-linear # npm package\n" +
" paperclipai plugin install @acme/plugin-linear@1.2 # pinned version",
)
.option("-l, --local", "Treat <package> as a local filesystem path", false)
.option("--version <version>", "Specific npm version to install (npm packages only)")
.action(async (packageArg: string, opts: PluginInstallOptions) => {
try {
const ctx = resolveCommandContext(opts);
// Auto-detect local paths: starts with . or / or ~ or is an absolute path
const isLocal =
opts.local ||
packageArg.startsWith("./") ||
packageArg.startsWith("../") ||
packageArg.startsWith("/") ||
packageArg.startsWith("~");
const resolvedPackage = resolvePackageArg(packageArg, isLocal);
if (!ctx.json) {
console.log(
pc.dim(
isLocal
? `Installing plugin from local path: ${resolvedPackage}`
: `Installing plugin: ${resolvedPackage}${opts.version ? `@${opts.version}` : ""}`,
),
);
}
const installedPlugin = await ctx.api.post<PluginRecord>("/api/plugins/install", {
packageName: resolvedPackage,
version: opts.version,
isLocalPath: isLocal,
});
if (ctx.json) {
printOutput(installedPlugin, { json: true });
return;
}
if (!installedPlugin) {
console.log(pc.dim("Install returned no plugin record."));
return;
}
console.log(
pc.green(
`✓ Installed ${pc.bold(installedPlugin.pluginKey)} v${installedPlugin.version} (${installedPlugin.status})`,
),
);
if (installedPlugin.lastError) {
console.log(pc.red(` Warning: ${installedPlugin.lastError}`));
}
} catch (err) {
handleCommandError(err);
}
}),
);
// -------------------------------------------------------------------------
// plugin uninstall <plugin-key-or-id>
// -------------------------------------------------------------------------
addCommonClientOptions(
plugin
.command("uninstall <pluginKey>")
.description(
"Uninstall a plugin by its plugin key or database ID.\n" +
" Use --force to hard-purge all state and config.",
)
.option("--force", "Purge all plugin state and config (hard delete)", false)
.action(async (pluginKey: string, opts: PluginUninstallOptions) => {
try {
const ctx = resolveCommandContext(opts);
const purge = opts.force === true;
const qs = purge ? "?purge=true" : "";
if (!ctx.json) {
console.log(
pc.dim(
purge
? `Uninstalling and purging plugin: ${pluginKey}`
: `Uninstalling plugin: ${pluginKey}`,
),
);
}
const result = await ctx.api.delete<PluginRecord | null>(
`/api/plugins/${encodeURIComponent(pluginKey)}${qs}`,
);
if (ctx.json) {
printOutput(result, { json: true });
return;
}
console.log(pc.green(`✓ Uninstalled ${pc.bold(pluginKey)}${purge ? " (purged)" : ""}`));
} catch (err) {
handleCommandError(err);
}
}),
);
// -------------------------------------------------------------------------
// plugin enable <plugin-key-or-id>
// -------------------------------------------------------------------------
addCommonClientOptions(
plugin
.command("enable <pluginKey>")
.description("Enable a disabled or errored plugin")
.action(async (pluginKey: string, opts: BaseClientOptions) => {
try {
const ctx = resolveCommandContext(opts);
const result = await ctx.api.post<PluginRecord>(
`/api/plugins/${encodeURIComponent(pluginKey)}/enable`,
);
if (ctx.json) {
printOutput(result, { json: true });
return;
}
console.log(pc.green(`✓ Enabled ${pc.bold(pluginKey)} — status: ${result?.status ?? "unknown"}`));
} catch (err) {
handleCommandError(err);
}
}),
);
// -------------------------------------------------------------------------
// plugin disable <plugin-key-or-id>
// -------------------------------------------------------------------------
addCommonClientOptions(
plugin
.command("disable <pluginKey>")
.description("Disable a running plugin without uninstalling it")
.action(async (pluginKey: string, opts: BaseClientOptions) => {
try {
const ctx = resolveCommandContext(opts);
const result = await ctx.api.post<PluginRecord>(
`/api/plugins/${encodeURIComponent(pluginKey)}/disable`,
);
if (ctx.json) {
printOutput(result, { json: true });
return;
}
console.log(pc.dim(`Disabled ${pc.bold(pluginKey)} — status: ${result?.status ?? "unknown"}`));
} catch (err) {
handleCommandError(err);
}
}),
);
// -------------------------------------------------------------------------
// plugin inspect <plugin-key-or-id>
// -------------------------------------------------------------------------
addCommonClientOptions(
plugin
.command("inspect <pluginKey>")
.description("Show full details for an installed plugin")
.action(async (pluginKey: string, opts: BaseClientOptions) => {
try {
const ctx = resolveCommandContext(opts);
const result = await ctx.api.get<PluginRecord>(
`/api/plugins/${encodeURIComponent(pluginKey)}`,
);
if (ctx.json) {
printOutput(result, { json: true });
return;
}
if (!result) {
console.log(pc.red(`Plugin not found: ${pluginKey}`));
process.exit(1);
}
console.log(formatPlugin(result));
if (result.lastError) {
console.log(`\n${pc.red("Last error:")}\n${result.lastError}`);
}
} catch (err) {
handleCommandError(err);
}
}),
);
// -------------------------------------------------------------------------
// plugin examples
// -------------------------------------------------------------------------
addCommonClientOptions(
plugin
.command("examples")
.description("List bundled example plugins available for local install")
.action(async (opts: BaseClientOptions) => {
try {
const ctx = resolveCommandContext(opts);
const examples = await ctx.api.get<
Array<{
packageName: string;
pluginKey: string;
displayName: string;
description: string;
localPath: string;
tag: string;
}>
>("/api/plugins/examples");
if (ctx.json) {
printOutput(examples, { json: true });
return;
}
const rows = examples ?? [];
if (rows.length === 0) {
console.log(pc.dim("No bundled examples available."));
return;
}
for (const ex of rows) {
console.log(
`${pc.bold(ex.displayName)} ${pc.dim(ex.pluginKey)}\n` +
` ${ex.description}\n` +
` ${pc.cyan(`paperclipai plugin install ${ex.localPath}`)}`,
);
}
} catch (err) {
handleCommandError(err);
}
}),
);
}

View File

@@ -0,0 +1,129 @@
import { inflateRawSync } from "node:zlib";
import path from "node:path";
import type { CompanyPortabilityFileEntry } from "@paperclipai/shared";
const textDecoder = new TextDecoder();
export const binaryContentTypeByExtension: Record<string, string> = {
".gif": "image/gif",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".png": "image/png",
".svg": "image/svg+xml",
".webp": "image/webp",
};
function normalizeArchivePath(pathValue: string) {
return pathValue
.replace(/\\/g, "/")
.split("/")
.filter(Boolean)
.join("/");
}
function readUint16(source: Uint8Array, offset: number) {
return source[offset]! | (source[offset + 1]! << 8);
}
function readUint32(source: Uint8Array, offset: number) {
return (
source[offset]! |
(source[offset + 1]! << 8) |
(source[offset + 2]! << 16) |
(source[offset + 3]! << 24)
) >>> 0;
}
function sharedArchiveRoot(paths: string[]) {
if (paths.length === 0) return null;
const firstSegments = paths
.map((entry) => normalizeArchivePath(entry).split("/").filter(Boolean))
.filter((parts) => parts.length > 0);
if (firstSegments.length === 0) return null;
const candidate = firstSegments[0]![0]!;
return firstSegments.every((parts) => parts.length > 1 && parts[0] === candidate)
? candidate
: null;
}
function bytesToPortableFileEntry(pathValue: string, bytes: Uint8Array): CompanyPortabilityFileEntry {
const contentType = binaryContentTypeByExtension[path.extname(pathValue).toLowerCase()];
if (!contentType) return textDecoder.decode(bytes);
return {
encoding: "base64",
data: Buffer.from(bytes).toString("base64"),
contentType,
};
}
async function inflateZipEntry(compressionMethod: number, bytes: Uint8Array) {
if (compressionMethod === 0) return bytes;
if (compressionMethod !== 8) {
throw new Error("Unsupported zip archive: only STORE and DEFLATE entries are supported.");
}
return new Uint8Array(inflateRawSync(bytes));
}
export async function readZipArchive(source: ArrayBuffer | Uint8Array): Promise<{
rootPath: string | null;
files: Record<string, CompanyPortabilityFileEntry>;
}> {
const bytes = source instanceof Uint8Array ? source : new Uint8Array(source);
const entries: Array<{ path: string; body: CompanyPortabilityFileEntry }> = [];
let offset = 0;
while (offset + 4 <= bytes.length) {
const signature = readUint32(bytes, offset);
if (signature === 0x02014b50 || signature === 0x06054b50) break;
if (signature !== 0x04034b50) {
throw new Error("Invalid zip archive: unsupported local file header.");
}
if (offset + 30 > bytes.length) {
throw new Error("Invalid zip archive: truncated local file header.");
}
const generalPurposeFlag = readUint16(bytes, offset + 6);
const compressionMethod = readUint16(bytes, offset + 8);
const compressedSize = readUint32(bytes, offset + 18);
const fileNameLength = readUint16(bytes, offset + 26);
const extraFieldLength = readUint16(bytes, offset + 28);
if ((generalPurposeFlag & 0x0008) !== 0) {
throw new Error("Unsupported zip archive: data descriptors are not supported.");
}
const nameOffset = offset + 30;
const bodyOffset = nameOffset + fileNameLength + extraFieldLength;
const bodyEnd = bodyOffset + compressedSize;
if (bodyEnd > bytes.length) {
throw new Error("Invalid zip archive: truncated file contents.");
}
const rawArchivePath = textDecoder.decode(bytes.slice(nameOffset, nameOffset + fileNameLength));
const archivePath = normalizeArchivePath(rawArchivePath);
const isDirectoryEntry = /\/$/.test(rawArchivePath.replace(/\\/g, "/"));
if (archivePath && !isDirectoryEntry) {
const entryBytes = await inflateZipEntry(compressionMethod, bytes.slice(bodyOffset, bodyEnd));
entries.push({
path: archivePath,
body: bytesToPortableFileEntry(archivePath, entryBytes),
});
}
offset = bodyEnd;
}
const rootPath = sharedArchiveRoot(entries.map((entry) => entry.path));
const files: Record<string, CompanyPortabilityFileEntry> = {};
for (const entry of entries) {
const normalizedPath =
rootPath && entry.path.startsWith(`${rootPath}/`)
? entry.path.slice(rootPath.length + 1)
: entry.path;
if (!normalizedPath) continue;
files[normalizedPath] = entry.body;
}
return { rootPath, files };
}

View File

@@ -10,6 +10,7 @@ import { defaultSecretsConfig, promptSecrets } from "../prompts/secrets.js";
import { defaultStorageConfig, promptStorage } from "../prompts/storage.js";
import { promptServer } from "../prompts/server.js";
import {
resolveDefaultBackupDir,
resolveDefaultEmbeddedPostgresDir,
resolveDefaultLogsDir,
resolvePaperclipInstanceId,
@@ -39,6 +40,12 @@ function defaultConfig(): PaperclipConfig {
mode: "embedded-postgres",
embeddedPostgresDataDir: resolveDefaultEmbeddedPostgresDir(instanceId),
embeddedPostgresPort: 54329,
backup: {
enabled: true,
intervalMinutes: 60,
retentionDays: 30,
dir: resolveDefaultBackupDir(instanceId),
},
},
logging: {
mode: "file",
@@ -47,6 +54,7 @@ function defaultConfig(): PaperclipConfig {
server: {
deploymentMode: "local_trusted",
exposure: "private",
bind: "loopback",
host: "127.0.0.1",
port: 3100,
allowedHostnames: [],
@@ -54,6 +62,10 @@ function defaultConfig(): PaperclipConfig {
},
auth: {
baseUrlMode: "auto",
disableSignUp: false,
},
telemetry: {
enabled: true,
},
storage: defaultStorageConfig(),
secrets: defaultSecretsConfig(),
@@ -118,7 +130,7 @@ export async function configure(opts: {
switch (section) {
case "database":
config.database = await promptDatabase();
config.database = await promptDatabase(config.database);
break;
case "llm": {
const llm = await promptLlm();

View File

@@ -0,0 +1,102 @@
import path from "node:path";
import * as p from "@clack/prompts";
import pc from "picocolors";
import { formatDatabaseBackupResult, runDatabaseBackup } from "@paperclipai/db";
import {
expandHomePrefix,
resolveDefaultBackupDir,
resolvePaperclipInstanceId,
} from "../config/home.js";
import { readConfig, resolveConfigPath } from "../config/store.js";
import { printPaperclipCliBanner } from "../utils/banner.js";
type DbBackupOptions = {
config?: string;
dir?: string;
retentionDays?: number;
filenamePrefix?: string;
json?: boolean;
};
function resolveConnectionString(configPath?: string): { value: string; source: string } {
const envUrl = process.env.DATABASE_URL?.trim();
if (envUrl) return { value: envUrl, source: "DATABASE_URL" };
const config = readConfig(configPath);
if (config?.database.mode === "postgres" && config.database.connectionString?.trim()) {
return { value: config.database.connectionString.trim(), source: "config.database.connectionString" };
}
const port = config?.database.embeddedPostgresPort ?? 54329;
return {
value: `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`,
source: `embedded-postgres@${port}`,
};
}
function normalizeRetentionDays(value: number | undefined, fallback: number): number {
const candidate = value ?? fallback;
if (!Number.isInteger(candidate) || candidate < 1) {
throw new Error(`Invalid retention days '${String(candidate)}'. Use a positive integer.`);
}
return candidate;
}
function resolveBackupDir(raw: string): string {
return path.resolve(expandHomePrefix(raw.trim()));
}
export async function dbBackupCommand(opts: DbBackupOptions): Promise<void> {
printPaperclipCliBanner();
p.intro(pc.bgCyan(pc.black(" paperclip db:backup ")));
const configPath = resolveConfigPath(opts.config);
const config = readConfig(opts.config);
const connection = resolveConnectionString(opts.config);
const defaultDir = resolveDefaultBackupDir(resolvePaperclipInstanceId());
const configuredDir = opts.dir?.trim() || config?.database.backup.dir || defaultDir;
const backupDir = resolveBackupDir(configuredDir);
const retentionDays = normalizeRetentionDays(
opts.retentionDays,
config?.database.backup.retentionDays ?? 30,
);
const filenamePrefix = opts.filenamePrefix?.trim() || "paperclip";
p.log.message(pc.dim(`Config: ${configPath}`));
p.log.message(pc.dim(`Connection source: ${connection.source}`));
p.log.message(pc.dim(`Backup dir: ${backupDir}`));
p.log.message(pc.dim(`Retention: ${retentionDays} day(s)`));
const spinner = p.spinner();
spinner.start("Creating database backup...");
try {
const result = await runDatabaseBackup({
connectionString: connection.value,
backupDir,
retention: { dailyDays: retentionDays, weeklyWeeks: 4, monthlyMonths: 1 },
filenamePrefix,
});
spinner.stop(`Backup saved: ${formatDatabaseBackupResult(result)}`);
if (opts.json) {
console.log(
JSON.stringify(
{
backupFile: result.backupFile,
sizeBytes: result.sizeBytes,
prunedCount: result.prunedCount,
backupDir,
retentionDays,
connectionSource: connection.source,
},
null,
2,
),
);
}
p.outro(pc.green("Backup completed."));
} catch (err) {
spinner.stop(pc.red("Backup failed."));
throw err;
}
}

View File

@@ -14,6 +14,7 @@ import {
storageCheck,
type CheckResult,
} from "../checks/index.js";
import { loadPaperclipEnvFile } from "../config/env.js";
import { printPaperclipCliBanner } from "../utils/banner.js";
const STATUS_ICON = {
@@ -31,6 +32,7 @@ export async function doctor(opts: {
p.intro(pc.bgCyan(pc.black(" paperclip doctor ")));
const configPath = resolveConfigPath(opts.config);
loadPaperclipEnvFile(configPath);
const results: CheckResult[] = [];
// 1. Config check (must pass before others)
@@ -64,28 +66,40 @@ export async function doctor(opts: {
printResult(deploymentAuthResult);
// 3. Agent JWT check
const jwtResult = agentJwtSecretCheck(opts.config);
results.push(jwtResult);
printResult(jwtResult);
await maybeRepair(jwtResult, opts);
results.push(
await runRepairableCheck({
run: () => agentJwtSecretCheck(opts.config),
configPath,
opts,
}),
);
// 4. Secrets adapter check
const secretsResult = secretsCheck(config, configPath);
results.push(secretsResult);
printResult(secretsResult);
await maybeRepair(secretsResult, opts);
results.push(
await runRepairableCheck({
run: () => secretsCheck(config, configPath),
configPath,
opts,
}),
);
// 5. Storage check
const storageResult = storageCheck(config, configPath);
results.push(storageResult);
printResult(storageResult);
await maybeRepair(storageResult, opts);
results.push(
await runRepairableCheck({
run: () => storageCheck(config, configPath),
configPath,
opts,
}),
);
// 6. Database check
const dbResult = await databaseCheck(config, configPath);
results.push(dbResult);
printResult(dbResult);
await maybeRepair(dbResult, opts);
results.push(
await runRepairableCheck({
run: () => databaseCheck(config, configPath),
configPath,
opts,
}),
);
// 7. LLM check
const llmResult = await llmCheck(config);
@@ -93,10 +107,13 @@ export async function doctor(opts: {
printResult(llmResult);
// 8. Log directory check
const logResult = logCheck(config, configPath);
results.push(logResult);
printResult(logResult);
await maybeRepair(logResult, opts);
results.push(
await runRepairableCheck({
run: () => logCheck(config, configPath),
configPath,
opts,
}),
);
// 9. Port check
const portResult = await portCheck(config);
@@ -118,9 +135,9 @@ function printResult(result: CheckResult): void {
async function maybeRepair(
result: CheckResult,
opts: { repair?: boolean; yes?: boolean },
): Promise<void> {
if (result.status === "pass" || !result.canRepair || !result.repair) return;
if (!opts.repair) return;
): Promise<boolean> {
if (result.status === "pass" || !result.canRepair || !result.repair) return false;
if (!opts.repair) return false;
let shouldRepair = opts.yes;
if (!shouldRepair) {
@@ -128,7 +145,7 @@ async function maybeRepair(
message: `Repair "${result.name}"?`,
initialValue: true,
});
if (p.isCancel(answer)) return;
if (p.isCancel(answer)) return false;
shouldRepair = answer;
}
@@ -136,10 +153,30 @@ async function maybeRepair(
try {
await result.repair();
p.log.success(`Repaired: ${result.name}`);
return true;
} catch (err) {
p.log.error(`Repair failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
return false;
}
async function runRepairableCheck(input: {
run: () => CheckResult | Promise<CheckResult>;
configPath: string;
opts: { repair?: boolean; yes?: boolean };
}): Promise<CheckResult> {
let result = await input.run();
printResult(result);
const repaired = await maybeRepair(result, input.opts);
if (!repaired) return result;
// Repairs may create/update the adjacent .env file or other local resources.
loadPaperclipEnvFile(input.configPath);
result = await input.run();
printResult(result);
return result;
}
function printSummary(results: CheckResult[]): { passed: number; warned: number; failed: number } {

View File

@@ -118,6 +118,29 @@ function collectDeploymentEnvRows(config: PaperclipConfig | null, configPath: st
const dbUrl = process.env.DATABASE_URL ?? config?.database?.connectionString ?? "";
const databaseMode = config?.database?.mode ?? "embedded-postgres";
const dbUrlSource: EnvSource = process.env.DATABASE_URL ? "env" : config?.database?.connectionString ? "config" : "missing";
const publicUrl =
process.env.PAPERCLIP_PUBLIC_URL ??
process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL ??
process.env.BETTER_AUTH_URL ??
process.env.BETTER_AUTH_BASE_URL ??
config?.auth?.publicBaseUrl ??
"";
const publicUrlSource: EnvSource =
process.env.PAPERCLIP_PUBLIC_URL
? "env"
: process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL || process.env.BETTER_AUTH_URL || process.env.BETTER_AUTH_BASE_URL
? "env"
: config?.auth?.publicBaseUrl
? "config"
: "missing";
let trustedOriginsDefault = "";
if (publicUrl) {
try {
trustedOriginsDefault = new URL(publicUrl).origin;
} catch {
trustedOriginsDefault = "";
}
}
const heartbeatInterval = process.env.HEARTBEAT_SCHEDULER_INTERVAL_MS ?? DEFAULT_HEARTBEAT_SCHEDULER_INTERVAL_MS;
const heartbeatEnabled = process.env.HEARTBEAT_SCHEDULER_ENABLED ?? "true";
@@ -192,6 +215,24 @@ function collectDeploymentEnvRows(config: PaperclipConfig | null, configPath: st
required: false,
note: "HTTP listen port",
},
{
key: "PAPERCLIP_PUBLIC_URL",
value: publicUrl,
source: publicUrlSource,
required: false,
note: "Canonical public URL for auth/callback/invite origin wiring",
},
{
key: "BETTER_AUTH_TRUSTED_ORIGINS",
value: process.env.BETTER_AUTH_TRUSTED_ORIGINS ?? trustedOriginsDefault,
source: process.env.BETTER_AUTH_TRUSTED_ORIGINS
? "env"
: trustedOriginsDefault
? "default"
: "missing",
required: false,
note: "Comma-separated auth origin allowlist (auto-derived from PAPERCLIP_PUBLIC_URL when possible)",
},
{
key: "PAPERCLIP_AGENT_JWT_TTL_SECONDS",
value: process.env.PAPERCLIP_AGENT_JWT_TTL_SECONDS ?? DEFAULT_AGENT_JWT_TTL_SECONDS,

View File

@@ -1,5 +1,22 @@
import * as p from "@clack/prompts";
import path from "node:path";
import pc from "picocolors";
import {
AUTH_BASE_URL_MODES,
BIND_MODES,
DEPLOYMENT_EXPOSURES,
DEPLOYMENT_MODES,
SECRET_PROVIDERS,
STORAGE_PROVIDERS,
inferBindModeFromHost,
resolveRuntimeBind,
type BindMode,
type AuthBaseUrlMode,
type DeploymentExposure,
type DeploymentMode,
type SecretProvider,
type StorageProvider,
} from "@paperclipai/shared";
import { configExists, readConfig, resolveConfigPath, writeConfig } from "../config/store.js";
import type { PaperclipConfig } from "../config/schema.js";
import { ensureAgentJwtSecret, resolveAgentJwtEnvFile } from "../config/env.js";
@@ -10,14 +27,22 @@ import { promptLogging } from "../prompts/logging.js";
import { defaultSecretsConfig } from "../prompts/secrets.js";
import { defaultStorageConfig, promptStorage } from "../prompts/storage.js";
import { promptServer } from "../prompts/server.js";
import { buildPresetServerConfig } from "../config/server-bind.js";
import {
describeLocalInstancePaths,
expandHomePrefix,
resolveDefaultBackupDir,
resolveDefaultEmbeddedPostgresDir,
resolveDefaultLogsDir,
resolvePaperclipInstanceId,
} from "../config/home.js";
import { bootstrapCeoInvite } from "./auth-bootstrap-ceo.js";
import { printPaperclipCliBanner } from "../utils/banner.js";
import {
getTelemetryClient,
trackInstallStarted,
trackInstallCompleted,
} from "../telemetry.js";
type SetupMode = "quickstart" | "advanced";
@@ -26,37 +51,282 @@ type OnboardOptions = {
run?: boolean;
yes?: boolean;
invokedByRun?: boolean;
bind?: BindMode;
};
function quickstartDefaults(): Pick<PaperclipConfig, "database" | "logging" | "server" | "auth" | "storage" | "secrets"> {
type OnboardDefaults = Pick<PaperclipConfig, "database" | "logging" | "server" | "auth" | "storage" | "secrets">;
const TAILNET_BIND_WARNING =
"No Tailscale address was detected during setup. The saved config will stay on loopback until Tailscale is available or PAPERCLIP_TAILNET_BIND_HOST is set.";
const ONBOARD_ENV_KEYS = [
"PAPERCLIP_PUBLIC_URL",
"DATABASE_URL",
"PAPERCLIP_DB_BACKUP_ENABLED",
"PAPERCLIP_DB_BACKUP_INTERVAL_MINUTES",
"PAPERCLIP_DB_BACKUP_RETENTION_DAYS",
"PAPERCLIP_DB_BACKUP_DIR",
"PAPERCLIP_DEPLOYMENT_MODE",
"PAPERCLIP_DEPLOYMENT_EXPOSURE",
"PAPERCLIP_BIND",
"PAPERCLIP_BIND_HOST",
"PAPERCLIP_TAILNET_BIND_HOST",
"HOST",
"PORT",
"SERVE_UI",
"PAPERCLIP_ALLOWED_HOSTNAMES",
"PAPERCLIP_AUTH_BASE_URL_MODE",
"PAPERCLIP_AUTH_PUBLIC_BASE_URL",
"BETTER_AUTH_URL",
"BETTER_AUTH_BASE_URL",
"PAPERCLIP_STORAGE_PROVIDER",
"PAPERCLIP_STORAGE_LOCAL_DIR",
"PAPERCLIP_STORAGE_S3_BUCKET",
"PAPERCLIP_STORAGE_S3_REGION",
"PAPERCLIP_STORAGE_S3_ENDPOINT",
"PAPERCLIP_STORAGE_S3_PREFIX",
"PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE",
"PAPERCLIP_SECRETS_PROVIDER",
"PAPERCLIP_SECRETS_STRICT_MODE",
"PAPERCLIP_SECRETS_MASTER_KEY_FILE",
] as const;
function parseBooleanFromEnv(rawValue: string | undefined): boolean | null {
if (rawValue === undefined) return null;
const lower = rawValue.trim().toLowerCase();
if (lower === "true" || lower === "1" || lower === "yes") return true;
if (lower === "false" || lower === "0" || lower === "no") return false;
return null;
}
function parseNumberFromEnv(rawValue: string | undefined): number | null {
if (!rawValue) return null;
const parsed = Number(rawValue);
if (!Number.isFinite(parsed)) return null;
return parsed;
}
function parseEnumFromEnv<T extends string>(rawValue: string | undefined, allowedValues: readonly T[]): T | null {
if (!rawValue) return null;
return allowedValues.includes(rawValue as T) ? (rawValue as T) : null;
}
function resolvePathFromEnv(rawValue: string | undefined): string | null {
if (!rawValue || rawValue.trim().length === 0) return null;
return path.resolve(expandHomePrefix(rawValue.trim()));
}
function describeServerBinding(server: Pick<PaperclipConfig["server"], "bind" | "customBindHost" | "host" | "port">): string {
const bind = server.bind ?? inferBindModeFromHost(server.host);
const detail =
bind === "custom"
? server.customBindHost ?? server.host
: bind === "tailnet"
? "detected tailscale address"
: server.host;
return `${bind}${detail ? ` (${detail})` : ""}:${server.port}`;
}
function quickstartDefaultsFromEnv(opts?: { preferTrustedLocal?: boolean }): {
defaults: OnboardDefaults;
usedEnvKeys: string[];
ignoredEnvKeys: Array<{ key: string; reason: string }>;
} {
const preferTrustedLocal = opts?.preferTrustedLocal ?? false;
const instanceId = resolvePaperclipInstanceId();
return {
const defaultStorage = defaultStorageConfig();
const defaultSecrets = defaultSecretsConfig();
const databaseUrl = process.env.DATABASE_URL?.trim() || undefined;
const publicUrl = preferTrustedLocal
? undefined
: (
process.env.PAPERCLIP_PUBLIC_URL?.trim() ||
process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL?.trim() ||
process.env.BETTER_AUTH_URL?.trim() ||
process.env.BETTER_AUTH_BASE_URL?.trim() ||
undefined
);
const deploymentMode = preferTrustedLocal
? "local_trusted"
: (parseEnumFromEnv<DeploymentMode>(process.env.PAPERCLIP_DEPLOYMENT_MODE, DEPLOYMENT_MODES) ?? "local_trusted");
const deploymentExposureFromEnv = parseEnumFromEnv<DeploymentExposure>(
process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE,
DEPLOYMENT_EXPOSURES,
);
const deploymentExposure =
deploymentMode === "local_trusted" ? "private" : (deploymentExposureFromEnv ?? "private");
const bindFromEnv = parseEnumFromEnv<BindMode>(process.env.PAPERCLIP_BIND, BIND_MODES);
const customBindHostFromEnv = process.env.PAPERCLIP_BIND_HOST?.trim() || undefined;
const hostFromEnv = process.env.HOST?.trim() || undefined;
const configuredBindHost = customBindHostFromEnv ?? hostFromEnv;
const bind = preferTrustedLocal
? "loopback"
: (
deploymentMode === "local_trusted"
? "loopback"
: (bindFromEnv ?? (configuredBindHost ? inferBindModeFromHost(configuredBindHost) : "lan"))
);
const resolvedBind = resolveRuntimeBind({
bind,
host: hostFromEnv ?? (bind === "loopback" ? "127.0.0.1" : "0.0.0.0"),
customBindHost: customBindHostFromEnv,
tailnetBindHost: process.env.PAPERCLIP_TAILNET_BIND_HOST?.trim(),
});
const authPublicBaseUrl = publicUrl;
const authBaseUrlModeFromEnv = parseEnumFromEnv<AuthBaseUrlMode>(
process.env.PAPERCLIP_AUTH_BASE_URL_MODE,
AUTH_BASE_URL_MODES,
);
const authBaseUrlMode = authBaseUrlModeFromEnv ?? (authPublicBaseUrl ? "explicit" : "auto");
const allowedHostnamesFromEnv = process.env.PAPERCLIP_ALLOWED_HOSTNAMES
? process.env.PAPERCLIP_ALLOWED_HOSTNAMES
.split(",")
.map((value) => value.trim().toLowerCase())
.filter((value) => value.length > 0)
: [];
const hostnameFromPublicUrl = publicUrl
? (() => {
try {
return new URL(publicUrl).hostname.trim().toLowerCase();
} catch {
return null;
}
})()
: null;
const storageProvider =
parseEnumFromEnv<StorageProvider>(process.env.PAPERCLIP_STORAGE_PROVIDER, STORAGE_PROVIDERS) ??
defaultStorage.provider;
const secretsProvider =
parseEnumFromEnv<SecretProvider>(process.env.PAPERCLIP_SECRETS_PROVIDER, SECRET_PROVIDERS) ??
defaultSecrets.provider;
const databaseBackupEnabled = parseBooleanFromEnv(process.env.PAPERCLIP_DB_BACKUP_ENABLED) ?? true;
const databaseBackupIntervalMinutes = Math.max(
1,
parseNumberFromEnv(process.env.PAPERCLIP_DB_BACKUP_INTERVAL_MINUTES) ?? 60,
);
const databaseBackupRetentionDays = Math.max(
1,
parseNumberFromEnv(process.env.PAPERCLIP_DB_BACKUP_RETENTION_DAYS) ?? 30,
);
const defaults: OnboardDefaults = {
database: {
mode: "embedded-postgres",
mode: databaseUrl ? "postgres" : "embedded-postgres",
...(databaseUrl ? { connectionString: databaseUrl } : {}),
embeddedPostgresDataDir: resolveDefaultEmbeddedPostgresDir(instanceId),
embeddedPostgresPort: 54329,
backup: {
enabled: databaseBackupEnabled,
intervalMinutes: databaseBackupIntervalMinutes,
retentionDays: databaseBackupRetentionDays,
dir: resolvePathFromEnv(process.env.PAPERCLIP_DB_BACKUP_DIR) ?? resolveDefaultBackupDir(instanceId),
},
},
logging: {
mode: "file",
logDir: resolveDefaultLogsDir(instanceId),
},
server: {
deploymentMode: "local_trusted",
exposure: "private",
host: "127.0.0.1",
port: 3100,
allowedHostnames: [],
serveUi: true,
deploymentMode,
exposure: deploymentExposure,
bind: resolvedBind.bind,
...(resolvedBind.customBindHost ? { customBindHost: resolvedBind.customBindHost } : {}),
host: resolvedBind.host,
port: Number(process.env.PORT) || 3100,
allowedHostnames: Array.from(new Set([...allowedHostnamesFromEnv, ...(hostnameFromPublicUrl ? [hostnameFromPublicUrl] : [])])),
serveUi: parseBooleanFromEnv(process.env.SERVE_UI) ?? true,
},
auth: {
baseUrlMode: "auto",
baseUrlMode: authBaseUrlMode,
disableSignUp: false,
...(authPublicBaseUrl ? { publicBaseUrl: authPublicBaseUrl } : {}),
},
storage: {
provider: storageProvider,
localDisk: {
baseDir:
resolvePathFromEnv(process.env.PAPERCLIP_STORAGE_LOCAL_DIR) ?? defaultStorage.localDisk.baseDir,
},
s3: {
bucket: process.env.PAPERCLIP_STORAGE_S3_BUCKET ?? defaultStorage.s3.bucket,
region: process.env.PAPERCLIP_STORAGE_S3_REGION ?? defaultStorage.s3.region,
endpoint: process.env.PAPERCLIP_STORAGE_S3_ENDPOINT ?? defaultStorage.s3.endpoint,
prefix: process.env.PAPERCLIP_STORAGE_S3_PREFIX ?? defaultStorage.s3.prefix,
forcePathStyle:
parseBooleanFromEnv(process.env.PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE) ??
defaultStorage.s3.forcePathStyle,
},
},
secrets: {
provider: secretsProvider,
strictMode: parseBooleanFromEnv(process.env.PAPERCLIP_SECRETS_STRICT_MODE) ?? defaultSecrets.strictMode,
localEncrypted: {
keyFilePath:
resolvePathFromEnv(process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE) ??
defaultSecrets.localEncrypted.keyFilePath,
},
},
storage: defaultStorageConfig(),
secrets: defaultSecretsConfig(),
};
const ignoredEnvKeys: Array<{ key: string; reason: string }> = [];
if (preferTrustedLocal) {
const forcedLocalReason = "Ignored because --yes quickstart forces trusted local loopback defaults";
for (const key of [
"PAPERCLIP_DEPLOYMENT_MODE",
"PAPERCLIP_DEPLOYMENT_EXPOSURE",
"PAPERCLIP_BIND",
"PAPERCLIP_BIND_HOST",
"HOST",
"PAPERCLIP_AUTH_BASE_URL_MODE",
"PAPERCLIP_AUTH_PUBLIC_BASE_URL",
"PAPERCLIP_PUBLIC_URL",
"BETTER_AUTH_URL",
"BETTER_AUTH_BASE_URL",
] as const) {
if (process.env[key] !== undefined) {
ignoredEnvKeys.push({ key, reason: forcedLocalReason });
}
}
}
if (deploymentMode === "local_trusted" && process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE !== undefined) {
ignoredEnvKeys.push({
key: "PAPERCLIP_DEPLOYMENT_EXPOSURE",
reason: "Ignored because deployment mode local_trusted always forces private exposure",
});
}
if (deploymentMode === "local_trusted" && process.env.PAPERCLIP_BIND !== undefined) {
ignoredEnvKeys.push({
key: "PAPERCLIP_BIND",
reason: "Ignored because deployment mode local_trusted always uses loopback reachability",
});
}
if (deploymentMode === "local_trusted" && process.env.PAPERCLIP_BIND_HOST !== undefined) {
ignoredEnvKeys.push({
key: "PAPERCLIP_BIND_HOST",
reason: "Ignored because deployment mode local_trusted always uses loopback reachability",
});
}
if (deploymentMode === "local_trusted" && process.env.HOST !== undefined) {
ignoredEnvKeys.push({
key: "HOST",
reason: "Ignored because deployment mode local_trusted always uses loopback reachability",
});
}
const ignoredKeySet = new Set(ignoredEnvKeys.map((entry) => entry.key));
const usedEnvKeys = ONBOARD_ENV_KEYS.filter(
(key) => process.env[key] !== undefined && !ignoredKeySet.has(key),
);
return { defaults, usedEnvKeys, ignoredEnvKeys };
}
function canCreateBootstrapInviteImmediately(config: Pick<PaperclipConfig, "database" | "server">): boolean {
return config.server.deploymentMode === "authenticated" && config.database.mode !== "embedded-postgres";
}
export async function onboard(opts: OnboardOptions): Promise<void> {
if (opts.bind && !["loopback", "lan", "tailnet"].includes(opts.bind)) {
throw new Error(`Unsupported bind preset for onboard: ${opts.bind}. Use loopback, lan, or tailnet.`);
}
printPaperclipCliBanner();
p.intro(pc.bgCyan(pc.black(" paperclipai onboard ")));
const configPath = resolveConfigPath(opts.config);
@@ -67,11 +337,12 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
),
);
let existingConfig: PaperclipConfig | null = null;
if (configExists(opts.config)) {
p.log.message(pc.dim(`${configPath} exists, updating config`));
p.log.message(pc.dim(`${configPath} exists`));
try {
readConfig(opts.config);
existingConfig = readConfig(opts.config);
} catch (err) {
p.log.message(
pc.yellow(
@@ -81,9 +352,85 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
}
}
if (existingConfig) {
p.log.message(
pc.dim("Existing Paperclip install detected; keeping the current configuration unchanged."),
);
p.log.message(pc.dim(`Use ${pc.cyan("paperclipai configure")} if you want to change settings.`));
const jwtSecret = ensureAgentJwtSecret(configPath);
const envFilePath = resolveAgentJwtEnvFile(configPath);
if (jwtSecret.created) {
p.log.success(`Created ${pc.cyan("PAPERCLIP_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`);
} else if (process.env.PAPERCLIP_AGENT_JWT_SECRET?.trim()) {
p.log.info(`Using existing ${pc.cyan("PAPERCLIP_AGENT_JWT_SECRET")} from environment`);
} else {
p.log.info(`Using existing ${pc.cyan("PAPERCLIP_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`);
}
const keyResult = ensureLocalSecretsKeyFile(existingConfig, configPath);
if (keyResult.status === "created") {
p.log.success(`Created local secrets key file at ${pc.dim(keyResult.path)}`);
} else if (keyResult.status === "existing") {
p.log.message(pc.dim(`Using existing local secrets key file at ${keyResult.path}`));
}
p.note(
[
"Existing config preserved",
`Database: ${existingConfig.database.mode}`,
existingConfig.llm ? `LLM: ${existingConfig.llm.provider}` : "LLM: not configured",
`Logging: ${existingConfig.logging.mode} -> ${existingConfig.logging.logDir}`,
`Server: ${existingConfig.server.deploymentMode}/${existingConfig.server.exposure} @ ${describeServerBinding(existingConfig.server)}`,
`Allowed hosts: ${existingConfig.server.allowedHostnames.length > 0 ? existingConfig.server.allowedHostnames.join(", ") : "(loopback only)"}`,
`Auth URL mode: ${existingConfig.auth.baseUrlMode}${existingConfig.auth.publicBaseUrl ? ` (${existingConfig.auth.publicBaseUrl})` : ""}`,
`Storage: ${existingConfig.storage.provider}`,
`Secrets: ${existingConfig.secrets.provider} (strict mode ${existingConfig.secrets.strictMode ? "on" : "off"})`,
"Agent auth: PAPERCLIP_AGENT_JWT_SECRET configured",
].join("\n"),
"Configuration ready",
);
p.note(
[
`Run: ${pc.cyan("paperclipai run")}`,
`Reconfigure later: ${pc.cyan("paperclipai configure")}`,
`Diagnose setup: ${pc.cyan("paperclipai doctor")}`,
].join("\n"),
"Next commands",
);
let shouldRunNow = opts.run === true || opts.yes === true;
if (!shouldRunNow && !opts.invokedByRun && process.stdin.isTTY && process.stdout.isTTY) {
const answer = await p.confirm({
message: "Start Paperclip now?",
initialValue: true,
});
if (!p.isCancel(answer)) {
shouldRunNow = answer;
}
}
if (shouldRunNow && !opts.invokedByRun) {
process.env.PAPERCLIP_OPEN_ON_LISTEN = "true";
const { runCommand } = await import("./run.js");
await runCommand({ config: configPath, repair: true, yes: true });
return;
}
p.outro("Existing Paperclip setup is ready.");
return;
}
let setupMode: SetupMode = "quickstart";
if (opts.yes) {
p.log.message(pc.dim("`--yes` enabled: using Quickstart defaults."));
p.log.message(
pc.dim(
opts.bind
? `\`--yes\` enabled: using Quickstart defaults with bind=${opts.bind}.`
: "`--yes` enabled: using Quickstart defaults.",
),
);
} else {
const setupModeChoice = await p.select({
message: "Choose setup path",
@@ -108,7 +455,13 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
setupMode = setupModeChoice as SetupMode;
}
const tc = getTelemetryClient();
if (tc) trackInstallStarted(tc);
let llm: PaperclipConfig["llm"] | undefined;
const { defaults: derivedDefaults, usedEnvKeys, ignoredEnvKeys } = quickstartDefaultsFromEnv({
preferTrustedLocal: opts.yes === true && !opts.bind,
});
let {
database,
logging,
@@ -116,11 +469,24 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
auth,
storage,
secrets,
} = quickstartDefaults();
} = derivedDefaults;
if (opts.bind === "loopback" || opts.bind === "lan" || opts.bind === "tailnet") {
const preset = buildPresetServerConfig(opts.bind, {
port: server.port,
allowedHostnames: server.allowedHostnames,
serveUi: server.serveUi,
});
server = preset.server;
auth = preset.auth;
if (opts.bind === "tailnet" && server.host === "127.0.0.1") {
p.log.warn(TAILNET_BIND_WARNING);
}
}
if (setupMode === "advanced") {
p.log.step(pc.bold("Database"));
database = await promptDatabase();
database = await promptDatabase(database);
if (database.mode === "postgres" && database.connectionString) {
const s = p.spinner();
@@ -184,13 +550,20 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
logging = await promptLogging();
p.log.step(pc.bold("Server"));
({ server, auth } = await promptServer());
({ server, auth } = await promptServer({ currentServer: server, currentAuth: auth }));
p.log.step(pc.bold("Storage"));
storage = await promptStorage(defaultStorageConfig());
storage = await promptStorage(storage);
p.log.step(pc.bold("Secrets"));
secrets = defaultSecretsConfig();
const secretsDefaults = defaultSecretsConfig();
secrets = {
provider: secrets.provider ?? secretsDefaults.provider,
strictMode: secrets.strictMode ?? secretsDefaults.strictMode,
localEncrypted: {
keyFilePath: secrets.localEncrypted?.keyFilePath ?? secretsDefaults.localEncrypted.keyFilePath,
},
};
p.log.message(
pc.dim(
`Using defaults: provider=${secrets.provider}, strictMode=${secrets.strictMode}, keyFile=${secrets.localEncrypted.keyFilePath}`,
@@ -199,8 +572,22 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
} else {
p.log.step(pc.bold("Quickstart"));
p.log.message(
pc.dim("Using local defaults: embedded database, no LLM provider, file storage, and local encrypted secrets."),
pc.dim(
opts.bind
? `Using quickstart defaults with bind=${opts.bind}.`
: `Using quickstart defaults: ${server.deploymentMode}/${server.exposure} @ ${describeServerBinding(server)}.`,
),
);
if (usedEnvKeys.length > 0) {
p.log.message(pc.dim(`Environment-aware defaults active (${usedEnvKeys.length} env var(s) detected).`));
} else {
p.log.message(
pc.dim("No environment overrides detected: embedded database, file storage, local encrypted secrets."),
);
}
for (const ignored of ignoredEnvKeys) {
p.log.message(pc.dim(`Ignored ${ignored.key}: ${ignored.reason}`));
}
}
const jwtSecret = ensureAgentJwtSecret(configPath);
@@ -224,6 +611,9 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
logging,
server,
auth,
telemetry: {
enabled: true,
},
storage,
secrets,
};
@@ -237,12 +627,16 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
writeConfig(config, opts.config);
if (tc) trackInstallCompleted(tc, {
adapterType: server.deploymentMode,
});
p.note(
[
`Database: ${database.mode}`,
llm ? `LLM: ${llm.provider}` : "LLM: not configured",
`Logging: ${logging.mode} -> ${logging.logDir}`,
`Server: ${server.deploymentMode}/${server.exposure} @ ${server.host}:${server.port}`,
`Server: ${server.deploymentMode}/${server.exposure} @ ${describeServerBinding(server)}`,
`Allowed hosts: ${server.allowedHostnames.length > 0 ? server.allowedHostnames.join(", ") : "(loopback only)"}`,
`Auth URL mode: ${auth.baseUrlMode}${auth.publicBaseUrl ? ` (${auth.publicBaseUrl})` : ""}`,
`Storage: ${storage.provider}`,
@@ -261,7 +655,7 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
"Next commands",
);
if (server.deploymentMode === "authenticated") {
if (canCreateBootstrapInviteImmediately({ database, server })) {
p.log.step("Generating bootstrap CEO invite");
await bootstrapCeoInvite({ config: configPath });
}
@@ -284,5 +678,15 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
return;
}
if (server.deploymentMode === "authenticated" && database.mode === "embedded-postgres") {
p.log.info(
[
"Bootstrap CEO invite will be created after the server starts.",
`Next: ${pc.cyan("paperclipai run")}`,
`Then: ${pc.cyan("paperclipai auth bootstrap-ceo")}`,
].join("\n"),
);
}
p.outro("You're all set!");
}

View File

@@ -0,0 +1,352 @@
import fs from "node:fs";
import net from "node:net";
import path from "node:path";
import { Command } from "commander";
import pc from "picocolors";
import {
applyPendingMigrations,
createDb,
createEmbeddedPostgresLogBuffer,
ensurePostgresDatabase,
formatEmbeddedPostgresError,
routines,
} from "@paperclipai/db";
import { eq, inArray } from "drizzle-orm";
import { loadPaperclipEnvFile } from "../config/env.js";
import { readConfig, resolveConfigPath } from "../config/store.js";
type RoutinesDisableAllOptions = {
config?: string;
dataDir?: string;
companyId?: string;
json?: boolean;
};
type DisableAllRoutinesResult = {
companyId: string;
totalRoutines: number;
pausedCount: number;
alreadyPausedCount: number;
archivedCount: number;
};
type EmbeddedPostgresInstance = {
initialise(): Promise<void>;
start(): Promise<void>;
stop(): Promise<void>;
};
type EmbeddedPostgresCtor = new (opts: {
databaseDir: string;
user: string;
password: string;
port: number;
persistent: boolean;
initdbFlags?: string[];
onLog?: (message: unknown) => void;
onError?: (message: unknown) => void;
}) => EmbeddedPostgresInstance;
type EmbeddedPostgresHandle = {
port: number;
startedByThisProcess: boolean;
stop: () => Promise<void>;
};
type ClosableDb = ReturnType<typeof createDb> & {
$client?: {
end?: (options?: { timeout?: number }) => Promise<void>;
};
};
function nonEmpty(value: string | null | undefined): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
async function isPortAvailable(port: number): Promise<boolean> {
return await new Promise<boolean>((resolve) => {
const server = net.createServer();
server.unref();
server.once("error", () => resolve(false));
server.listen(port, "127.0.0.1", () => {
server.close(() => resolve(true));
});
});
}
async function findAvailablePort(preferredPort: number): Promise<number> {
let port = Math.max(1, Math.trunc(preferredPort));
while (!(await isPortAvailable(port))) {
port += 1;
}
return port;
}
function readPidFilePort(postmasterPidFile: string): number | null {
if (!fs.existsSync(postmasterPidFile)) return null;
try {
const lines = fs.readFileSync(postmasterPidFile, "utf8").split("\n");
const port = Number(lines[3]?.trim());
return Number.isInteger(port) && port > 0 ? port : null;
} catch {
return null;
}
}
function readRunningPostmasterPid(postmasterPidFile: string): number | null {
if (!fs.existsSync(postmasterPidFile)) return null;
try {
const pid = Number(fs.readFileSync(postmasterPidFile, "utf8").split("\n")[0]?.trim());
if (!Number.isInteger(pid) || pid <= 0) return null;
process.kill(pid, 0);
return pid;
} catch {
return null;
}
}
async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): Promise<EmbeddedPostgresHandle> {
const moduleName = "embedded-postgres";
let EmbeddedPostgres: EmbeddedPostgresCtor;
try {
const mod = await import(moduleName);
EmbeddedPostgres = mod.default as EmbeddedPostgresCtor;
} catch {
throw new Error(
"Embedded PostgreSQL support requires dependency `embedded-postgres`. Reinstall dependencies and try again.",
);
}
const postmasterPidFile = path.resolve(dataDir, "postmaster.pid");
const runningPid = readRunningPostmasterPid(postmasterPidFile);
if (runningPid) {
return {
port: readPidFilePort(postmasterPidFile) ?? preferredPort,
startedByThisProcess: false,
stop: async () => {},
};
}
const port = await findAvailablePort(preferredPort);
const logBuffer = createEmbeddedPostgresLogBuffer();
const instance = new EmbeddedPostgres({
databaseDir: dataDir,
user: "paperclip",
password: "paperclip",
port,
persistent: true,
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
onLog: logBuffer.append,
onError: logBuffer.append,
});
if (!fs.existsSync(path.resolve(dataDir, "PG_VERSION"))) {
try {
await instance.initialise();
} catch (error) {
throw formatEmbeddedPostgresError(error, {
fallbackMessage: `Failed to initialize embedded PostgreSQL cluster in ${dataDir} on port ${port}`,
recentLogs: logBuffer.getRecentLogs(),
});
}
}
if (fs.existsSync(postmasterPidFile)) {
fs.rmSync(postmasterPidFile, { force: true });
}
try {
await instance.start();
} catch (error) {
throw formatEmbeddedPostgresError(error, {
fallbackMessage: `Failed to start embedded PostgreSQL on port ${port}`,
recentLogs: logBuffer.getRecentLogs(),
});
}
return {
port,
startedByThisProcess: true,
stop: async () => {
await instance.stop();
},
};
}
async function closeDb(db: ClosableDb): Promise<void> {
await db.$client?.end?.({ timeout: 5 }).catch(() => undefined);
}
async function openConfiguredDb(configPath: string): Promise<{
db: ClosableDb;
stop: () => Promise<void>;
}> {
const config = readConfig(configPath);
if (!config) {
throw new Error(`Config not found at ${configPath}.`);
}
let embeddedHandle: EmbeddedPostgresHandle | null = null;
try {
if (config.database.mode === "embedded-postgres") {
embeddedHandle = await ensureEmbeddedPostgres(
config.database.embeddedPostgresDataDir,
config.database.embeddedPostgresPort,
);
const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${embeddedHandle.port}/postgres`;
await ensurePostgresDatabase(adminConnectionString, "paperclip");
const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${embeddedHandle.port}/paperclip`;
await applyPendingMigrations(connectionString);
const db = createDb(connectionString) as ClosableDb;
return {
db,
stop: async () => {
await closeDb(db);
if (embeddedHandle?.startedByThisProcess) {
await embeddedHandle.stop().catch(() => undefined);
}
},
};
}
const connectionString = nonEmpty(config.database.connectionString);
if (!connectionString) {
throw new Error(`Config at ${configPath} does not define a database connection string.`);
}
await applyPendingMigrations(connectionString);
const db = createDb(connectionString) as ClosableDb;
return {
db,
stop: async () => {
await closeDb(db);
},
};
} catch (error) {
if (embeddedHandle?.startedByThisProcess) {
await embeddedHandle.stop().catch(() => undefined);
}
throw error;
}
}
export async function disableAllRoutinesInConfig(
options: Pick<RoutinesDisableAllOptions, "config" | "companyId">,
): Promise<DisableAllRoutinesResult> {
const configPath = resolveConfigPath(options.config);
loadPaperclipEnvFile(configPath);
const companyId =
nonEmpty(options.companyId)
?? nonEmpty(process.env.PAPERCLIP_COMPANY_ID)
?? null;
if (!companyId) {
throw new Error("Company ID is required. Pass --company-id or set PAPERCLIP_COMPANY_ID.");
}
const config = readConfig(configPath);
if (!config) {
throw new Error(`Config not found at ${configPath}.`);
}
let embeddedHandle: EmbeddedPostgresHandle | null = null;
let db: ClosableDb | null = null;
try {
if (config.database.mode === "embedded-postgres") {
embeddedHandle = await ensureEmbeddedPostgres(
config.database.embeddedPostgresDataDir,
config.database.embeddedPostgresPort,
);
const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${embeddedHandle.port}/postgres`;
await ensurePostgresDatabase(adminConnectionString, "paperclip");
const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${embeddedHandle.port}/paperclip`;
await applyPendingMigrations(connectionString);
db = createDb(connectionString) as ClosableDb;
} else {
const connectionString = nonEmpty(config.database.connectionString);
if (!connectionString) {
throw new Error(`Config at ${configPath} does not define a database connection string.`);
}
await applyPendingMigrations(connectionString);
db = createDb(connectionString) as ClosableDb;
}
const existing = await db
.select({
id: routines.id,
status: routines.status,
})
.from(routines)
.where(eq(routines.companyId, companyId));
const alreadyPausedCount = existing.filter((routine) => routine.status === "paused").length;
const archivedCount = existing.filter((routine) => routine.status === "archived").length;
const idsToPause = existing
.filter((routine) => routine.status !== "paused" && routine.status !== "archived")
.map((routine) => routine.id);
if (idsToPause.length > 0) {
await db
.update(routines)
.set({
status: "paused",
updatedAt: new Date(),
})
.where(inArray(routines.id, idsToPause));
}
return {
companyId,
totalRoutines: existing.length,
pausedCount: idsToPause.length,
alreadyPausedCount,
archivedCount,
};
} finally {
if (db) {
await closeDb(db);
}
if (embeddedHandle?.startedByThisProcess) {
await embeddedHandle.stop().catch(() => undefined);
}
}
}
export async function disableAllRoutinesCommand(options: RoutinesDisableAllOptions): Promise<void> {
const result = await disableAllRoutinesInConfig(options);
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
if (result.totalRoutines === 0) {
console.log(pc.dim(`No routines found for company ${result.companyId}.`));
return;
}
console.log(
`Paused ${result.pausedCount} routine(s) for company ${result.companyId} ` +
`(${result.alreadyPausedCount} already paused, ${result.archivedCount} archived).`,
);
}
export function registerRoutineCommands(program: Command): void {
const routinesCommand = program.command("routines").description("Local routine maintenance commands");
routinesCommand
.command("disable-all")
.description("Pause all non-archived routines in the configured local instance for one company")
.option("-c, --config <path>", "Path to config file")
.option("-d, --data-dir <path>", "Paperclip data directory root (isolates state from ~/.paperclip)")
.option("-C, --company-id <id>", "Company ID")
.option("--json", "Output raw JSON")
.action(async (opts: RoutinesDisableAllOptions) => {
try {
await disableAllRoutinesCommand(opts);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(pc.red(message));
process.exit(1);
}
});
}

View File

@@ -1,11 +1,16 @@
import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath, pathToFileURL } from "node:url";
import * as p from "@clack/prompts";
import pc from "picocolors";
import { bootstrapCeoInvite } from "./auth-bootstrap-ceo.js";
import { onboard } from "./onboard.js";
import { doctor } from "./doctor.js";
import { loadPaperclipEnvFile } from "../config/env.js";
import { configExists, resolveConfigPath } from "../config/store.js";
import type { PaperclipConfig } from "../config/schema.js";
import { readConfig } from "../config/store.js";
import {
describeLocalInstancePaths,
resolvePaperclipHomeDir,
@@ -17,6 +22,14 @@ interface RunOptions {
instance?: string;
repair?: boolean;
yes?: boolean;
bind?: "loopback" | "lan" | "tailnet";
}
interface StartedServer {
apiUrl: string;
databaseUrl: string;
host: string;
listenPort: number;
}
export async function runCommand(opts: RunOptions): Promise<void> {
@@ -31,6 +44,7 @@ export async function runCommand(opts: RunOptions): Promise<void> {
const configPath = resolveConfigPath(opts.config);
process.env.PAPERCLIP_CONFIG = configPath;
loadPaperclipEnvFile(configPath);
p.intro(pc.bgCyan(pc.black(" paperclipai run ")));
p.log.message(pc.dim(`Home: ${paths.homeDir}`));
@@ -45,7 +59,7 @@ export async function runCommand(opts: RunOptions): Promise<void> {
}
p.log.step("No config found. Starting onboarding...");
await onboard({ config: configPath, invokedByRun: true });
await onboard({ config: configPath, invokedByRun: true, bind: opts.bind });
}
p.log.step("Running doctor checks...");
@@ -60,8 +74,41 @@ export async function runCommand(opts: RunOptions): Promise<void> {
process.exit(1);
}
const config = readConfig(configPath);
if (!config) {
p.log.error(`No config found at ${configPath}.`);
process.exit(1);
}
p.log.step("Starting Paperclip server...");
await importServerEntry();
const startedServer = await importServerEntry();
if (shouldGenerateBootstrapInviteAfterStart(config)) {
p.log.step("Generating bootstrap CEO invite");
await bootstrapCeoInvite({
config: configPath,
dbUrl: startedServer.databaseUrl,
baseUrl: resolveBootstrapInviteBaseUrl(config, startedServer),
});
}
}
function resolveBootstrapInviteBaseUrl(
config: PaperclipConfig,
startedServer: StartedServer,
): string {
const explicitBaseUrl =
process.env.PAPERCLIP_PUBLIC_URL ??
process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL ??
process.env.BETTER_AUTH_URL ??
process.env.BETTER_AUTH_BASE_URL ??
(config.auth.baseUrlMode === "explicit" ? config.auth.publicBaseUrl : undefined);
if (typeof explicitBaseUrl === "string" && explicitBaseUrl.trim().length > 0) {
return explicitBaseUrl.trim().replace(/\/+$/, "");
}
return startedServer.apiUrl.replace(/\/api$/, "");
}
function formatError(err: unknown): string {
@@ -84,6 +131,15 @@ function isModuleNotFoundError(err: unknown): boolean {
return err.message.includes("Cannot find module");
}
function getMissingModuleSpecifier(err: unknown): string | null {
if (!(err instanceof Error)) return null;
const packageMatch = err.message.match(/Cannot find package '([^']+)' imported from/);
if (packageMatch?.[1]) return packageMatch[1];
const moduleMatch = err.message.match(/Cannot find module '([^']+)'/);
if (moduleMatch?.[1]) return moduleMatch[1];
return null;
}
function maybeEnableUiDevMiddleware(entrypoint: string): void {
if (process.env.PAPERCLIP_UI_DEV_MIDDLEWARE !== undefined) return;
const normalized = entrypoint.replaceAll("\\", "/");
@@ -92,24 +148,69 @@ function maybeEnableUiDevMiddleware(entrypoint: string): void {
}
}
async function importServerEntry(): Promise<void> {
function ensureDevWorkspaceBuildDeps(projectRoot: string): void {
const buildScript = path.resolve(projectRoot, "scripts/ensure-plugin-build-deps.mjs");
if (!fs.existsSync(buildScript)) return;
const result = spawnSync(process.execPath, [buildScript], {
cwd: projectRoot,
stdio: "inherit",
timeout: 120_000,
});
if (result.error) {
throw new Error(
`Failed to prepare workspace build artifacts before starting the Paperclip dev server.\n${formatError(result.error)}`,
);
}
if ((result.status ?? 1) !== 0) {
throw new Error(
"Failed to prepare workspace build artifacts before starting the Paperclip dev server.",
);
}
}
async function importServerEntry(): Promise<StartedServer> {
// Dev mode: try local workspace path (monorepo with tsx)
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const devEntry = path.resolve(projectRoot, "server/src/index.ts");
if (fs.existsSync(devEntry)) {
ensureDevWorkspaceBuildDeps(projectRoot);
maybeEnableUiDevMiddleware(devEntry);
await import(pathToFileURL(devEntry).href);
return;
const mod = await import(pathToFileURL(devEntry).href);
return await startServerFromModule(mod, devEntry);
}
// Production mode: import the published @paperclipai/server package
try {
await import("@paperclipai/server");
const mod = await import("@paperclipai/server");
return await startServerFromModule(mod, "@paperclipai/server");
} catch (err) {
const missingSpecifier = getMissingModuleSpecifier(err);
const missingServerEntrypoint = !missingSpecifier || missingSpecifier === "@paperclipai/server";
if (isModuleNotFoundError(err) && missingServerEntrypoint) {
throw new Error(
`Could not locate a Paperclip server entrypoint.\n` +
`Tried: ${devEntry}, @paperclipai/server\n` +
`${formatError(err)}`,
);
}
throw new Error(
`Could not locate a Paperclip server entrypoint.\n` +
`Tried: ${devEntry}, @paperclipai/server\n` +
`Paperclip server failed to start.\n` +
`${formatError(err)}`,
);
}
}
function shouldGenerateBootstrapInviteAfterStart(config: PaperclipConfig): boolean {
return config.server.deploymentMode === "authenticated" && config.database.mode === "embedded-postgres";
}
async function startServerFromModule(mod: unknown, label: string): Promise<StartedServer> {
const startServer = (mod as { startServer?: () => Promise<StartedServer> }).startServer;
if (typeof startServer !== "function") {
throw new Error(`Paperclip server entrypoint did not export startServer(): ${label}`);
}
return await startServer();
}

View File

@@ -0,0 +1,279 @@
import { randomInt } from "node:crypto";
import path from "node:path";
import type { PaperclipConfig } from "../config/schema.js";
import { expandHomePrefix } from "../config/home.js";
export const DEFAULT_WORKTREE_HOME = "~/.paperclip-worktrees";
export const WORKTREE_SEED_MODES = ["minimal", "full"] as const;
export type WorktreeSeedMode = (typeof WORKTREE_SEED_MODES)[number];
export type WorktreeSeedPlan = {
mode: WorktreeSeedMode;
excludedTables: string[];
nullifyColumns: Record<string, string[]>;
};
const MINIMAL_WORKTREE_EXCLUDED_TABLES = [
"activity_log",
"agent_runtime_state",
"agent_task_sessions",
"agent_wakeup_requests",
"cost_events",
"heartbeat_run_events",
"heartbeat_runs",
"workspace_runtime_services",
];
const MINIMAL_WORKTREE_NULLIFIED_COLUMNS: Record<string, string[]> = {
issues: ["checkout_run_id", "execution_run_id"],
};
export type WorktreeLocalPaths = {
cwd: string;
repoConfigDir: string;
configPath: string;
envPath: string;
homeDir: string;
instanceId: string;
instanceRoot: string;
contextPath: string;
embeddedPostgresDataDir: string;
backupDir: string;
logDir: string;
secretsKeyFilePath: string;
storageDir: string;
};
export type WorktreeUiBranding = {
name: string;
color: string;
};
export function isWorktreeSeedMode(value: string): value is WorktreeSeedMode {
return (WORKTREE_SEED_MODES as readonly string[]).includes(value);
}
export function resolveWorktreeSeedPlan(mode: WorktreeSeedMode): WorktreeSeedPlan {
if (mode === "full") {
return {
mode,
excludedTables: [],
nullifyColumns: {},
};
}
return {
mode,
excludedTables: [...MINIMAL_WORKTREE_EXCLUDED_TABLES],
nullifyColumns: {
...MINIMAL_WORKTREE_NULLIFIED_COLUMNS,
},
};
}
function nonEmpty(value: string | null | undefined): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function isLoopbackHost(hostname: string): boolean {
const value = hostname.trim().toLowerCase();
return value === "127.0.0.1" || value === "localhost" || value === "::1";
}
export function sanitizeWorktreeInstanceId(rawValue: string): string {
const trimmed = rawValue.trim().toLowerCase();
const normalized = trimmed
.replace(/[^a-z0-9_-]+/g, "-")
.replace(/-+/g, "-")
.replace(/^[-_]+|[-_]+$/g, "");
return normalized || "worktree";
}
export function resolveSuggestedWorktreeName(cwd: string, explicitName?: string): string {
return nonEmpty(explicitName) ?? path.basename(path.resolve(cwd));
}
function hslComponentToHex(n: number): string {
return Math.round(Math.max(0, Math.min(255, n)))
.toString(16)
.padStart(2, "0");
}
function hslToHex(hue: number, saturation: number, lightness: number): string {
const s = Math.max(0, Math.min(100, saturation)) / 100;
const l = Math.max(0, Math.min(100, lightness)) / 100;
const c = (1 - Math.abs((2 * l) - 1)) * s;
const h = ((hue % 360) + 360) % 360;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = l - (c / 2);
let r = 0;
let g = 0;
let b = 0;
if (h < 60) {
r = c;
g = x;
} else if (h < 120) {
r = x;
g = c;
} else if (h < 180) {
g = c;
b = x;
} else if (h < 240) {
g = x;
b = c;
} else if (h < 300) {
r = x;
b = c;
} else {
r = c;
b = x;
}
return `#${hslComponentToHex((r + m) * 255)}${hslComponentToHex((g + m) * 255)}${hslComponentToHex((b + m) * 255)}`;
}
export function generateWorktreeColor(): string {
return hslToHex(randomInt(0, 360), 68, 56);
}
export function resolveWorktreeLocalPaths(opts: {
cwd: string;
homeDir?: string;
instanceId: string;
}): WorktreeLocalPaths {
const cwd = path.resolve(opts.cwd);
const homeDir = path.resolve(expandHomePrefix(opts.homeDir ?? DEFAULT_WORKTREE_HOME));
const instanceRoot = path.resolve(homeDir, "instances", opts.instanceId);
const repoConfigDir = path.resolve(cwd, ".paperclip");
return {
cwd,
repoConfigDir,
configPath: path.resolve(repoConfigDir, "config.json"),
envPath: path.resolve(repoConfigDir, ".env"),
homeDir,
instanceId: opts.instanceId,
instanceRoot,
contextPath: path.resolve(homeDir, "context.json"),
embeddedPostgresDataDir: path.resolve(instanceRoot, "db"),
backupDir: path.resolve(instanceRoot, "data", "backups"),
logDir: path.resolve(instanceRoot, "logs"),
secretsKeyFilePath: path.resolve(instanceRoot, "secrets", "master.key"),
storageDir: path.resolve(instanceRoot, "data", "storage"),
};
}
export function rewriteLocalUrlPort(rawUrl: string | undefined, port: number): string | undefined {
if (!rawUrl) return undefined;
try {
const parsed = new URL(rawUrl);
if (!isLoopbackHost(parsed.hostname)) return rawUrl;
parsed.port = String(port);
return parsed.toString();
} catch {
return rawUrl;
}
}
export function buildWorktreeConfig(input: {
sourceConfig: PaperclipConfig | null;
paths: WorktreeLocalPaths;
serverPort: number;
databasePort: number;
now?: Date;
}): PaperclipConfig {
const { sourceConfig, paths, serverPort, databasePort } = input;
const nowIso = (input.now ?? new Date()).toISOString();
const source = sourceConfig;
const authPublicBaseUrl = rewriteLocalUrlPort(source?.auth.publicBaseUrl, serverPort);
return {
$meta: {
version: 1,
updatedAt: nowIso,
source: "configure",
},
...(source?.llm ? { llm: source.llm } : {}),
database: {
mode: "embedded-postgres",
embeddedPostgresDataDir: paths.embeddedPostgresDataDir,
embeddedPostgresPort: databasePort,
backup: {
enabled: source?.database.backup.enabled ?? true,
intervalMinutes: source?.database.backup.intervalMinutes ?? 60,
retentionDays: source?.database.backup.retentionDays ?? 30,
dir: paths.backupDir,
},
},
logging: {
mode: source?.logging.mode ?? "file",
logDir: paths.logDir,
},
server: {
deploymentMode: source?.server.deploymentMode ?? "local_trusted",
exposure: source?.server.exposure ?? "private",
...(source?.server.bind ? { bind: source.server.bind } : {}),
...(source?.server.customBindHost ? { customBindHost: source.server.customBindHost } : {}),
host: source?.server.host ?? "127.0.0.1",
port: serverPort,
allowedHostnames: source?.server.allowedHostnames ?? [],
serveUi: source?.server.serveUi ?? true,
},
auth: {
baseUrlMode: source?.auth.baseUrlMode ?? "auto",
...(authPublicBaseUrl ? { publicBaseUrl: authPublicBaseUrl } : {}),
disableSignUp: source?.auth.disableSignUp ?? false,
},
telemetry: {
enabled: source?.telemetry?.enabled ?? true,
},
storage: {
provider: source?.storage.provider ?? "local_disk",
localDisk: {
baseDir: paths.storageDir,
},
s3: {
bucket: source?.storage.s3.bucket ?? "paperclip",
region: source?.storage.s3.region ?? "us-east-1",
endpoint: source?.storage.s3.endpoint,
prefix: source?.storage.s3.prefix ?? "",
forcePathStyle: source?.storage.s3.forcePathStyle ?? false,
},
},
secrets: {
provider: source?.secrets.provider ?? "local_encrypted",
strictMode: source?.secrets.strictMode ?? false,
localEncrypted: {
keyFilePath: paths.secretsKeyFilePath,
},
},
};
}
export function buildWorktreeEnvEntries(
paths: WorktreeLocalPaths,
branding?: WorktreeUiBranding,
): Record<string, string> {
return {
PAPERCLIP_HOME: paths.homeDir,
PAPERCLIP_INSTANCE_ID: paths.instanceId,
PAPERCLIP_CONFIG: paths.configPath,
PAPERCLIP_CONTEXT: paths.contextPath,
PAPERCLIP_IN_WORKTREE: "true",
...(branding?.name ? { PAPERCLIP_WORKTREE_NAME: branding.name } : {}),
...(branding?.color ? { PAPERCLIP_WORKTREE_COLOR: branding.color } : {}),
};
}
function shellEscape(value: string): string {
return `'${value.replaceAll("'", `'\"'\"'`)}'`;
}
export function formatShellExports(entries: Record<string, string>): string {
return Object.entries(entries)
.filter(([, value]) => typeof value === "string" && value.trim().length > 0)
.map(([key, value]) => `export ${key}=${shellEscape(value)}`)
.join("\n");
}

View File

@@ -0,0 +1,764 @@
import {
agents,
assets,
documentRevisions,
goals,
issueAttachments,
issueComments,
issueDocuments,
issues,
projects,
projectWorkspaces,
} from "@paperclipai/db";
type IssueRow = typeof issues.$inferSelect;
type CommentRow = typeof issueComments.$inferSelect;
type AgentRow = typeof agents.$inferSelect;
type ProjectRow = typeof projects.$inferSelect;
type ProjectWorkspaceRow = typeof projectWorkspaces.$inferSelect;
type GoalRow = typeof goals.$inferSelect;
type IssueDocumentLinkRow = typeof issueDocuments.$inferSelect;
type DocumentRevisionTableRow = typeof documentRevisions.$inferSelect;
type IssueAttachmentTableRow = typeof issueAttachments.$inferSelect;
type AssetRow = typeof assets.$inferSelect;
export const WORKTREE_MERGE_SCOPES = ["issues", "comments"] as const;
export type WorktreeMergeScope = (typeof WORKTREE_MERGE_SCOPES)[number];
export type ImportAdjustment =
| "clear_assignee_agent"
| "clear_project"
| "clear_project_workspace"
| "clear_goal"
| "clear_author_agent"
| "coerce_in_progress_to_todo"
| "clear_document_agent"
| "clear_document_revision_agent"
| "clear_attachment_agent";
export type IssueMergeAction = "skip_existing" | "insert";
export type CommentMergeAction = "skip_existing" | "skip_missing_parent" | "insert";
export type PlannedIssueInsert = {
source: IssueRow;
action: "insert";
previewIssueNumber: number;
previewIdentifier: string;
targetStatus: string;
targetAssigneeAgentId: string | null;
targetCreatedByAgentId: string | null;
targetProjectId: string | null;
targetProjectWorkspaceId: string | null;
targetGoalId: string | null;
projectResolution: "preserved" | "cleared" | "mapped" | "imported";
mappedProjectName: string | null;
adjustments: ImportAdjustment[];
};
export type PlannedIssueSkip = {
source: IssueRow;
action: "skip_existing";
driftKeys: string[];
};
export type PlannedCommentInsert = {
source: CommentRow;
action: "insert";
targetAuthorAgentId: string | null;
adjustments: ImportAdjustment[];
};
export type PlannedCommentSkip = {
source: CommentRow;
action: "skip_existing" | "skip_missing_parent";
};
export type IssueDocumentRow = {
id: IssueDocumentLinkRow["id"];
companyId: IssueDocumentLinkRow["companyId"];
issueId: IssueDocumentLinkRow["issueId"];
documentId: IssueDocumentLinkRow["documentId"];
key: IssueDocumentLinkRow["key"];
linkCreatedAt: IssueDocumentLinkRow["createdAt"];
linkUpdatedAt: IssueDocumentLinkRow["updatedAt"];
title: string | null;
format: string;
latestBody: string;
latestRevisionId: string | null;
latestRevisionNumber: number;
createdByAgentId: string | null;
createdByUserId: string | null;
updatedByAgentId: string | null;
updatedByUserId: string | null;
documentCreatedAt: Date;
documentUpdatedAt: Date;
};
export type DocumentRevisionRow = {
id: DocumentRevisionTableRow["id"];
companyId: DocumentRevisionTableRow["companyId"];
documentId: DocumentRevisionTableRow["documentId"];
revisionNumber: DocumentRevisionTableRow["revisionNumber"];
body: DocumentRevisionTableRow["body"];
changeSummary: DocumentRevisionTableRow["changeSummary"];
createdByAgentId: string | null;
createdByUserId: string | null;
createdAt: Date;
};
export type IssueAttachmentRow = {
id: IssueAttachmentTableRow["id"];
companyId: IssueAttachmentTableRow["companyId"];
issueId: IssueAttachmentTableRow["issueId"];
issueCommentId: IssueAttachmentTableRow["issueCommentId"];
assetId: IssueAttachmentTableRow["assetId"];
provider: AssetRow["provider"];
objectKey: AssetRow["objectKey"];
contentType: AssetRow["contentType"];
byteSize: AssetRow["byteSize"];
sha256: AssetRow["sha256"];
originalFilename: AssetRow["originalFilename"];
createdByAgentId: string | null;
createdByUserId: string | null;
assetCreatedAt: Date;
assetUpdatedAt: Date;
attachmentCreatedAt: Date;
attachmentUpdatedAt: Date;
};
export type PlannedDocumentRevisionInsert = {
source: DocumentRevisionRow;
targetRevisionNumber: number;
targetCreatedByAgentId: string | null;
adjustments: ImportAdjustment[];
};
export type PlannedIssueDocumentInsert = {
source: IssueDocumentRow;
action: "insert";
targetCreatedByAgentId: string | null;
targetUpdatedByAgentId: string | null;
latestRevisionId: string | null;
latestRevisionNumber: number;
revisionsToInsert: PlannedDocumentRevisionInsert[];
adjustments: ImportAdjustment[];
};
export type PlannedIssueDocumentMerge = {
source: IssueDocumentRow;
action: "merge_existing";
targetCreatedByAgentId: string | null;
targetUpdatedByAgentId: string | null;
latestRevisionId: string | null;
latestRevisionNumber: number;
revisionsToInsert: PlannedDocumentRevisionInsert[];
adjustments: ImportAdjustment[];
};
export type PlannedIssueDocumentSkip = {
source: IssueDocumentRow;
action: "skip_existing" | "skip_missing_parent" | "skip_conflicting_key";
};
export type PlannedAttachmentInsert = {
source: IssueAttachmentRow;
action: "insert";
targetIssueCommentId: string | null;
targetCreatedByAgentId: string | null;
adjustments: ImportAdjustment[];
};
export type PlannedAttachmentSkip = {
source: IssueAttachmentRow;
action: "skip_existing" | "skip_missing_parent";
};
export type PlannedProjectImport = {
source: ProjectRow;
targetLeadAgentId: string | null;
targetGoalId: string | null;
workspaces: ProjectWorkspaceRow[];
};
export type WorktreeMergePlan = {
companyId: string;
companyName: string;
issuePrefix: string;
previewIssueCounterStart: number;
scopes: WorktreeMergeScope[];
projectImports: PlannedProjectImport[];
issuePlans: Array<PlannedIssueInsert | PlannedIssueSkip>;
commentPlans: Array<PlannedCommentInsert | PlannedCommentSkip>;
documentPlans: Array<PlannedIssueDocumentInsert | PlannedIssueDocumentMerge | PlannedIssueDocumentSkip>;
attachmentPlans: Array<PlannedAttachmentInsert | PlannedAttachmentSkip>;
counts: {
projectsToImport: number;
issuesToInsert: number;
issuesExisting: number;
issueDrift: number;
commentsToInsert: number;
commentsExisting: number;
commentsMissingParent: number;
documentsToInsert: number;
documentsToMerge: number;
documentsExisting: number;
documentsConflictingKey: number;
documentsMissingParent: number;
documentRevisionsToInsert: number;
attachmentsToInsert: number;
attachmentsExisting: number;
attachmentsMissingParent: number;
};
adjustments: Record<ImportAdjustment, number>;
};
function compareIssueCoreFields(source: IssueRow, target: IssueRow): string[] {
const driftKeys: string[] = [];
if (source.title !== target.title) driftKeys.push("title");
if ((source.description ?? null) !== (target.description ?? null)) driftKeys.push("description");
if (source.status !== target.status) driftKeys.push("status");
if (source.priority !== target.priority) driftKeys.push("priority");
if ((source.parentId ?? null) !== (target.parentId ?? null)) driftKeys.push("parentId");
if ((source.projectId ?? null) !== (target.projectId ?? null)) driftKeys.push("projectId");
if ((source.projectWorkspaceId ?? null) !== (target.projectWorkspaceId ?? null)) driftKeys.push("projectWorkspaceId");
if ((source.goalId ?? null) !== (target.goalId ?? null)) driftKeys.push("goalId");
if ((source.assigneeAgentId ?? null) !== (target.assigneeAgentId ?? null)) driftKeys.push("assigneeAgentId");
if ((source.assigneeUserId ?? null) !== (target.assigneeUserId ?? null)) driftKeys.push("assigneeUserId");
return driftKeys;
}
function incrementAdjustment(
counts: Record<ImportAdjustment, number>,
adjustment: ImportAdjustment,
): void {
counts[adjustment] += 1;
}
function groupBy<T>(rows: T[], keyFor: (row: T) => string): Map<string, T[]> {
const out = new Map<string, T[]>();
for (const row of rows) {
const key = keyFor(row);
const existing = out.get(key);
if (existing) {
existing.push(row);
} else {
out.set(key, [row]);
}
}
return out;
}
function sameDate(left: Date, right: Date): boolean {
return left.getTime() === right.getTime();
}
function sortDocumentRows(rows: IssueDocumentRow[]): IssueDocumentRow[] {
return [...rows].sort((left, right) => {
const createdDelta = left.documentCreatedAt.getTime() - right.documentCreatedAt.getTime();
if (createdDelta !== 0) return createdDelta;
const linkDelta = left.linkCreatedAt.getTime() - right.linkCreatedAt.getTime();
if (linkDelta !== 0) return linkDelta;
return left.documentId.localeCompare(right.documentId);
});
}
function sortDocumentRevisions(rows: DocumentRevisionRow[]): DocumentRevisionRow[] {
return [...rows].sort((left, right) => {
const revisionDelta = left.revisionNumber - right.revisionNumber;
if (revisionDelta !== 0) return revisionDelta;
const createdDelta = left.createdAt.getTime() - right.createdAt.getTime();
if (createdDelta !== 0) return createdDelta;
return left.id.localeCompare(right.id);
});
}
function sortAttachments(rows: IssueAttachmentRow[]): IssueAttachmentRow[] {
return [...rows].sort((left, right) => {
const createdDelta = left.attachmentCreatedAt.getTime() - right.attachmentCreatedAt.getTime();
if (createdDelta !== 0) return createdDelta;
return left.id.localeCompare(right.id);
});
}
function sortIssuesForImport(sourceIssues: IssueRow[]): IssueRow[] {
const byId = new Map(sourceIssues.map((issue) => [issue.id, issue]));
const memoDepth = new Map<string, number>();
const depthFor = (issue: IssueRow, stack = new Set<string>()): number => {
const memoized = memoDepth.get(issue.id);
if (memoized !== undefined) return memoized;
if (!issue.parentId) {
memoDepth.set(issue.id, 0);
return 0;
}
if (stack.has(issue.id)) {
memoDepth.set(issue.id, 0);
return 0;
}
const parent = byId.get(issue.parentId);
if (!parent) {
memoDepth.set(issue.id, 0);
return 0;
}
stack.add(issue.id);
const depth = depthFor(parent, stack) + 1;
stack.delete(issue.id);
memoDepth.set(issue.id, depth);
return depth;
};
return [...sourceIssues].sort((left, right) => {
const depthDelta = depthFor(left) - depthFor(right);
if (depthDelta !== 0) return depthDelta;
const createdDelta = left.createdAt.getTime() - right.createdAt.getTime();
if (createdDelta !== 0) return createdDelta;
return left.id.localeCompare(right.id);
});
}
export function parseWorktreeMergeScopes(rawValue: string | undefined): WorktreeMergeScope[] {
if (!rawValue || rawValue.trim().length === 0) {
return ["issues", "comments"];
}
const parsed = rawValue
.split(",")
.map((value) => value.trim().toLowerCase())
.filter((value): value is WorktreeMergeScope =>
(WORKTREE_MERGE_SCOPES as readonly string[]).includes(value),
);
if (parsed.length === 0) {
throw new Error(
`Invalid scope "${rawValue}". Expected a comma-separated list of: ${WORKTREE_MERGE_SCOPES.join(", ")}.`,
);
}
return [...new Set(parsed)];
}
export function buildWorktreeMergePlan(input: {
companyId: string;
companyName: string;
issuePrefix: string;
previewIssueCounterStart: number;
scopes: WorktreeMergeScope[];
sourceIssues: IssueRow[];
targetIssues: IssueRow[];
sourceComments: CommentRow[];
targetComments: CommentRow[];
sourceProjects?: ProjectRow[];
sourceProjectWorkspaces?: ProjectWorkspaceRow[];
sourceDocuments?: IssueDocumentRow[];
targetDocuments?: IssueDocumentRow[];
sourceDocumentRevisions?: DocumentRevisionRow[];
targetDocumentRevisions?: DocumentRevisionRow[];
sourceAttachments?: IssueAttachmentRow[];
targetAttachments?: IssueAttachmentRow[];
targetAgents: AgentRow[];
targetProjects: ProjectRow[];
targetProjectWorkspaces: ProjectWorkspaceRow[];
targetGoals: GoalRow[];
importProjectIds?: Iterable<string>;
projectIdOverrides?: Record<string, string | null | undefined>;
}): WorktreeMergePlan {
const targetIssuesById = new Map(input.targetIssues.map((issue) => [issue.id, issue]));
const targetCommentIds = new Set(input.targetComments.map((comment) => comment.id));
const targetAgentIds = new Set(input.targetAgents.map((agent) => agent.id));
const targetProjectIds = new Set(input.targetProjects.map((project) => project.id));
const targetProjectsById = new Map(input.targetProjects.map((project) => [project.id, project]));
const targetProjectWorkspaceIds = new Set(input.targetProjectWorkspaces.map((workspace) => workspace.id));
const targetGoalIds = new Set(input.targetGoals.map((goal) => goal.id));
const sourceProjectsById = new Map((input.sourceProjects ?? []).map((project) => [project.id, project]));
const sourceProjectWorkspaces = input.sourceProjectWorkspaces ?? [];
const sourceProjectWorkspacesByProjectId = groupBy(sourceProjectWorkspaces, (workspace) => workspace.projectId);
const importProjectIds = new Set(input.importProjectIds ?? []);
const scopes = new Set(input.scopes);
const adjustmentCounts: Record<ImportAdjustment, number> = {
clear_assignee_agent: 0,
clear_project: 0,
clear_project_workspace: 0,
clear_goal: 0,
clear_author_agent: 0,
coerce_in_progress_to_todo: 0,
clear_document_agent: 0,
clear_document_revision_agent: 0,
clear_attachment_agent: 0,
};
const projectImports: PlannedProjectImport[] = [];
for (const projectId of importProjectIds) {
if (targetProjectIds.has(projectId)) continue;
const sourceProject = sourceProjectsById.get(projectId);
if (!sourceProject) continue;
projectImports.push({
source: sourceProject,
targetLeadAgentId:
sourceProject.leadAgentId && targetAgentIds.has(sourceProject.leadAgentId)
? sourceProject.leadAgentId
: null,
targetGoalId:
sourceProject.goalId && targetGoalIds.has(sourceProject.goalId)
? sourceProject.goalId
: null,
workspaces: [...(sourceProjectWorkspacesByProjectId.get(projectId) ?? [])].sort((left, right) => {
const primaryDelta = Number(right.isPrimary) - Number(left.isPrimary);
if (primaryDelta !== 0) return primaryDelta;
const createdDelta = left.createdAt.getTime() - right.createdAt.getTime();
if (createdDelta !== 0) return createdDelta;
return left.id.localeCompare(right.id);
}),
});
}
const importedProjectWorkspaceIds = new Set(
projectImports.flatMap((project) => project.workspaces.map((workspace) => workspace.id)),
);
const issuePlans: Array<PlannedIssueInsert | PlannedIssueSkip> = [];
let nextPreviewIssueNumber = input.previewIssueCounterStart;
for (const issue of sortIssuesForImport(input.sourceIssues)) {
const existing = targetIssuesById.get(issue.id);
if (existing) {
issuePlans.push({
source: issue,
action: "skip_existing",
driftKeys: compareIssueCoreFields(issue, existing),
});
continue;
}
nextPreviewIssueNumber += 1;
const adjustments: ImportAdjustment[] = [];
const targetAssigneeAgentId =
issue.assigneeAgentId && targetAgentIds.has(issue.assigneeAgentId) ? issue.assigneeAgentId : null;
if (issue.assigneeAgentId && !targetAssigneeAgentId) {
adjustments.push("clear_assignee_agent");
incrementAdjustment(adjustmentCounts, "clear_assignee_agent");
}
const targetCreatedByAgentId =
issue.createdByAgentId && targetAgentIds.has(issue.createdByAgentId) ? issue.createdByAgentId : null;
let targetProjectId =
issue.projectId && targetProjectIds.has(issue.projectId) ? issue.projectId : null;
let projectResolution: PlannedIssueInsert["projectResolution"] = targetProjectId ? "preserved" : "cleared";
let mappedProjectName: string | null = null;
const overrideProjectId =
issue.projectId && input.projectIdOverrides
? input.projectIdOverrides[issue.projectId] ?? null
: null;
if (!targetProjectId && overrideProjectId && targetProjectIds.has(overrideProjectId)) {
targetProjectId = overrideProjectId;
projectResolution = "mapped";
mappedProjectName = targetProjectsById.get(overrideProjectId)?.name ?? null;
}
if (!targetProjectId && issue.projectId && importProjectIds.has(issue.projectId)) {
const sourceProject = sourceProjectsById.get(issue.projectId);
if (sourceProject) {
targetProjectId = sourceProject.id;
projectResolution = "imported";
mappedProjectName = sourceProject.name;
}
}
if (issue.projectId && !targetProjectId) {
adjustments.push("clear_project");
incrementAdjustment(adjustmentCounts, "clear_project");
}
const targetProjectWorkspaceId =
targetProjectId
&& targetProjectId === issue.projectId
&& issue.projectWorkspaceId
&& (targetProjectWorkspaceIds.has(issue.projectWorkspaceId)
|| importedProjectWorkspaceIds.has(issue.projectWorkspaceId))
? issue.projectWorkspaceId
: null;
if (issue.projectWorkspaceId && !targetProjectWorkspaceId) {
adjustments.push("clear_project_workspace");
incrementAdjustment(adjustmentCounts, "clear_project_workspace");
}
const targetGoalId =
issue.goalId && targetGoalIds.has(issue.goalId) ? issue.goalId : null;
if (issue.goalId && !targetGoalId) {
adjustments.push("clear_goal");
incrementAdjustment(adjustmentCounts, "clear_goal");
}
let targetStatus = issue.status;
if (
targetStatus === "in_progress"
&& !targetAssigneeAgentId
&& !(issue.assigneeUserId && issue.assigneeUserId.trim().length > 0)
) {
targetStatus = "todo";
adjustments.push("coerce_in_progress_to_todo");
incrementAdjustment(adjustmentCounts, "coerce_in_progress_to_todo");
}
issuePlans.push({
source: issue,
action: "insert",
previewIssueNumber: nextPreviewIssueNumber,
previewIdentifier: `${input.issuePrefix}-${nextPreviewIssueNumber}`,
targetStatus,
targetAssigneeAgentId,
targetCreatedByAgentId,
targetProjectId,
targetProjectWorkspaceId,
targetGoalId,
projectResolution,
mappedProjectName,
adjustments,
});
}
const issueIdsAvailableAfterImport = new Set<string>([
...input.targetIssues.map((issue) => issue.id),
...issuePlans.filter((plan): plan is PlannedIssueInsert => plan.action === "insert").map((plan) => plan.source.id),
]);
const commentPlans: Array<PlannedCommentInsert | PlannedCommentSkip> = [];
if (scopes.has("comments")) {
const sortedComments = [...input.sourceComments].sort((left, right) => {
const createdDelta = left.createdAt.getTime() - right.createdAt.getTime();
if (createdDelta !== 0) return createdDelta;
return left.id.localeCompare(right.id);
});
for (const comment of sortedComments) {
if (targetCommentIds.has(comment.id)) {
commentPlans.push({ source: comment, action: "skip_existing" });
continue;
}
if (!issueIdsAvailableAfterImport.has(comment.issueId)) {
commentPlans.push({ source: comment, action: "skip_missing_parent" });
continue;
}
const adjustments: ImportAdjustment[] = [];
const targetAuthorAgentId =
comment.authorAgentId && targetAgentIds.has(comment.authorAgentId) ? comment.authorAgentId : null;
if (comment.authorAgentId && !targetAuthorAgentId) {
adjustments.push("clear_author_agent");
incrementAdjustment(adjustmentCounts, "clear_author_agent");
}
commentPlans.push({
source: comment,
action: "insert",
targetAuthorAgentId,
adjustments,
});
}
}
const sourceDocuments = input.sourceDocuments ?? [];
const targetDocuments = input.targetDocuments ?? [];
const sourceDocumentRevisions = input.sourceDocumentRevisions ?? [];
const targetDocumentRevisions = input.targetDocumentRevisions ?? [];
const targetDocumentsById = new Map(targetDocuments.map((document) => [document.documentId, document]));
const targetDocumentsByIssueKey = new Map(targetDocuments.map((document) => [`${document.issueId}:${document.key}`, document]));
const sourceRevisionsByDocumentId = groupBy(sourceDocumentRevisions, (revision) => revision.documentId);
const targetRevisionsByDocumentId = groupBy(targetDocumentRevisions, (revision) => revision.documentId);
const commentIdsAvailableAfterImport = new Set<string>([
...input.targetComments.map((comment) => comment.id),
...commentPlans.filter((plan): plan is PlannedCommentInsert => plan.action === "insert").map((plan) => plan.source.id),
]);
const documentPlans: Array<PlannedIssueDocumentInsert | PlannedIssueDocumentMerge | PlannedIssueDocumentSkip> = [];
for (const document of sortDocumentRows(sourceDocuments)) {
if (!issueIdsAvailableAfterImport.has(document.issueId)) {
documentPlans.push({ source: document, action: "skip_missing_parent" });
continue;
}
const existingDocument = targetDocumentsById.get(document.documentId);
const conflictingIssueKeyDocument = targetDocumentsByIssueKey.get(`${document.issueId}:${document.key}`);
if (!existingDocument && conflictingIssueKeyDocument && conflictingIssueKeyDocument.documentId !== document.documentId) {
documentPlans.push({ source: document, action: "skip_conflicting_key" });
continue;
}
const adjustments: ImportAdjustment[] = [];
const targetCreatedByAgentId =
document.createdByAgentId && targetAgentIds.has(document.createdByAgentId) ? document.createdByAgentId : null;
const targetUpdatedByAgentId =
document.updatedByAgentId && targetAgentIds.has(document.updatedByAgentId) ? document.updatedByAgentId : null;
if (
(document.createdByAgentId && !targetCreatedByAgentId)
|| (document.updatedByAgentId && !targetUpdatedByAgentId)
) {
adjustments.push("clear_document_agent");
incrementAdjustment(adjustmentCounts, "clear_document_agent");
}
const sourceRevisions = sortDocumentRevisions(sourceRevisionsByDocumentId.get(document.documentId) ?? []);
const targetRevisions = sortDocumentRevisions(targetRevisionsByDocumentId.get(document.documentId) ?? []);
const existingRevisionIds = new Set(targetRevisions.map((revision) => revision.id));
const usedRevisionNumbers = new Set(targetRevisions.map((revision) => revision.revisionNumber));
let nextRevisionNumber = targetRevisions.reduce(
(maxValue, revision) => Math.max(maxValue, revision.revisionNumber),
0,
) + 1;
const targetRevisionNumberById = new Map<string, number>(
targetRevisions.map((revision) => [revision.id, revision.revisionNumber]),
);
const revisionsToInsert: PlannedDocumentRevisionInsert[] = [];
for (const revision of sourceRevisions) {
if (existingRevisionIds.has(revision.id)) continue;
let targetRevisionNumber = revision.revisionNumber;
if (usedRevisionNumbers.has(targetRevisionNumber)) {
while (usedRevisionNumbers.has(nextRevisionNumber)) {
nextRevisionNumber += 1;
}
targetRevisionNumber = nextRevisionNumber;
nextRevisionNumber += 1;
}
usedRevisionNumbers.add(targetRevisionNumber);
targetRevisionNumberById.set(revision.id, targetRevisionNumber);
const revisionAdjustments: ImportAdjustment[] = [];
const targetCreatedByAgentId =
revision.createdByAgentId && targetAgentIds.has(revision.createdByAgentId) ? revision.createdByAgentId : null;
if (revision.createdByAgentId && !targetCreatedByAgentId) {
revisionAdjustments.push("clear_document_revision_agent");
incrementAdjustment(adjustmentCounts, "clear_document_revision_agent");
}
revisionsToInsert.push({
source: revision,
targetRevisionNumber,
targetCreatedByAgentId,
adjustments: revisionAdjustments,
});
}
const latestRevisionId = document.latestRevisionId ?? existingDocument?.latestRevisionId ?? null;
const latestRevisionNumber =
(latestRevisionId ? targetRevisionNumberById.get(latestRevisionId) : undefined)
?? document.latestRevisionNumber
?? existingDocument?.latestRevisionNumber
?? 0;
if (!existingDocument) {
documentPlans.push({
source: document,
action: "insert",
targetCreatedByAgentId,
targetUpdatedByAgentId,
latestRevisionId,
latestRevisionNumber,
revisionsToInsert,
adjustments,
});
continue;
}
const documentAlreadyMatches =
existingDocument.key === document.key
&& existingDocument.title === document.title
&& existingDocument.format === document.format
&& existingDocument.latestBody === document.latestBody
&& (existingDocument.latestRevisionId ?? null) === latestRevisionId
&& existingDocument.latestRevisionNumber === latestRevisionNumber
&& (existingDocument.updatedByAgentId ?? null) === targetUpdatedByAgentId
&& (existingDocument.updatedByUserId ?? null) === (document.updatedByUserId ?? null)
&& sameDate(existingDocument.documentUpdatedAt, document.documentUpdatedAt)
&& sameDate(existingDocument.linkUpdatedAt, document.linkUpdatedAt)
&& revisionsToInsert.length === 0;
if (documentAlreadyMatches) {
documentPlans.push({ source: document, action: "skip_existing" });
continue;
}
documentPlans.push({
source: document,
action: "merge_existing",
targetCreatedByAgentId,
targetUpdatedByAgentId,
latestRevisionId,
latestRevisionNumber,
revisionsToInsert,
adjustments,
});
}
const sourceAttachments = input.sourceAttachments ?? [];
const targetAttachmentIds = new Set((input.targetAttachments ?? []).map((attachment) => attachment.id));
const attachmentPlans: Array<PlannedAttachmentInsert | PlannedAttachmentSkip> = [];
for (const attachment of sortAttachments(sourceAttachments)) {
if (targetAttachmentIds.has(attachment.id)) {
attachmentPlans.push({ source: attachment, action: "skip_existing" });
continue;
}
if (!issueIdsAvailableAfterImport.has(attachment.issueId)) {
attachmentPlans.push({ source: attachment, action: "skip_missing_parent" });
continue;
}
const adjustments: ImportAdjustment[] = [];
const targetCreatedByAgentId =
attachment.createdByAgentId && targetAgentIds.has(attachment.createdByAgentId)
? attachment.createdByAgentId
: null;
if (attachment.createdByAgentId && !targetCreatedByAgentId) {
adjustments.push("clear_attachment_agent");
incrementAdjustment(adjustmentCounts, "clear_attachment_agent");
}
attachmentPlans.push({
source: attachment,
action: "insert",
targetIssueCommentId:
attachment.issueCommentId && commentIdsAvailableAfterImport.has(attachment.issueCommentId)
? attachment.issueCommentId
: null,
targetCreatedByAgentId,
adjustments,
});
}
const counts = {
projectsToImport: projectImports.length,
issuesToInsert: issuePlans.filter((plan) => plan.action === "insert").length,
issuesExisting: issuePlans.filter((plan) => plan.action === "skip_existing").length,
issueDrift: issuePlans.filter((plan) => plan.action === "skip_existing" && plan.driftKeys.length > 0).length,
commentsToInsert: commentPlans.filter((plan) => plan.action === "insert").length,
commentsExisting: commentPlans.filter((plan) => plan.action === "skip_existing").length,
commentsMissingParent: commentPlans.filter((plan) => plan.action === "skip_missing_parent").length,
documentsToInsert: documentPlans.filter((plan) => plan.action === "insert").length,
documentsToMerge: documentPlans.filter((plan) => plan.action === "merge_existing").length,
documentsExisting: documentPlans.filter((plan) => plan.action === "skip_existing").length,
documentsConflictingKey: documentPlans.filter((plan) => plan.action === "skip_conflicting_key").length,
documentsMissingParent: documentPlans.filter((plan) => plan.action === "skip_missing_parent").length,
documentRevisionsToInsert: documentPlans.reduce(
(sum, plan) =>
sum + (plan.action === "insert" || plan.action === "merge_existing" ? plan.revisionsToInsert.length : 0),
0,
),
attachmentsToInsert: attachmentPlans.filter((plan) => plan.action === "insert").length,
attachmentsExisting: attachmentPlans.filter((plan) => plan.action === "skip_existing").length,
attachmentsMissingParent: attachmentPlans.filter((plan) => plan.action === "skip_missing_parent").length,
};
return {
companyId: input.companyId,
companyName: input.companyName,
issuePrefix: input.issuePrefix,
previewIssueCounterStart: input.previewIssueCounterStart,
scopes: input.scopes,
projectImports,
issuePlans,
commentPlans,
documentPlans,
attachmentPlans,
counts,
adjustments: adjustmentCounts,
};
}

2876
cli/src/commands/worktree.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -22,20 +22,35 @@ function parseEnvFile(contents: string) {
}
}
function formatEnvValue(value: string): string {
if (/^[A-Za-z0-9_./:@-]+$/.test(value)) {
return value;
}
return JSON.stringify(value);
}
function renderEnvFile(entries: Record<string, string>) {
const lines = [
"# Paperclip environment variables",
"# Generated by `paperclipai onboard`",
...Object.entries(entries).map(([key, value]) => `${key}=${value}`),
"# Generated by Paperclip CLI commands",
...Object.entries(entries).map(([key, value]) => `${key}=${formatEnvValue(value)}`),
"",
];
return lines.join("\n");
}
export function resolvePaperclipEnvFile(configPath?: string): string {
return resolveEnvFilePath(configPath);
}
export function resolveAgentJwtEnvFile(configPath?: string): string {
return resolveEnvFilePath(configPath);
}
export function loadPaperclipEnvFile(configPath?: string): void {
loadAgentJwtEnvFile(resolveEnvFilePath(configPath));
}
export function loadAgentJwtEnvFile(filePath = resolveEnvFilePath()): void {
if (loadedEnvFiles.has(filePath)) return;
@@ -78,13 +93,33 @@ export function ensureAgentJwtSecret(configPath?: string): { secret: string; cre
}
export function writeAgentJwtEnv(secret: string, filePath = resolveEnvFilePath()): void {
mergePaperclipEnvEntries({ [JWT_SECRET_ENV_KEY]: secret }, filePath);
}
export function readPaperclipEnvEntries(filePath = resolveEnvFilePath()): Record<string, string> {
if (!fs.existsSync(filePath)) return {};
return parseEnvFile(fs.readFileSync(filePath, "utf-8"));
}
export function writePaperclipEnvEntries(entries: Record<string, string>, filePath = resolveEnvFilePath()): void {
const dir = path.dirname(filePath);
fs.mkdirSync(dir, { recursive: true });
const current = fs.existsSync(filePath) ? parseEnvFile(fs.readFileSync(filePath, "utf-8")) : {};
current[JWT_SECRET_ENV_KEY] = secret;
fs.writeFileSync(filePath, renderEnvFile(current), {
fs.writeFileSync(filePath, renderEnvFile(entries), {
mode: 0o600,
});
}
export function mergePaperclipEnvEntries(
entries: Record<string, string>,
filePath = resolveEnvFilePath(),
): Record<string, string> {
const current = readPaperclipEnvEntries(filePath);
const next = {
...current,
...Object.fromEntries(
Object.entries(entries).filter(([, value]) => typeof value === "string" && value.trim().length > 0),
),
};
writePaperclipEnvEntries(next, filePath);
return next;
}

View File

@@ -33,6 +33,10 @@ export function resolveDefaultContextPath(): string {
return path.resolve(resolvePaperclipHomeDir(), "context.json");
}
export function resolveDefaultCliAuthPath(): string {
return path.resolve(resolvePaperclipHomeDir(), "auth.json");
}
export function resolveDefaultEmbeddedPostgresDir(instanceId?: string): string {
return path.resolve(resolvePaperclipInstanceRoot(instanceId), "db");
}
@@ -49,6 +53,10 @@ export function resolveDefaultStorageDir(instanceId?: string): string {
return path.resolve(resolvePaperclipInstanceRoot(instanceId), "data", "storage");
}
export function resolveDefaultBackupDir(instanceId?: string): string {
return path.resolve(resolvePaperclipInstanceRoot(instanceId), "data", "backups");
}
export function expandHomePrefix(value: string): string {
if (value === "~") return os.homedir();
if (value.startsWith("~/")) return path.resolve(os.homedir(), value.slice(2));
@@ -64,6 +72,7 @@ export function describeLocalInstancePaths(instanceId?: string) {
instanceRoot,
configPath: resolveDefaultConfigPath(resolvedInstanceId),
embeddedPostgresDataDir: resolveDefaultEmbeddedPostgresDir(resolvedInstanceId),
backupDir: resolveDefaultBackupDir(resolvedInstanceId),
logDir: resolveDefaultLogsDir(resolvedInstanceId),
secretsKeyFilePath: resolveDefaultSecretsKeyFilePath(resolvedInstanceId),
storageDir: resolveDefaultStorageDir(resolvedInstanceId),

View File

@@ -2,10 +2,12 @@ export {
paperclipConfigSchema,
configMetaSchema,
llmConfigSchema,
databaseBackupConfigSchema,
databaseConfigSchema,
loggingConfigSchema,
serverConfigSchema,
authConfigSchema,
telemetryConfigSchema,
storageConfigSchema,
storageLocalDiskConfigSchema,
storageS3ConfigSchema,
@@ -13,14 +15,16 @@ export {
secretsLocalEncryptedConfigSchema,
type PaperclipConfig,
type LlmConfig,
type DatabaseBackupConfig,
type DatabaseConfig,
type LoggingConfig,
type ServerConfig,
type AuthConfig,
type TelemetryConfig,
type StorageConfig,
type StorageLocalDiskConfig,
type StorageS3Config,
type SecretsConfig,
type SecretsLocalEncryptedConfig,
type ConfigMeta,
} from "@paperclipai/shared";
} from "../../../packages/shared/src/config-schema.js";

View File

@@ -0,0 +1,183 @@
import { execFileSync } from "node:child_process";
import {
ALL_INTERFACES_BIND_HOST,
LOOPBACK_BIND_HOST,
inferBindModeFromHost,
isAllInterfacesHost,
isLoopbackHost,
type BindMode,
type DeploymentExposure,
type DeploymentMode,
} from "@paperclipai/shared";
import type { AuthConfig, ServerConfig } from "./schema.js";
const TAILSCALE_DETECT_TIMEOUT_MS = 3000;
type BaseServerInput = {
port: number;
allowedHostnames: string[];
serveUi: boolean;
};
export function inferConfiguredBind(server?: Partial<ServerConfig>): BindMode {
if (server?.bind) return server.bind;
return inferBindModeFromHost(server?.customBindHost ?? server?.host);
}
export function detectTailnetBindHost(): string | undefined {
const explicit = process.env.PAPERCLIP_TAILNET_BIND_HOST?.trim();
if (explicit) return explicit;
try {
const stdout = execFileSync("tailscale", ["ip", "-4"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
timeout: TAILSCALE_DETECT_TIMEOUT_MS,
});
return stdout
.split(/\r?\n/)
.map((line) => line.trim())
.find(Boolean);
} catch {
return undefined;
}
}
export function buildPresetServerConfig(
bind: Exclude<BindMode, "custom">,
input: BaseServerInput,
): { server: ServerConfig; auth: AuthConfig } {
const host =
bind === "loopback"
? LOOPBACK_BIND_HOST
: bind === "tailnet"
? (detectTailnetBindHost() ?? LOOPBACK_BIND_HOST)
: ALL_INTERFACES_BIND_HOST;
return {
server: {
deploymentMode: bind === "loopback" ? "local_trusted" : "authenticated",
exposure: "private",
bind,
customBindHost: undefined,
host,
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
},
auth: {
baseUrlMode: "auto",
disableSignUp: false,
},
};
}
export function buildCustomServerConfig(input: BaseServerInput & {
deploymentMode: DeploymentMode;
exposure: DeploymentExposure;
host: string;
publicBaseUrl?: string;
}): { server: ServerConfig; auth: AuthConfig } {
const normalizedHost = input.host.trim();
const bind = isLoopbackHost(normalizedHost)
? "loopback"
: isAllInterfacesHost(normalizedHost)
? "lan"
: "custom";
return {
server: {
deploymentMode: input.deploymentMode,
exposure: input.deploymentMode === "local_trusted" ? "private" : input.exposure,
bind,
customBindHost: bind === "custom" ? normalizedHost : undefined,
host: normalizedHost,
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
},
auth:
input.deploymentMode === "authenticated" && input.exposure === "public"
? {
baseUrlMode: "explicit",
disableSignUp: false,
publicBaseUrl: input.publicBaseUrl,
}
: {
baseUrlMode: "auto",
disableSignUp: false,
},
};
}
export function resolveQuickstartServerConfig(input: {
bind?: BindMode | null;
deploymentMode?: DeploymentMode | null;
exposure?: DeploymentExposure | null;
host?: string | null;
port: number;
allowedHostnames: string[];
serveUi: boolean;
publicBaseUrl?: string;
}): { server: ServerConfig; auth: AuthConfig } {
const trimmedHost = input.host?.trim();
const explicitBind = input.bind ?? null;
if (explicitBind === "loopback" || explicitBind === "lan" || explicitBind === "tailnet") {
return buildPresetServerConfig(explicitBind, {
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
});
}
if (explicitBind === "custom") {
return buildCustomServerConfig({
deploymentMode: input.deploymentMode ?? "authenticated",
exposure: input.exposure ?? "private",
host: trimmedHost || LOOPBACK_BIND_HOST,
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
publicBaseUrl: input.publicBaseUrl,
});
}
if (trimmedHost) {
return buildCustomServerConfig({
deploymentMode: input.deploymentMode ?? (isLoopbackHost(trimmedHost) ? "local_trusted" : "authenticated"),
exposure: input.exposure ?? "private",
host: trimmedHost,
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
publicBaseUrl: input.publicBaseUrl,
});
}
if (input.deploymentMode === "authenticated") {
if (input.exposure === "public") {
return buildCustomServerConfig({
deploymentMode: "authenticated",
exposure: "public",
host: ALL_INTERFACES_BIND_HOST,
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
publicBaseUrl: input.publicBaseUrl,
});
}
return buildPresetServerConfig("lan", {
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
});
}
return buildPresetServerConfig("loopback", {
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
});
}

View File

@@ -7,6 +7,7 @@ import { addAllowedHostname } from "./commands/allowed-hostname.js";
import { heartbeatRun } from "./commands/heartbeat-run.js";
import { runCommand } from "./commands/run.js";
import { bootstrapCeoInvite } from "./commands/auth-bootstrap-ceo.js";
import { dbBackupCommand } from "./commands/db-backup.js";
import { registerContextCommands } from "./commands/client/context.js";
import { registerCompanyCommands } from "./commands/client/company.js";
import { registerIssueCommands } from "./commands/client/issue.js";
@@ -14,7 +15,15 @@ import { registerAgentCommands } from "./commands/client/agent.js";
import { registerApprovalCommands } from "./commands/client/approval.js";
import { registerActivityCommands } from "./commands/client/activity.js";
import { registerDashboardCommands } from "./commands/client/dashboard.js";
import { registerRoutineCommands } from "./commands/routines.js";
import { registerFeedbackCommands } from "./commands/client/feedback.js";
import { applyDataDirOverride, type DataDirOptionLike } from "./config/data-dir.js";
import { loadPaperclipEnvFile } from "./config/env.js";
import { initTelemetryFromConfigFile, flushTelemetry } from "./telemetry.js";
import { registerWorktreeCommands } from "./commands/worktree.js";
import { registerPluginCommands } from "./commands/client/plugin.js";
import { registerClientAuthCommands } from "./commands/client/auth.js";
import { cliVersion } from "./version.js";
const program = new Command();
const DATA_DIR_OPTION_HELP =
@@ -23,7 +32,7 @@ const DATA_DIR_OPTION_HELP =
program
.name("paperclipai")
.description("Paperclip CLI — setup, diagnose, and configure your instance")
.version("0.2.0");
.version(cliVersion);
program.hook("preAction", (_thisCommand, actionCommand) => {
const options = actionCommand.optsWithGlobals() as DataDirOptionLike;
@@ -32,6 +41,8 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
hasConfigOption: optionNames.has("config"),
hasContextOption: optionNames.has("context"),
});
loadPaperclipEnvFile(options.config);
initTelemetryFromConfigFile(options.config);
});
program
@@ -39,7 +50,8 @@ program
.description("Interactive first-run setup wizard")
.option("-c, --config <path>", "Path to config file")
.option("-d, --data-dir <path>", DATA_DIR_OPTION_HELP)
.option("-y, --yes", "Accept defaults (quickstart + start immediately)", false)
.option("--bind <mode>", "Quickstart reachability preset (loopback, lan, tailnet)")
.option("-y, --yes", "Accept quickstart defaults (trusted local loopback unless --bind is set) and start immediately", false)
.option("--run", "Start Paperclip immediately after saving config", false)
.action(onboard);
@@ -70,6 +82,19 @@ program
.option("-s, --section <section>", "Section to configure (llm, database, logging, server, storage, secrets)")
.action(configure);
program
.command("db:backup")
.description("Create a one-off database backup using current config")
.option("-c, --config <path>", "Path to config file")
.option("-d, --data-dir <path>", DATA_DIR_OPTION_HELP)
.option("--dir <path>", "Backup output directory (overrides config)")
.option("--retention-days <days>", "Retention window used for pruning", (value) => Number(value))
.option("--filename-prefix <prefix>", "Backup filename prefix", "paperclip")
.option("--json", "Print backup metadata as JSON")
.action(async (opts) => {
await dbBackupCommand(opts);
});
program
.command("allowed-hostname")
.description("Allow a hostname for authenticated/private mode access")
@@ -84,6 +109,7 @@ program
.option("-c, --config <path>", "Path to config file")
.option("-d, --data-dir <path>", DATA_DIR_OPTION_HELP)
.option("-i, --instance <id>", "Local instance id (default: default)")
.option("--bind <mode>", "On first run, use onboarding reachability preset (loopback, lan, tailnet)")
.option("--repair", "Attempt automatic repairs during doctor", true)
.option("--no-repair", "Disable automatic repairs during doctor")
.action(runCommand);
@@ -118,6 +144,10 @@ registerAgentCommands(program);
registerApprovalCommands(program);
registerActivityCommands(program);
registerDashboardCommands(program);
registerRoutineCommands(program);
registerFeedbackCommands(program);
registerWorktreeCommands(program);
registerPluginCommands(program);
const auth = program.command("auth").description("Authentication and bootstrap utilities");
@@ -131,7 +161,22 @@ auth
.option("--base-url <url>", "Public base URL used to print invite link")
.action(bootstrapCeoInvite);
program.parseAsync().catch((err) => {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
});
registerClientAuthCommands(auth);
async function main(): Promise<void> {
let failed = false;
try {
await program.parseAsync();
} catch (err) {
failed = true;
console.error(err instanceof Error ? err.message : String(err));
} finally {
await flushTelemetry();
}
if (failed) {
process.exit(1);
}
}
void main();

View File

@@ -1,9 +1,26 @@
import * as p from "@clack/prompts";
import type { DatabaseConfig } from "../config/schema.js";
import { resolveDefaultEmbeddedPostgresDir, resolvePaperclipInstanceId } from "../config/home.js";
import {
resolveDefaultBackupDir,
resolveDefaultEmbeddedPostgresDir,
resolvePaperclipInstanceId,
} from "../config/home.js";
export async function promptDatabase(): Promise<DatabaseConfig> {
const defaultEmbeddedDir = resolveDefaultEmbeddedPostgresDir(resolvePaperclipInstanceId());
export async function promptDatabase(current?: DatabaseConfig): Promise<DatabaseConfig> {
const instanceId = resolvePaperclipInstanceId();
const defaultEmbeddedDir = resolveDefaultEmbeddedPostgresDir(instanceId);
const defaultBackupDir = resolveDefaultBackupDir(instanceId);
const base: DatabaseConfig = current ?? {
mode: "embedded-postgres",
embeddedPostgresDataDir: defaultEmbeddedDir,
embeddedPostgresPort: 54329,
backup: {
enabled: true,
intervalMinutes: 60,
retentionDays: 30,
dir: defaultBackupDir,
},
};
const mode = await p.select({
message: "Database mode",
@@ -11,6 +28,7 @@ export async function promptDatabase(): Promise<DatabaseConfig> {
{ value: "embedded-postgres" as const, label: "Embedded PostgreSQL (managed locally)", hint: "recommended" },
{ value: "postgres" as const, label: "PostgreSQL (external server)" },
],
initialValue: base.mode,
});
if (p.isCancel(mode)) {
@@ -18,9 +36,14 @@ export async function promptDatabase(): Promise<DatabaseConfig> {
process.exit(0);
}
let connectionString: string | undefined = base.connectionString;
let embeddedPostgresDataDir = base.embeddedPostgresDataDir || defaultEmbeddedDir;
let embeddedPostgresPort = base.embeddedPostgresPort || 54329;
if (mode === "postgres") {
const connectionString = await p.text({
const value = await p.text({
message: "PostgreSQL connection string",
defaultValue: base.connectionString ?? "",
placeholder: "postgres://user:pass@localhost:5432/paperclip",
validate: (val) => {
if (!val) return "Connection string is required for PostgreSQL mode";
@@ -28,48 +51,107 @@ export async function promptDatabase(): Promise<DatabaseConfig> {
},
});
if (p.isCancel(connectionString)) {
if (p.isCancel(value)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
return {
mode: "postgres",
connectionString,
embeddedPostgresDataDir: defaultEmbeddedDir,
embeddedPostgresPort: 54329,
};
connectionString = value;
} else {
const dataDir = await p.text({
message: "Embedded PostgreSQL data directory",
defaultValue: base.embeddedPostgresDataDir || defaultEmbeddedDir,
placeholder: defaultEmbeddedDir,
});
if (p.isCancel(dataDir)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
embeddedPostgresDataDir = dataDir || defaultEmbeddedDir;
const portValue = await p.text({
message: "Embedded PostgreSQL port",
defaultValue: String(base.embeddedPostgresPort || 54329),
placeholder: "54329",
validate: (val) => {
const n = Number(val);
if (!Number.isInteger(n) || n < 1 || n > 65535) return "Port must be an integer between 1 and 65535";
},
});
if (p.isCancel(portValue)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
embeddedPostgresPort = Number(portValue || "54329");
connectionString = undefined;
}
const embeddedPostgresDataDir = await p.text({
message: "Embedded PostgreSQL data directory",
defaultValue: defaultEmbeddedDir,
placeholder: defaultEmbeddedDir,
const backupEnabled = await p.confirm({
message: "Enable automatic database backups?",
initialValue: base.backup.enabled,
});
if (p.isCancel(embeddedPostgresDataDir)) {
if (p.isCancel(backupEnabled)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
const embeddedPostgresPort = await p.text({
message: "Embedded PostgreSQL port",
defaultValue: "54329",
placeholder: "54329",
const backupDirInput = await p.text({
message: "Backup directory",
defaultValue: base.backup.dir || defaultBackupDir,
placeholder: defaultBackupDir,
validate: (val) => (!val || val.trim().length === 0 ? "Backup directory is required" : undefined),
});
if (p.isCancel(backupDirInput)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
const backupIntervalInput = await p.text({
message: "Backup interval (minutes)",
defaultValue: String(base.backup.intervalMinutes || 60),
placeholder: "60",
validate: (val) => {
const n = Number(val);
if (!Number.isInteger(n) || n < 1 || n > 65535) return "Port must be an integer between 1 and 65535";
if (!Number.isInteger(n) || n < 1) return "Interval must be a positive integer";
if (n > 10080) return "Interval must be 10080 minutes (7 days) or less";
return undefined;
},
});
if (p.isCancel(backupIntervalInput)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
if (p.isCancel(embeddedPostgresPort)) {
const backupRetentionInput = await p.text({
message: "Backup retention (days)",
defaultValue: String(base.backup.retentionDays || 30),
placeholder: "30",
validate: (val) => {
const n = Number(val);
if (!Number.isInteger(n) || n < 1) return "Retention must be a positive integer";
if (n > 3650) return "Retention must be 3650 days or less";
return undefined;
},
});
if (p.isCancel(backupRetentionInput)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
return {
mode: "embedded-postgres",
embeddedPostgresDataDir: embeddedPostgresDataDir || defaultEmbeddedDir,
embeddedPostgresPort: Number(embeddedPostgresPort || "54329"),
mode,
connectionString,
embeddedPostgresDataDir,
embeddedPostgresPort,
backup: {
enabled: backupEnabled,
intervalMinutes: Number(backupIntervalInput || "60"),
retentionDays: Number(backupRetentionInput || "30"),
dir: backupDirInput || defaultBackupDir,
},
};
}

View File

@@ -1,6 +1,16 @@
import * as p from "@clack/prompts";
import { isLoopbackHost, type BindMode } from "@paperclipai/shared";
import type { AuthConfig, ServerConfig } from "../config/schema.js";
import { parseHostnameCsv } from "../config/hostnames.js";
import { buildCustomServerConfig, buildPresetServerConfig, inferConfiguredBind } from "../config/server-bind.js";
const TAILNET_BIND_WARNING =
"No Tailscale address was detected during setup. The saved config will stay on loopback until Tailscale is available or PAPERCLIP_TAILNET_BIND_HOST is set.";
function cancelled(): never {
p.cancel("Setup cancelled.");
process.exit(0);
}
export async function promptServer(opts?: {
currentServer?: Partial<ServerConfig>;
@@ -8,69 +18,37 @@ export async function promptServer(opts?: {
}): Promise<{ server: ServerConfig; auth: AuthConfig }> {
const currentServer = opts?.currentServer;
const currentAuth = opts?.currentAuth;
const currentBind = inferConfiguredBind(currentServer);
const deploymentModeSelection = await p.select({
message: "Deployment mode",
const bindSelection = await p.select({
message: "Reachability",
options: [
{
value: "local_trusted",
label: "Local trusted",
hint: "Easiest for local setup (no login, localhost-only)",
value: "loopback" as const,
label: "Trusted local",
hint: "Recommended for first run: localhost only, no login friction",
},
{
value: "authenticated",
label: "Authenticated",
hint: "Login required; use for private network or public hosting",
value: "lan" as const,
label: "Private network",
hint: "Broad private bind for LAN, VPN, or legacy --tailscale-auth style access",
},
{
value: "tailnet" as const,
label: "Tailnet",
hint: "Private authenticated access using the machine's detected Tailscale address",
},
{
value: "custom" as const,
label: "Custom",
hint: "Choose exact auth mode, exposure, and host manually",
},
],
initialValue: currentServer?.deploymentMode ?? "local_trusted",
initialValue: currentBind,
});
if (p.isCancel(deploymentModeSelection)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
const deploymentMode = deploymentModeSelection as ServerConfig["deploymentMode"];
let exposure: ServerConfig["exposure"] = "private";
if (deploymentMode === "authenticated") {
const exposureSelection = await p.select({
message: "Exposure profile",
options: [
{
value: "private",
label: "Private network",
hint: "Private access (for example Tailscale), lower setup friction",
},
{
value: "public",
label: "Public internet",
hint: "Internet-facing deployment with stricter requirements",
},
],
initialValue: currentServer?.exposure ?? "private",
});
if (p.isCancel(exposureSelection)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
exposure = exposureSelection as ServerConfig["exposure"];
}
const hostDefault = deploymentMode === "local_trusted" ? "127.0.0.1" : "0.0.0.0";
const hostStr = await p.text({
message: "Bind host",
defaultValue: currentServer?.host ?? hostDefault,
placeholder: hostDefault,
validate: (val) => {
if (!val.trim()) return "Host is required";
},
});
if (p.isCancel(hostStr)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
if (p.isCancel(bindSelection)) cancelled();
const bind = bindSelection as BindMode;
const portStr = await p.text({
message: "Server port",
@@ -84,15 +62,113 @@ export async function promptServer(opts?: {
},
});
if (p.isCancel(portStr)) {
p.cancel("Setup cancelled.");
process.exit(0);
if (p.isCancel(portStr)) cancelled();
const port = Number(portStr) || 3100;
const serveUi = currentServer?.serveUi ?? true;
if (bind === "loopback") {
return buildPresetServerConfig("loopback", {
port,
allowedHostnames: [],
serveUi,
});
}
if (bind === "lan" || bind === "tailnet") {
const allowedHostnamesInput = await p.text({
message: "Allowed private hostnames (comma-separated, optional)",
defaultValue: (currentServer?.allowedHostnames ?? []).join(", "),
placeholder:
bind === "tailnet"
? "your-machine.tailnet.ts.net"
: "dotta-macbook-pro, host.docker.internal",
validate: (val) => {
try {
parseHostnameCsv(val);
return;
} catch (err) {
return err instanceof Error ? err.message : "Invalid hostname list";
}
},
});
if (p.isCancel(allowedHostnamesInput)) cancelled();
const preset = buildPresetServerConfig(bind, {
port,
allowedHostnames: parseHostnameCsv(allowedHostnamesInput),
serveUi,
});
if (bind === "tailnet" && isLoopbackHost(preset.server.host)) {
p.log.warn(TAILNET_BIND_WARNING);
}
return preset;
}
const deploymentModeSelection = await p.select({
message: "Auth mode",
options: [
{
value: "local_trusted",
label: "Local trusted",
hint: "No login required; only safe with loopback-only or similarly trusted access",
},
{
value: "authenticated",
label: "Authenticated",
hint: "Login required; supports both private-network and public deployments",
},
],
initialValue: currentServer?.deploymentMode ?? "authenticated",
});
if (p.isCancel(deploymentModeSelection)) cancelled();
const deploymentMode = deploymentModeSelection as ServerConfig["deploymentMode"];
let exposure: ServerConfig["exposure"] = "private";
if (deploymentMode === "authenticated") {
const exposureSelection = await p.select({
message: "Exposure profile",
options: [
{
value: "private",
label: "Private network",
hint: "Private access only, with automatic URL handling",
},
{
value: "public",
label: "Public internet",
hint: "Internet-facing deployment with explicit public URL requirements",
},
],
initialValue: currentServer?.exposure ?? "private",
});
if (p.isCancel(exposureSelection)) cancelled();
exposure = exposureSelection as ServerConfig["exposure"];
}
const defaultHost =
currentServer?.customBindHost ??
currentServer?.host ??
(deploymentMode === "local_trusted" ? "127.0.0.1" : "0.0.0.0");
const host = await p.text({
message: "Bind host",
defaultValue: defaultHost,
placeholder: defaultHost,
validate: (val) => {
if (!val.trim()) return "Host is required";
if (deploymentMode === "local_trusted" && !isLoopbackHost(val.trim())) {
return "Local trusted mode requires a loopback host such as 127.0.0.1";
}
},
});
if (p.isCancel(host)) cancelled();
let allowedHostnames: string[] = [];
if (deploymentMode === "authenticated" && exposure === "private") {
const allowedHostnamesInput = await p.text({
message: "Allowed hostnames (comma-separated, optional)",
message: "Allowed private hostnames (comma-separated, optional)",
defaultValue: (currentServer?.allowedHostnames ?? []).join(", "),
placeholder: "dotta-macbook-pro, your-host.tailnet.ts.net",
validate: (val) => {
@@ -105,15 +181,11 @@ export async function promptServer(opts?: {
},
});
if (p.isCancel(allowedHostnamesInput)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
if (p.isCancel(allowedHostnamesInput)) cancelled();
allowedHostnames = parseHostnameCsv(allowedHostnamesInput);
}
const port = Number(portStr) || 3100;
let auth: AuthConfig = { baseUrlMode: "auto" };
let publicBaseUrl: string | undefined;
if (deploymentMode === "authenticated" && exposure === "public") {
const urlInput = await p.text({
message: "Public base URL",
@@ -133,24 +205,17 @@ export async function promptServer(opts?: {
}
},
});
if (p.isCancel(urlInput)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
auth = {
baseUrlMode: "explicit",
publicBaseUrl: urlInput.trim().replace(/\/+$/, ""),
};
} else if (currentAuth?.baseUrlMode === "explicit" && currentAuth.publicBaseUrl) {
auth = {
baseUrlMode: "explicit",
publicBaseUrl: currentAuth.publicBaseUrl,
};
if (p.isCancel(urlInput)) cancelled();
publicBaseUrl = urlInput.trim().replace(/\/+$/, "");
}
return {
server: { deploymentMode, exposure, host: hostStr.trim(), port, allowedHostnames, serveUi: true },
auth,
};
return buildCustomServerConfig({
deploymentMode,
exposure,
host: host.trim(),
port,
allowedHostnames,
serveUi,
publicBaseUrl,
});
}

49
cli/src/telemetry.ts Normal file
View File

@@ -0,0 +1,49 @@
import path from "node:path";
import {
TelemetryClient,
resolveTelemetryConfig,
loadOrCreateState,
trackInstallStarted,
trackInstallCompleted,
trackCompanyImported,
} from "../../packages/shared/src/telemetry/index.js";
import { resolvePaperclipInstanceRoot } from "./config/home.js";
import { readConfig } from "./config/store.js";
import { cliVersion } from "./version.js";
let client: TelemetryClient | null = null;
export function initTelemetry(fileConfig?: { enabled?: boolean }): TelemetryClient | null {
if (client) return client;
const config = resolveTelemetryConfig(fileConfig);
if (!config.enabled) return null;
const stateDir = path.join(resolvePaperclipInstanceRoot(), "telemetry");
client = new TelemetryClient(config, () => loadOrCreateState(stateDir, cliVersion), cliVersion);
return client;
}
export function initTelemetryFromConfigFile(configPath?: string): TelemetryClient | null {
try {
return initTelemetry(readConfig(configPath)?.telemetry);
} catch {
return initTelemetry();
}
}
export function getTelemetryClient(): TelemetryClient | null {
return client;
}
export async function flushTelemetry(): Promise<void> {
if (client) {
await client.flush();
}
}
export {
trackInstallStarted,
trackInstallCompleted,
trackCompanyImported,
};

10
cli/src/version.ts Normal file
View File

@@ -0,0 +1,10 @@
import { createRequire } from "node:module";
type PackageJson = {
version?: string;
};
const require = createRequire(import.meta.url);
const pkg = require("../package.json") as PackageJson;
export const cliVersion = pkg.version ?? "0.0.0";

View File

@@ -1,8 +1,8 @@
{
"extends": "../tsconfig.json",
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
"rootDir": ".."
},
"include": ["src"]
"include": ["src", "../packages/shared/src"]
}

View File

@@ -0,0 +1,115 @@
# Agent Companies Spec Inventory
This document indexes every part of the Paperclip codebase that touches the [Agent Companies Specification](docs/companies/companies-spec.md) (`agentcompanies/v1-draft`).
Use it when you need to:
1. **Update the spec** — know which implementation code must change in lockstep.
2. **Change code that involves the spec** — find all related files quickly.
3. **Keep things aligned** — audit whether implementation matches the spec.
---
## 1. Specification & Design Documents
| File | Role |
|---|---|
| `docs/companies/companies-spec.md` | **Normative spec** — defines the markdown-first package format (COMPANY.md, TEAM.md, AGENTS.md, PROJECT.md, TASK.md, SKILL.md), reserved files, frontmatter schemas, and vendor extension conventions (`.paperclip.yaml`). |
| `doc/plans/2026-03-13-company-import-export-v2.md` | Implementation plan for the markdown-first package model cutover — phases, API changes, UI plan, and rollout strategy. |
| `doc/SPEC-implementation.md` | V1 implementation contract; references the portability system and `.paperclip.yaml` sidecar format. |
| `docs/specs/cliphub-plan.md` | Earlier blueprint bundle plan; partially superseded by the markdown-first spec (noted in the v2 plan). |
| `doc/plans/2026-02-16-module-system.md` | Module system plan; JSON-only company template sections superseded by the markdown-first model. |
| `doc/plans/2026-03-14-skills-ui-product-plan.md` | Skills UI plan; references portable skill files and `.paperclip.yaml`. |
| `doc/plans/2026-03-14-adapter-skill-sync-rollout.md` | Adapter skill sync rollout; companion to the v2 import/export plan. |
## 2. Shared Types & Validators
These define the contract between server, CLI, and UI.
| File | What it defines |
|---|---|
| `packages/shared/src/types/company-portability.ts` | TypeScript interfaces: `CompanyPortabilityManifest`, `CompanyPortabilityFileEntry`, `CompanyPortabilityEnvInput`, export/import/preview request and result types, manifest entry types for agents, skills, projects, issues, recurring routines, companies. |
| `packages/shared/src/validators/company-portability.ts` | Zod schemas for all portability request/response shapes — used by both server routes and CLI. |
| `packages/shared/src/types/index.ts` | Re-exports portability types. |
| `packages/shared/src/validators/index.ts` | Re-exports portability validators. |
## 3. Server — Services
| File | Responsibility |
|---|---|
| `server/src/services/company-portability.ts` | **Core portability service.** Export (manifest generation, markdown file emission, `.paperclip.yaml` sidecars), import (graph resolution, collision handling, entity creation), preview (planned-action summary). Handles skill key derivation, recurring task <-> routine mapping, legacy recurrence migration, and package README generation. References `agentcompanies/v1` version string. |
| `server/src/services/routines.ts` | Paperclip routine runtime service. Portability now exports routines as recurring `TASK.md` entries and imports recurring tasks back through this service. |
| `server/src/services/company-export-readme.ts` | Generates `README.md` and Mermaid org-chart for exported company packages. |
| `server/src/services/index.ts` | Re-exports `companyPortabilityService`. |
## 4. Server — Routes
| File | Endpoints |
|---|---|
| `server/src/routes/companies.ts` | `POST /api/companies/:companyId/export` — legacy export bundle<br>`POST /api/companies/:companyId/exports/preview` — export preview<br>`POST /api/companies/:companyId/exports` — export package<br>`POST /api/companies/import/preview` — import preview<br>`POST /api/companies/import` — perform import |
Route registration lives in `server/src/app.ts` via `companyRoutes(db, storage)`.
## 5. Server — Tests
| File | Coverage |
|---|---|
| `server/src/__tests__/company-portability.test.ts` | Unit tests for the portability service (export, import, preview, manifest shape, `agentcompanies/v1` version). |
| `server/src/__tests__/company-portability-routes.test.ts` | Integration tests for the portability HTTP endpoints. |
## 6. CLI
| File | Commands |
|---|---|
| `cli/src/commands/client/company.ts` | `company export` — exports a company package to disk (flags: `--out`, `--include`, `--projects`, `--issues`, `--projectIssues`).<br>`company import <fromPathOrUrl>` — imports a company package from a file or folder (flags: positional source path/URL or GitHub shorthand, `--include`, `--target`, `--companyId`, `--newCompanyName`, `--agents`, `--collision`, `--ref`, `--dryRun`).<br>Reads/writes portable file entries and handles `.paperclip.yaml` filtering. |
## 7. UI — Pages
| File | Role |
|---|---|
| `ui/src/pages/CompanyExport.tsx` | Export UI: preview, manifest display, file tree visualization, ZIP archive creation and download. Filters `.paperclip.yaml` based on selection. Shows manifest and README in editor. |
| `ui/src/pages/CompanyImport.tsx` | Import UI: source input (upload/folder/GitHub URL/generic URL), ZIP reading, preview pane with dependency tree, entity selection checkboxes, trust/licensing warnings, secrets requirements, collision strategy, adapter config. |
## 8. UI — Components
| File | Role |
|---|---|
| `ui/src/components/PackageFileTree.tsx` | Reusable file tree component for both import and export. Builds tree from `CompanyPortabilityFileEntry` items, parses frontmatter, shows action indicators (create/update/skip), and maps frontmatter field labels. |
## 9. UI — Libraries
| File | Role |
|---|---|
| `ui/src/lib/portable-files.ts` | Helpers for portable file entries: `getPortableFileText`, `getPortableFileDataUrl`, `getPortableFileContentType`, `isPortableImageFile`. |
| `ui/src/lib/zip.ts` | ZIP archive creation (`createZipArchive`) and reading (`readZipArchive`) — implements ZIP format from scratch for company packages. CRC32, DOS date/time encoding. |
| `ui/src/lib/zip.test.ts` | Tests for ZIP utilities; exercises round-trip with portability file entries and `.paperclip.yaml` content. |
## 10. UI — API Client
| File | Functions |
|---|---|
| `ui/src/api/companies.ts` | `companiesApi.exportBundle`, `companiesApi.exportPreview`, `companiesApi.exportPackage`, `companiesApi.importPreview`, `companiesApi.importBundle` — typed fetch wrappers for the portability endpoints. |
## 11. Skills & Agent Instructions
| File | Relevance |
|---|---|
| `skills/paperclip/references/company-skills.md` | Reference doc for company skill library workflow — install, inspect, update, assign. Skill packages are a subset of the agent companies spec. |
| `server/src/services/company-skills.ts` | Company skill management service — handles SKILL.md-based imports and company-level skill library. |
| `server/src/services/agent-instructions.ts` | Agent instructions service — resolves AGENTS.md paths for agent instruction loading. |
## 12. Quick Cross-Reference by Spec Concept
| Spec concept | Primary implementation files |
|---|---|
| `COMPANY.md` frontmatter & body | `company-portability.ts` (export emitter + import parser) |
| `AGENTS.md` frontmatter & body | `company-portability.ts`, `agent-instructions.ts` |
| `PROJECT.md` frontmatter & body | `company-portability.ts` |
| `TASK.md` frontmatter & body | `company-portability.ts` |
| `SKILL.md` packages | `company-portability.ts`, `company-skills.ts` |
| `.paperclip.yaml` vendor sidecar | `company-portability.ts`, `routines.ts`, `CompanyExport.tsx`, `company.ts` (CLI) |
| `manifest.json` | `company-portability.ts` (generation), shared types (schema) |
| ZIP package format | `zip.ts` (UI), `company.ts` (CLI file I/O) |
| Collision resolution | `company-portability.ts` (server), `CompanyImport.tsx` (UI) |
| Env/secrets declarations | shared types (`CompanyPortabilityEnvInput`), `CompanyImport.tsx` (UI) |
| README + org chart | `company-export-readme.ts` |

Some files were not shown because too many files have changed in this diff Show More