1439 lines
82 KiB
Markdown
1439 lines
82 KiB
Markdown
# Developing
|
||
|
||
This project can run fully in local dev without setting up PostgreSQL manually.
|
||
|
||
## Deployment Modes
|
||
|
||
For mode definitions and intended CLI behavior, see `doc/DEPLOYMENT-MODES.md`.
|
||
|
||
Current implementation status:
|
||
|
||
- canonical model: `local_trusted` and `authenticated` (with `private/public` exposure)
|
||
|
||
## Prerequisites
|
||
|
||
- Node.js 24.11+
|
||
- pnpm 9+
|
||
|
||
## Dependency Lockfile Policy
|
||
|
||
GitHub Actions owns `pnpm-lock.yaml`.
|
||
|
||
- Do not commit `pnpm-lock.yaml` in pull requests.
|
||
- Pull request CI validates dependency resolution when manifests change.
|
||
- Pushes to `master` regenerate `pnpm-lock.yaml` with `pnpm install --lockfile-only --no-frozen-lockfile`, commit it back if needed, and then run verification with `--frozen-lockfile`.
|
||
|
||
## Start Dev
|
||
|
||
From repo root:
|
||
|
||
```sh
|
||
pnpm install
|
||
pnpm dev
|
||
```
|
||
|
||
This starts:
|
||
|
||
- API server: `http://localhost:3100`
|
||
- UI: served by the API server in dev middleware mode (same origin as API)
|
||
|
||
`pnpm dev` runs the server in watch mode and restarts on changes from workspace packages (including adapter packages). Use `pnpm dev:once` to run without file watching.
|
||
|
||
`pnpm dev:once` auto-applies pending local migrations by default before starting the dev server.
|
||
|
||
`pnpm dev` and `pnpm dev:once` are now idempotent for the current repo and instance: if the matching Paperclip dev runner is already alive, Paperclip reports the existing process instead of starting a duplicate.
|
||
|
||
To run against a separate local state root, pass `--data-dir`. The dev runner
|
||
translates it to an isolated `PAPERCLIP_HOME` before migration checks or server
|
||
startup, so embedded PostgreSQL and other default instance state live under that
|
||
directory:
|
||
|
||
```sh
|
||
pnpm dev --data-dir ./tmp/paperclip-dev
|
||
```
|
||
|
||
Pass the same option to the service-management commands so they use the
|
||
isolated runtime-service registry:
|
||
|
||
```sh
|
||
pnpm dev:list --data-dir ./tmp/paperclip-dev
|
||
pnpm dev:stop --data-dir ./tmp/paperclip-dev
|
||
```
|
||
|
||
Issue execution may also use project execution workspace policies and workspace runtime services for per-project worktrees, preview servers, and managed dev commands. Configure those through the project workspace/runtime surfaces rather than starting long-running unmanaged processes when a task needs a reusable service.
|
||
|
||
### Mobile-friendly preview (`pnpm dev:mobile`)
|
||
|
||
The vite dev server serves an unbundled module graph. This is fast to reload on a local machine but too heavy for phones and tablets on slow links (airplane wifi, mobile data, distant tailnet peers). `pnpm dev:mobile` builds the UI once and serves the small production bundle on port `3101` via `vite preview`, proxying `/api` requests to the dev API on `3100`.
|
||
|
||
- `pnpm dev:mobile` — build the UI and start the preview server on `:3101`. Rebuild manually to pick up UI source changes.
|
||
- `pnpm dev:both` — run `pnpm dev` and `pnpm dev:mobile` together with prefixed output and shared signal handling.
|
||
|
||
The preview server binds `0.0.0.0` and accepts any Host, so a tailnet or LAN address (e.g. `http://<host>.ts.net:3101/`) works out of the box. The `/api` proxy sets `x-forwarded-host` and `x-forwarded-proto`, which the server's board mutation guard uses to trust the browser's Origin — mutations from `:3101` succeed against the API on `:3100` without further configuration. An HTTPS tunnel in front of the preview server (ngrok, tailscale funnel) is also supported: the tunnel's `x-forwarded-proto` header is preserved when set.
|
||
|
||
## Storybook
|
||
|
||
The board UI Storybook keeps stories and Storybook config under `ui/storybook/` so component review files stay out of the app source routes.
|
||
|
||
```sh
|
||
pnpm storybook
|
||
pnpm build-storybook
|
||
```
|
||
|
||
These run the `@paperclipai/ui` Storybook on port `6006` and build the static output to `ui/storybook-static/`.
|
||
|
||
Use **Chat & Comments → Issue Thread Interactions → Composer Questions Auto Advance**
|
||
to try the paged composer form. A single selection shows a brief checked-state animation before advancing to the
|
||
next question. Reduced-motion mode advances without animation.
|
||
Multi-select and custom answers wait for Next, and the final page waits for
|
||
Submit answers. The adjacent **Verified** story exercises the full flow.
|
||
|
||
The Storybook visual regression suite uses external PNG baselines instead of
|
||
committed screenshots:
|
||
|
||
```sh
|
||
pnpm test:storybook-visual
|
||
pnpm test:storybook-visual:update
|
||
```
|
||
|
||
`pnpm test:storybook-visual` downloads and verifies the baseline archive from
|
||
`tests/storybook-visual/baseline-manifest.json` before running Playwright.
|
||
Accepted visual changes should update the manifest metadata and publish a new
|
||
immutable archive with `pnpm storybook-visual:baseline pack` and
|
||
`pnpm storybook-visual:baseline upload`; do not commit generated PNG snapshots.
|
||
|
||
Known limitation: Storybook visual baselines are Linux/Ubuntu-only. The manifest
|
||
pins the capture environment to `ubuntu-24.04` and the Playwright suite uses
|
||
pixel-exact comparison, so local runs on macOS, Windows, or other non-matching
|
||
platforms can report false-positive diffs from font rasterization and subpixel
|
||
rendering. Use the `Storybook Visual` GitHub Actions workflow on `ubuntu-latest`
|
||
as the source of truth, or run locally in a matching Linux environment before
|
||
accepting or updating baselines.
|
||
|
||
PR visual checks are opt-in while the suite stabilizes. Add the
|
||
`storybook-visual` label to a PR, or run the `Storybook Visual` GitHub Actions
|
||
workflow manually, to produce downloadable Playwright report/test-result
|
||
artifacts. Normal PR visual runs use read-only repository permissions and do not
|
||
upload or mutate baseline objects.
|
||
|
||
### Publish a branch Storybook
|
||
|
||
CODEOWNERS can publish a repository branch through **Actions → Storybook Deploy →
|
||
Run workflow**. Keep the workflow branch on `master` and enter the source branch
|
||
in `branch`. The source branch does not need to contain the workflow. Leaving
|
||
`branch` empty publishes the selected workflow branch's dispatched commit.
|
||
|
||
```sh
|
||
gh workflow run storybook-deploy.yml --ref master -f branch=your-branch
|
||
```
|
||
|
||
The existing **Storybook Visual** workflow also offers a `deploy_preview` checkbox,
|
||
which publishes through the same workflow instead of running visual tests:
|
||
|
||
```sh
|
||
gh workflow run storybook-visual.yml --ref master -f deploy_preview=true -f branch=your-branch
|
||
```
|
||
|
||
Approve the `storybook-deploy` environment as a CODEOWNER. The workflow summary
|
||
links the **stable branch URL** and **this build**. The run also uploads a
|
||
`storybook-deployment-<run-id>-<attempt>` artifact containing
|
||
`storybook-deployment.md` with both links and the source commit. Different branches have
|
||
different URLs; publishing one never replaces another. Redeploying the same
|
||
branch updates its stable URL only after all files for the new build are uploaded.
|
||
Previous build links keep working. The branch entry preserves Storybook query
|
||
parameters and fragments when redirecting to the completed build.
|
||
|
||
Bookmark URLs use `storybook/branches/<branch>/`, for example
|
||
`https://d1p6rlowie26tp.cloudfront.net/storybook/branches/master/`.
|
||
Copy the **stable branch URL** from the run summary when saving a bookmark;
|
||
opening it redirects to the latest published build. Branch names preserve case.
|
||
Characters other than letters, digits, `_`, and `-` use `~HH` UTF-8 escapes, so
|
||
`feature/foo` becomes `feature~2Ffoo` and stays distinct from `feature-foo`.
|
||
Names ending in a hyphen and 16 lowercase hex digits escape that hyphen to
|
||
reserve the existing build directories. Very long names use a hash suffix.
|
||
Existing hashed branch URLs keep updating and remain valid. Build files remain
|
||
under `storybook/branches/<readable-branch>-<hash>/builds/<run-id>-<attempt>/`.
|
||
`deployment.json` in each build records its branch, source commit and URLs.
|
||
Builds run independently; publication is serialized per branch. Retained builds
|
||
are not automatically deleted and will accumulate until an operator prunes them.
|
||
|
||
Publishing requires both the original actor and the current rerunner to be
|
||
individual GitHub accounts named in `.github/CODEOWNERS` on the current default
|
||
branch. Comments, teams and email entries do not grant access. Authorization runs
|
||
before the build and again before deployment, including deployment-only reruns.
|
||
GitHub also requires a CODEOWNER environment approval, so editing authorization
|
||
code on a branch cannot grant AWS access without an authorized reviewer.
|
||
|
||
The build downloads the public source archive with no GitHub token permissions,
|
||
AWS credentials or repository secrets. Dependency caching and install lifecycle
|
||
scripts are disabled. The separate publisher uses GitHub OIDC to assume a role limited to
|
||
`storybook/branches/*`. It treats the build artifact as static files and runs only
|
||
the publisher from the workflow checkout. It cannot delete objects, change AWS
|
||
settings, or overwrite the runner dashboard. The Storybook site itself is public.
|
||
Pushes and PR events never publish it.
|
||
|
||
The existing S3 bucket and CloudFront distribution also serve runner reports in
|
||
separate prefixes. GitHub Pages and its dashboard workflow are independent.
|
||
See [Storybook deployment setup](STORYBOOK-DEPLOYMENT.md) for the environment,
|
||
repository variables, AWS policies and one-time operator setup.
|
||
|
||
GitHub requires a new dispatch workflow to exist on the default branch before
|
||
it becomes a manual entry point.
|
||
|
||
## UI Fonts And Screenshots
|
||
|
||
The board UI ships its own sans-serif webfont assets in `ui/public/fonts/`.
|
||
`ui/src/index.css` declares Inter v4.1 variable regular and italic faces and wires
|
||
the Tailwind `font-sans` token to those bundled files before system fallbacks.
|
||
Linux screenshot or Storybook capture jobs should not install host Inter packages
|
||
or inject external font CSS to make Paperclip text render correctly.
|
||
|
||
Font assets live in Vite's public directory so `pnpm --filter @paperclipai/ui build`
|
||
emits them under `ui/dist/fonts/`. The server package copies the same output into
|
||
`server/ui-dist/fonts/` through `scripts/prepare-server-ui-dist.sh`.
|
||
|
||
Inspect or stop the current repo's managed dev runner:
|
||
|
||
```sh
|
||
pnpm dev:list
|
||
pnpm dev:stop
|
||
```
|
||
|
||
`pnpm dev:once` now tracks backend-relevant file changes and pending migrations. When the current boot is stale, the board UI shows a `Restart required` banner. You can also enable guarded auto-restart in `Instance Settings > Experimental`, which waits for queued/running local agent runs to finish before restarting the dev server.
|
||
|
||
Worktree dependency provisioning records its fingerprint only after a successful
|
||
install. Frozen installs with outdated lockfiles or patched-dependency hash
|
||
mismatches retry once without `--frozen-lockfile`; other failures retain their
|
||
exit status. Patch contents are part of the install fingerprint. Generated
|
||
lockfile changes remain local to the worktree; the repository's lockfile bot
|
||
owns committed updates.
|
||
|
||
## Hot-Restart Deploys
|
||
|
||
Primary-instance rebuilds that restart `paperclip.service` can request one-shot live-run adoption instead of using the normal graceful shutdown drain. Before restarting the service, write the marker from the newly staged app with the current service PID:
|
||
|
||
```sh
|
||
old_main_pid="$(systemctl show paperclip.service -p MainPID --value)"
|
||
pnpm --filter @paperclipai/server exec tsx ../scripts/request-hot-restart.ts --server-pid "$old_main_pid"
|
||
systemctl restart paperclip.service
|
||
```
|
||
|
||
The staged command records the target server's boot identity and operating
|
||
system process start time with the PID. It reads process metadata through
|
||
`/proc` on Linux, `ps` on macOS and BSD, and PowerShell on Windows. These
|
||
identities let a later request reclaim an abandoned marker after the operating
|
||
system recycles the numeric PID. Older markers stay compatible and use process
|
||
start metadata when available. When OS metadata is unavailable, the current
|
||
server's health-reported boot time can still prove that a legacy marker predates
|
||
the process now using its PID. Paperclip refuses to create a new request without
|
||
at least one identity source. Supported-platform process probes fail explicitly
|
||
instead of silently treating a live PID as either the original owner or a
|
||
recycled process when identity cannot be established.
|
||
|
||
Use `--drain-required` only when the deploy intentionally requires the old terminate-and-retry behavior. Without that flag, the old server verifies that the marker targets its own PID, stops new scheduler work, waits for any queue-claim callback already in flight, snapshots currently running heartbeat run IDs and child PIDs, and skips the shutdown drain so eligible detached local-agent processes can keep running. ACP-backed local runs use server-owned stdio and cannot survive their parent server, so the old server instead persists their complete snapshot, changes the marker to `drainRequired` with `drainReason: "active_acp_run"`, and drains only those runs to bounded conversation retries. These retries resume the prior session when compatible, otherwise carry the full task conversation into a fresh session. They do not automatically replay tool calls or require receipts for every prior action. Detached CLI runs remain eligible for adoption during the same mixed restart. If an ACP process terminates but its terminal run update does not persist, startup classifies it as lost with reason `selective_drain_not_finalized` rather than treating the drain as successful. On startup the new server writes `$PAPERCLIP_HOME/instances/${PAPERCLIP_INSTANCE_ID:-default}/hot-restart-report.json` with `previousServerPid`, `newServerPid`, `previousServerVersion`, `newServerVersion`, `drainReason`, `adoptedRunIds`, `finalizedWhileDownRunIds`, `lostRunIds`, and per-run classifications before the normal orphan reaper runs.
|
||
|
||
When Paperclip manages embedded PostgreSQL, it suppresses that dependency's eager
|
||
`SIGINT`/`SIGTERM` cleanup hooks. Paperclip owns signal ordering so the heartbeat
|
||
snapshot and any required drain complete while the database is still available;
|
||
the coordinated shutdown path stops embedded PostgreSQL afterward.
|
||
|
||
The request command records the preflight set of running heartbeat IDs and writes
|
||
an instance-scoped marker plus a PID-targeted legacy home-root handoff marker.
|
||
This lets a previous server version capture its snapshot at the old path while
|
||
the new server correlates that snapshot back to the authoritative instance
|
||
request. If any preflight run ID is absent from the shutdown snapshot, the
|
||
startup report includes it in `lostRunIds`; a missing snapshot therefore cannot
|
||
look like a zero-loss restart.
|
||
|
||
A healthy guarded deploy must compare the report against `/api/health` (`version` or `serverVersion`) and treat any `lostRunIds` entry as a continuity failure that needs recovery before marking deployment complete.
|
||
|
||
### Recovering a deploy blocked by missing process metadata
|
||
|
||
If the currently installed version already has a running local-agent heartbeat
|
||
whose `processPid` and `processGroupId` are both null, that pre-fix run cannot be
|
||
made adoptable retroactively. Cross that version boundary once with the normal
|
||
drain-and-retry path:
|
||
|
||
```sh
|
||
old_main_pid="$(systemctl show paperclip.service -p MainPID --value)"
|
||
pnpm --filter @paperclipai/server exec tsx ../scripts/request-hot-restart.ts \
|
||
--server-pid "$old_main_pid" --drain-required
|
||
systemctl restart paperclip.service
|
||
```
|
||
|
||
After the fixed server starts, wait for the replacement `codex_local` heartbeat
|
||
to spawn, then confirm its run record has an identity (use an authenticated API
|
||
request in authenticated mode):
|
||
|
||
```sh
|
||
PAPERCLIP_API_BASE="${PAPERCLIP_API_URL:-http://127.0.0.1:3100}"
|
||
PAPERCLIP_API_BASE="${PAPERCLIP_API_BASE%/api}"
|
||
curl -fsS "$PAPERCLIP_API_BASE/api/heartbeat-runs/$RUN_ID" \
|
||
| jq -e '.status == "running" and (.processPid != null or .processGroupId != null)'
|
||
```
|
||
|
||
The next continuity check should use the ordinary marker without
|
||
`--drain-required`. After restart, require both an empty loss list and an
|
||
explicit outcome for the run that was live before restart:
|
||
|
||
```sh
|
||
jq -e --arg run "$RUN_ID" \
|
||
'(.lostRunIds | length) == 0 and ((.adoptedRunIds + .finalizedWhileDownRunIds) | index($run) != null)' \
|
||
"$PAPERCLIP_HOME/hot-restart-report.json"
|
||
```
|
||
|
||
An alive child appears in `adoptedRunIds`; a child that completed during the
|
||
restart window appears in `finalizedWhileDownRunIds`. Either is continuous. A
|
||
`lostRunIds` entry remains a failed deploy and must not be waived.
|
||
|
||
For a recovery from a version that can stop embedded PostgreSQL before writing
|
||
its shutdown snapshot, use `--drain-required` once to cross the broken boundary.
|
||
After the fixed server is live, perform another ordinary hot restart. Require
|
||
`lostRunIds` to be empty and every preflight run to appear in either
|
||
`adoptedRunIds` or `finalizedWhileDownRunIds`; an ACP-backed original should be
|
||
finalized and have a queued retry rather than be adopted.
|
||
|
||
Tailscale/private-auth dev mode:
|
||
|
||
```sh
|
||
pnpm dev --bind lan
|
||
```
|
||
|
||
This runs dev as `authenticated/private` with a private-network bind preset.
|
||
On a fresh authenticated/private instance, open the app, sign in or create an
|
||
account, and use the setup screen to claim the first instance admin from the
|
||
browser. The CLI fallback remains:
|
||
|
||
```sh
|
||
pnpm paperclipai auth bootstrap-ceo
|
||
```
|
||
|
||
For Tailscale-only reachability on a detected tailnet address:
|
||
|
||
```sh
|
||
pnpm dev --bind tailnet
|
||
```
|
||
|
||
Legacy aliases still map to the old broad private-network behavior:
|
||
|
||
```sh
|
||
pnpm dev --tailscale-auth
|
||
pnpm dev --authenticated-private
|
||
```
|
||
|
||
Allow additional private hostnames (for example custom Tailscale hostnames):
|
||
|
||
```sh
|
||
npx paperclipai allowed-hostname dotta-macbook-pro
|
||
```
|
||
|
||
## Test Commands
|
||
|
||
Use the cheap local default unless you are specifically working on browser flows:
|
||
|
||
```sh
|
||
pnpm test
|
||
```
|
||
|
||
`pnpm test` runs the Vitest suite only. For interactive Vitest watch mode use:
|
||
|
||
```sh
|
||
pnpm test:watch
|
||
```
|
||
|
||
Browser suites stay separate:
|
||
|
||
```sh
|
||
pnpm test:e2e
|
||
pnpm test:release-smoke
|
||
```
|
||
|
||
These browser suites are intended for targeted local verification and CI, not the default agent/human test command.
|
||
|
||
For normal issue work, start with the smallest targeted check that proves the change. Reserve repo-wide typecheck/build/test runs for PR-ready handoff or changes broad enough that narrow checks do not cover the risk.
|
||
|
||
### Task search evaluation
|
||
|
||
The task search relevance rubric and regression corpus are documented in
|
||
[SEARCH.md](SEARCH.md). Run the real PostgreSQL relevance suite with:
|
||
|
||
```sh
|
||
pnpm exec vitest run server/src/__tests__/task-search-quality.test.ts
|
||
```
|
||
|
||
Set `SEARCH_EVAL_SCALE=1` to additionally measure a disposable 10,000-task,
|
||
30,000-comment dataset. `SEARCH_EVAL_REPORT=/tmp/search-quality.json` saves
|
||
per-query results and latency measurements; scale measurements are opt-in.
|
||
|
||
### Recent task ordering
|
||
|
||
The streamlined sidebar keeps five recent tasks per company and account in browser
|
||
storage. It sorts by the newest observed task or comment activity, not by live-run
|
||
state. Older detail responses cannot move the stored activity time backward.
|
||
Activity-only reorderings wait for one second without further activity changes;
|
||
new and removed tasks appear immediately. Titles, status, and live indicators stay
|
||
current during that delay.
|
||
|
||
## One-Command Local Run
|
||
|
||
For a first-time local install, you can bootstrap and run in one command:
|
||
|
||
```sh
|
||
pnpm paperclipai run
|
||
```
|
||
|
||
> **Note: private npm registry `.npmrc` + first-run onboarding**
|
||
>
|
||
> The first-run experience often starts with `npx paperclipai onboard --yes` (before you have a repo checkout). If your global `~/.npmrc` sets `registry` to a private registry (for example GitHub Packages), `npx` may try to resolve `paperclipai` from that private registry and fail with `E404`.
|
||
>
|
||
> Diagnostic:
|
||
>
|
||
> ```sh
|
||
> npm config get registry
|
||
> ```
|
||
>
|
||
> Workaround (cross-platform; force the public npm registry for this command):
|
||
>
|
||
> ```sh
|
||
> npx --registry https://registry.npmjs.org paperclipai onboard --yes
|
||
> ```
|
||
|
||
`paperclipai run` does:
|
||
|
||
1. auto-onboard if config is missing
|
||
2. `paperclipai doctor` with repair enabled
|
||
3. starts the server when checks pass
|
||
|
||
### One-command isolated manual test drive
|
||
|
||
Use `test-drive` when you want to exercise the UI from a fresh checkout or SHA
|
||
without completing onboarding by hand:
|
||
|
||
```sh
|
||
ANTHROPIC_API_KEY=... node cli/node_modules/tsx/dist/cli.mjs cli/src/index.ts test-drive
|
||
OPENAI_API_KEY=... node cli/node_modules/tsx/dist/cli.mjs cli/src/index.ts test-drive --harness codex --model gpt-5.4
|
||
OPENROUTER_API_KEY=... node cli/node_modules/tsx/dist/cli.mjs cli/src/index.ts test-drive \
|
||
--harness opencode \
|
||
--model openrouter/anthropic/claude-sonnet-4.5
|
||
```
|
||
|
||
The command creates a trusted-loopback instance in a unique OS temporary data
|
||
directory, prints the absolute directory, runs onboarding and doctor
|
||
non-interactively, creates `Test Company` with a `CEO` agent, then opens the
|
||
browser. It runs in the foreground, does not install a background service, and
|
||
does not create a goal, project, issue, task, or first heartbeat. Temporary
|
||
directories are retained for inspection. Use `--data-dir <path>` to reuse one
|
||
or `--no-browser` to suppress browser opening. Reused directories must use the
|
||
embedded database: the command rejects database URL environment overrides and
|
||
configs with `database.mode: postgres`, suppresses the invocation directory's
|
||
`.env` when the server starts, and keeps the normal managed-service collision
|
||
guard. The selected instance's own environment file still loads. The command
|
||
selects the first available loopback port at or above `3100`.
|
||
|
||
Source-checkout startup builds the shared and plugin SDK packages when needed.
|
||
It prints build progress and any wait for another build. Interrupted builds
|
||
release their lock after the compiler stops; later startups recover locks whose
|
||
owner and compiler have exited. Empty locks from older versions are recovered
|
||
once they are at least two minutes old. The command remains in the foreground
|
||
after printing its ready URL to serve the instance; use Ctrl-C to stop it.
|
||
Each package gets a completion marker only after a successful build. A hard
|
||
kill leaves that marker absent, so the next startup rebuilds partial output.
|
||
The marker records source and output content fingerprints, so recovery does
|
||
not depend on filesystem timestamp precision. Direct `tsc` builds that produce
|
||
identical output reuse the marker. Changed or partial output is rebuilt once
|
||
before later startups reuse the completed build.
|
||
|
||
Claude uses `ANTHROPIC_API_KEY`; Codex uses `OPENAI_API_KEY`; OpenCode uses
|
||
`OPENROUTER_API_KEY` and requires an `openrouter/...` model. `--api-key-env`
|
||
can name a different source variable while the agent still receives the
|
||
canonical variable. `--api-key <value>` is also supported and is mutually
|
||
exclusive with `--api-key-env`; Paperclip redacts it from its own output, but
|
||
also removes it from the JavaScript argument view before telemetry, diagnostics,
|
||
or server startup. Wrappers, operating-system process listings, and shell
|
||
history may still expose argument values. This is an explicit local test-drive
|
||
tradeoff; use an environment-backed input when that exposure is not acceptable. In a
|
||
linked Git worktree the command sets worktree runtime mode and safely arms
|
||
**Run tasks in this worktree** for the current isolated instance. Primary
|
||
checkouts and non-Git directories leave that experimental setting unchanged.
|
||
|
||
If the selected data directory already contains any company, `test-drive`
|
||
preserves all companies, agents, and secrets and ignores the bootstrap flags.
|
||
The worktree execution setting is the only value it may reconcile in that
|
||
case.
|
||
|
||
## Docker Quickstart (No local Node install)
|
||
|
||
Build and run Paperclip in Docker:
|
||
|
||
```sh
|
||
docker build -t paperclip-local .
|
||
docker run --name paperclip \
|
||
-p 3100:3100 \
|
||
-e HOST=0.0.0.0 \
|
||
-e PAPERCLIP_HOME=/paperclip \
|
||
-v "$(pwd)/data/docker-paperclip:/paperclip" \
|
||
paperclip-local
|
||
```
|
||
|
||
Or use Compose:
|
||
|
||
```sh
|
||
docker compose -f docker/docker-compose.quickstart.yml up --build
|
||
```
|
||
|
||
See `doc/DOCKER.md` for API key wiring (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY`) and persistence details.
|
||
|
||
## Docker For Untrusted PR Review
|
||
|
||
For a separate review-oriented container that keeps `codex`/`claude` login state in Docker volumes and checks out PRs into an isolated scratch workspace, see `doc/UNTRUSTED-PR-REVIEW.md`.
|
||
|
||
## Local Instance Layout
|
||
|
||
Every local install keeps runtime state directly under the selected instance root:
|
||
|
||
```text
|
||
~/.paperclip/instances/default/ # instance root
|
||
config.json # runtime config
|
||
.env # instance env file
|
||
db/ # embedded PostgreSQL data
|
||
data/
|
||
storage/ # local_disk uploads
|
||
backups/ # automatic DB backups
|
||
logs/
|
||
runtime-services/ # managed local-service registry
|
||
runtime-service-logs/ # append-only managed-service stdout/stderr
|
||
secrets/master.key # local_encrypted master key
|
||
workspaces/<agent-id>/ # default agent workspaces
|
||
projects/ # project execution workspaces
|
||
companies/<company-id>/agents/<agent-id>/codex-home/
|
||
# per-agent codex_local home
|
||
```
|
||
|
||
`PAPERCLIP_HOME` and `PAPERCLIP_INSTANCE_ID` override the home root and instance id respectively. `paperclipai onboard` echoes the resolved values in its banner (`Local home: <home> | instance: <id> | config: <path>`) so you can confirm where state will land before continuing.
|
||
|
||
Config updates preserve unrecognized top-level and nested keys so provider or
|
||
plugin extensions survive `configure` and worktree port repair. Likely
|
||
misspellings of known keys produce a warning but are not removed. If an
|
||
existing `config.json` is malformed, `onboard` and `configure` first create a
|
||
byte-for-byte sibling backup named `config.json.invalid-1` (then `-2`, and so
|
||
on). Repair from defaults requires an interactive confirmation; non-interactive
|
||
runs stop without replacing the original.
|
||
|
||
## Database in Dev (Auto-Handled)
|
||
|
||
For local development, leave `DATABASE_URL` unset.
|
||
The server will automatically use embedded PostgreSQL and persist data at:
|
||
|
||
- `~/.paperclip/instances/default/db`
|
||
|
||
Override home or instance:
|
||
|
||
```sh
|
||
PAPERCLIP_HOME=/custom/path PAPERCLIP_INSTANCE_ID=dev pnpm paperclipai run
|
||
```
|
||
|
||
No Docker or external database is required for this mode.
|
||
|
||
## Storage in Dev (Auto-Handled)
|
||
|
||
For local development, the default storage provider is `local_disk`, which persists uploaded images/attachments at:
|
||
|
||
- `~/.paperclip/instances/default/data/storage`
|
||
|
||
Configure storage provider/settings:
|
||
|
||
```sh
|
||
pnpm paperclipai configure --section storage
|
||
```
|
||
|
||
## Agent Artifact Uploads
|
||
|
||
When an agent generates a file that a board user or reviewer should inspect as
|
||
a deliverable, attach it to the issue before marking the task complete. Do not
|
||
rely on a local workspace path as the only access path.
|
||
|
||
Use the helper bundled with the Paperclip skill from the repo root:
|
||
|
||
```sh
|
||
skills/paperclip/scripts/paperclip-upload-artifact.sh dist/demo.mp4 \
|
||
--title "Demo video render" \
|
||
--summary "MP4 render for board review"
|
||
```
|
||
|
||
For WebM output:
|
||
|
||
```sh
|
||
skills/paperclip/scripts/paperclip-upload-artifact.sh out/walkthrough.webm \
|
||
--title "Walkthrough video" \
|
||
--summary "WebM walkthrough render"
|
||
```
|
||
|
||
The helper uploads the file as an issue attachment, creates an artifact work
|
||
product by default, and prints markdown links for the final issue comment. See
|
||
`doc/AGENT-ARTIFACTS.md` for the full completion pattern and direct API shape.
|
||
If a file intentionally remains workspace-only, create a work product with
|
||
`metadata.resourceRef.kind: "workspace_file"` and include the workspace-relative
|
||
path in the final comment. Use browse/search only as the fallback for recovering
|
||
that file, not as the main completion path for deliverables.
|
||
|
||
## Default Agent Workspaces
|
||
|
||
When a local agent run has no resolved project/session workspace, Paperclip falls back to an agent home workspace under the instance root:
|
||
|
||
- `~/.paperclip/instances/default/workspaces/<agent-id>`
|
||
|
||
This path honors `PAPERCLIP_HOME` and `PAPERCLIP_INSTANCE_ID` in non-default setups.
|
||
|
||
For `codex_local`, Paperclip assigns new and updated agents an isolated Codex home under the instance root and blocks shared host/company Codex homes:
|
||
|
||
- `~/.paperclip/instances/default/companies/<company-id>/agents/<agent-id>/codex-home`
|
||
|
||
Paperclip also persists an empty `OPENAI_API_KEY` override for those agents so a host-level `OPENAI_API_KEY` cannot leak into Codex runs through process inheritance. If an operator explicitly configures `adapterConfig.env.CODEX_HOME`, it must not point at the shared company `codex-home`, `$CODEX_HOME`, or `~/.codex`.
|
||
|
||
If the `codex` CLI is not installed or not on `PATH`, `codex_local` agent runs fail at execution time with a clear adapter error. Quota polling uses a short-lived `codex app-server` subprocess: when `codex` cannot be spawned, that provider reports `ok: false` in aggregated quota results and the API server keeps running (it must not exit on a missing binary).
|
||
|
||
Local adapters require their corresponding CLI/session setup on the machine running Paperclip. External adapters are installed through the adapter/plugin flow and should not require hardcoded imports in `server/` or `ui/`.
|
||
|
||
## Config Freshness
|
||
|
||
Agent, project, environment, secret, skill, and workspace config edits are sampled at the next run boundary. A heartbeat that is already running finishes with the config it started with.
|
||
|
||
When effective run config changes, Paperclip may intentionally skip a saved adapter session, refresh persisted workspace runtime config, replace a reused execution workspace, or avoid reusing a sandbox/environment lease. Fresh execution can lose adapter-specific session, workspace, or sandbox state; correctness of the next run's config takes priority over continuity. Plain environment values affect freshness through value hashes; run result JSON and workspace operation logs expose only the non-sensitive freshness decision categories, without storing secret values, full env maps, provider credentials, or private path details.
|
||
|
||
## Workspace Git Scan Protection
|
||
|
||
Paperclip applies one process-wide scheduler to expensive host-side workspace Git enumeration, including changed-file browsing, runtime/finalization cleanliness guards, and adapter sandbox-sync snapshots. The scheduler defaults to two active scans and a bounded queue of 32. Identical scans of the same canonical worktree share one subprocess, while successful changed-file listings are cached for 10 seconds. Correctness-sensitive runtime guards bypass the result cache.
|
||
|
||
The cache intentionally trades up to a few seconds of changed-file freshness for stable server latency. The file browser retains an explicit refresh action, does not start its query while the panel or browser tab is hidden, and presents overloads as retryable failures rather than an empty workspace. A full queue returns `503` with code `workspace_git_scan_saturated`; a scan exceeding its wall-clock limit returns `504` with code `workspace_git_scan_timeout`. Both responses include `Retry-After: 1`.
|
||
|
||
Environment overrides:
|
||
|
||
- `PAPERCLIP_WORKSPACE_GIT_SCAN_CONCURRENCY` (default `2`, range `1`–`16`)
|
||
- `PAPERCLIP_WORKSPACE_GIT_SCAN_QUEUE_CAPACITY` (default `32`, range `0`–`1024`)
|
||
- `PAPERCLIP_WORKSPACE_GIT_SCAN_TIMEOUT_MS` (default `8000`, range `100`–`120000`)
|
||
- `PAPERCLIP_WORKSPACE_GIT_SCAN_CACHE_TTL_MS` (default `10000`, range `0`–`60000`)
|
||
|
||
Structured `workspace_git_scan` logs expose the operation name, a non-reversible workspace-path hash, queue and execution durations, active/queued counts, cache and single-flight use, and terminal outcome. Saturation and timeout warnings are rate-limited so an overload does not create a second logging storm.
|
||
|
||
## Worktree-local Instances
|
||
|
||
When developing from multiple git worktrees, do not point two Paperclip servers at the same embedded PostgreSQL data directory.
|
||
|
||
Instead, create a repo-local Paperclip config plus an isolated instance for the worktree:
|
||
|
||
```sh
|
||
paperclipai worktree init
|
||
# or create the git worktree and initialize it in one step:
|
||
npx paperclipai worktree:make paperclip-pr-432
|
||
```
|
||
|
||
This command:
|
||
|
||
- writes repo-local files at `.paperclip/config.json` and `.paperclip/.env`
|
||
- creates an isolated instance under `~/.paperclip-worktrees/instances/<worktree-id>/`
|
||
- when run inside a linked git worktree, mirrors the effective git hooks into that worktree's private git dir
|
||
- picks a free app port and embedded PostgreSQL port
|
||
- disables automatic database backups for the isolated instance
|
||
- by default seeds the isolated DB in `minimal` mode from the current effective Paperclip instance/config (repo-local worktree config when present, otherwise the default instance) via a logical SQL snapshot
|
||
|
||
Seed modes:
|
||
|
||
- `minimal` keeps core app state like companies, projects, issues, comments, approvals, and auth state, preserves schema for all tables, but omits row data from heavy operational history such as heartbeat runs, wake requests, activity logs, runtime services, and agent session state
|
||
- `full` makes a full logical clone of the source instance
|
||
- `--no-seed` creates an empty isolated instance
|
||
|
||
Seeded worktree instances quarantine copied live execution by default for both `minimal` and `full` seeds. During restore, Paperclip disables copied agent timer heartbeats, resets copied `running` agents to `idle`, blocks and unassigns copied agent-owned `in_progress` issues, and unassigns copied agent-owned `todo`/`in_review` issues. This keeps a freshly booted worktree from starting agents for work already owned by the source instance. Pass `--preserve-live-work` only when you intentionally want the isolated worktree to resume copied assignments.
|
||
|
||
The same quarantine stops copied project/execution-workspace runtime desired states and clears copied runtime process claims. Without this reset, booting the cloned Paperclip database could restart a source workspace's dev service from the isolated instance, creating duplicate runners, port reassignment, and stale public URLs.
|
||
|
||
After `worktree init`, both the server and the CLI auto-load the repo-local `.paperclip/.env` when run inside that worktree, so normal commands like `pnpm dev`, `paperclipai doctor`, and `paperclipai db:backup` stay scoped to the worktree instance.
|
||
|
||
`pnpm dev` now fails fast in a linked git worktree when `.paperclip/.env` is missing, instead of silently booting against the default instance/port. If that happens, run `paperclipai worktree init` in the worktree first.
|
||
|
||
### Lean worktrees and deferred seeding
|
||
|
||
Seeding a worktree database is the heaviest part of `worktree init`. That work can be deferred so a worktree is cheap to create and only pays the seed cost the first time it is actually used — the CLI/dev-time analog of the server's lazy runtime provisioning (see the board-operator guide's "Lazy runtime provisioning" section).
|
||
|
||
Seeding state is tracked in `.paperclip/seed-manifest.json`. The versioned manifest records only non-secret evidence: source instance id/config path, target instance id, seed mode, snapshot time, migration revision, attempt timestamps, current phase, terminal state, and a bounded phase diagnostic history. It never stores database credentials or auth credential material. The legacy `seed-pending` and `seed-complete` files remain read-only compatibility signals for worktrees created before the manifest shipped.
|
||
|
||
The default `worktree init` still seeds eagerly. A lean worktree (created without an eager seed) has a `pending` manifest until something seeds it on demand:
|
||
|
||
- `pnpm paperclipai worktree ensure-seeded` performs the deferred seed **exactly once**. It is lock-guarded and idempotent: only a complete `verified` manifest short-circuits it, so it is safe to call repeatedly and from concurrent processes. Managed workspaces derive the source from the control-plane-provided base project workspace when it carries its own `.paperclip/config.json`, and otherwise from the control plane's own registered instance config; either way the workspace's manifest never selects it. Manual worktrees must pass `--from-config`.
|
||
- `paperclipai run` calls `ensureWorktreeSeeded` automatically before doctor/boot. Managed runs transparently seed a lean worktree from their registered base workspace; an unmanaged lean worktree must first run `worktree ensure-seeded --from-config <source-config>`.
|
||
- Managed Paperclip git worktrees default to the repository's `scripts/provision-worktree.sh` when the strategy omits `provisionCommand`, so the isolated config and pending manifest cannot be silently skipped. Runtime startup also runs `scripts/provision-worktree-runtime.sh` automatically when no explicit runtime provision command is configured and the manifest is not verified. Explicitly configured provision commands still take precedence.
|
||
- The built-in deferred seed is recorded as its own terminal `workspace_seed` operation. A zero exit code is not enough for success: the operation succeeds only when `.paperclip/seed-manifest.json` contains complete verified evidence; failed, missing, or malformed manifests produce a failed operation with the seed phase in metadata.
|
||
- Worktrees created before lazy seeding shipped may have neither marker. Paperclip adopts them only after their configured database proves a compatible migration journal and the core Paperclip schema; otherwise managed startup creates a pending manifest and performs the normal verified seed. Manual markerless worktrees must provide `--from-config` so the source remains explicit.
|
||
|
||
Both `minimal` and `full` modes use the same terminal data-validation contract. Source validation accepts a migration journal that is a prefix of the checkout's journal and records the source revision in seed diagnostics; it rejects a source that is ahead of the checkout because that would require a downgrade. After restore, Paperclip applies pending migrations and requires the target journal to be current. Both validations also read an auth user with an instance administrator role, active company membership, and representative cloned company/issue pair. Authenticated instances additionally require that administrator to have a non-empty credential account. `local_trusted` instances accept the implicit local Board user without an account row because that mode intentionally has no human login flow. Restore, migrations, execution quarantine, routine pausing, workspace rebinding, and post-restore validation all run under the seed lock. An interruption leaves the exact active phase in terminal `failed` state; it cannot produce readiness evidence.
|
||
|
||
The seed process must own the target embedded PostgreSQL lifecycle for that entire sequence. It refuses to restore into a target postmaster that is already running, suppresses the embedded provider's process-global exit hooks, and stops its owned target only after validation or failure cleanup. A shutdown detected during restore is recorded as a target-database shutdown diagnostic rather than a generic restore failure.
|
||
|
||
The seed manifest never grants source-path authority. Its source path and instance are diagnostic assertions derived from the realpath-canonical registered source. Deferred seeding resolves registration independently before taking the seed lock; under that lock it replaces stale source diagnostics from the registered value and then revalidates the manifest before any backup, service stop, spawn, or database mutation. Missing registration, invalid registered paths, source/target identity collisions, target-instance mismatches, and company mismatches fail closed.
|
||
|
||
**Unverified-seed guard.** `pnpm dev` (the dev-runner) refuses to boot a worktree whose manifest is pending, running, failed, malformed, or missing required verification evidence and points you at the fix:
|
||
|
||
```
|
||
[paperclip] this worktree database is seed-pending. Run `pnpm paperclipai worktree ensure-seeded` before `pnpm dev`.
|
||
```
|
||
|
||
This guard (`isWorktreeSeedPending` in `server/src/dev-runner-worktree.ts`) prevents `pnpm dev` from starting the app against an empty or partially restored database — run `worktree ensure-seeded` once and re-run `pnpm dev`.
|
||
|
||
Provisioned git worktrees also pause seeded routines that still have enabled schedule triggers in the isolated worktree database by default. This prevents copied daily/cron routines from firing unexpectedly inside the new workspace instance during development without disabling webhook/API-only routines.
|
||
|
||
That repo-local env also sets:
|
||
|
||
- `PAPERCLIP_IN_WORKTREE=true`
|
||
- `PAPERCLIP_DB_BACKUP_ENABLED=false`
|
||
- `PAPERCLIP_WORKTREE_NAME=<worktree-name>`
|
||
- `PAPERCLIP_WORKTREE_COLOR=<hex-color>`
|
||
|
||
The server/UI use those values for worktree-specific branding such as the top banner and dynamically colored favicon.
|
||
Authenticated worktree servers also use the `PAPERCLIP_INSTANCE_ID` value to scope Better Auth cookie names.
|
||
Browser cookies are shared by host rather than port, so this prevents logging into one `127.0.0.1:<port>` worktree from replacing another worktree server's session cookie.
|
||
|
||
When Paperclip closes a server-managed git worktree, it also reclaims the isolated instance referenced by that worktree's repo-local `.paperclip/.env`. New server-managed worktrees use a collision-resistant instance id derived from the resolved absolute worktree path, and Paperclip persists the resulting instance root as execution-workspace ownership metadata. Cleanup requires the env pointer to match that persisted root, stops a running embedded PostgreSQL process, and then removes the instance directory. The deletion guard only accepts canonical instance paths below `PAPERCLIP_WORKTREES_DIR/instances/`; legacy or mismatched ownership, pointers to the default/live Paperclip home, and all other locations are logged and left untouched.
|
||
|
||
Print shell exports explicitly when needed:
|
||
|
||
```sh
|
||
paperclipai worktree env
|
||
# or:
|
||
eval "$(paperclipai worktree env)"
|
||
```
|
||
|
||
### Workspace login handoff and readiness
|
||
|
||
Opening a managed workspace board no longer depends on knowing which cloned password is current. `Open workspace` asks the main control plane for a short-lived, single-use ticket; the isolated workspace verifies it and creates its own instance-scoped Better Auth session.
|
||
|
||
- **Issue** — `POST /api/execution-workspaces/{id}/login-handoff` (board actors only). The ticket is bound to the caller's user id and email, the execution workspace id, the workspace's company id, the isolated instance id, the live runtime origin, a nonce, and a ~90 s expiry. Nothing in the request body influences that binding; only the landing path is caller-supplied and it is reduced to a same-origin path before signing.
|
||
- **Exchange** — `GET /api/auth/{workspace-handoff}/exchange?ticket=…` on the workspace itself, registered as a Better Auth plugin so session creation and cookie signing use Better Auth's own path. It verifies the signature, expiry, origin, instance, workspace, company, the cloned user's email, and an active membership **in that company**, records the nonce so a replay loses, and answers with an HTTP redirect — which is what keeps the ticket out of browser history. Request logs redact the `ticket` parameter.
|
||
- **Fallback** — direct email/password sign-in still works and the UI labels it accurately as *snapshot-local credentials*. A rejected ticket redirects to `/auth?workspaceHandoffError=<reason>` rather than failing opaquely.
|
||
|
||
Key material is derived, never shared. The control plane keeps a root secret (`PAPERCLIP_WORKSPACE_HANDOFF_SECRET`, or a domain-separated derivation from the instance's existing signing secret when that is unset) and injects only per-workspace values into the guest process:
|
||
|
||
| Variable | Purpose |
|
||
| --- | --- |
|
||
| `PAPERCLIP_WORKSPACE_HANDOFF_KEY` | Per-workspace ticket verification key. A guest cannot mint a ticket for a sibling workspace. |
|
||
| `PAPERCLIP_WORKSPACE_READINESS_TOKEN` | Bearer token the control plane presents to read this workspace's protected readiness. |
|
||
| `PAPERCLIP_EXECUTION_WORKSPACE_ID` | Execution workspace the guest was provisioned for, used for identity checks. |
|
||
| `PAPERCLIP_EXECUTION_WORKSPACE_COMPANY_ID` | Company whose board the guest represents. Scopes both the membership check and the readiness probes, so "some company in the clone is fine" cannot pass for the one being opened. |
|
||
|
||
Protected `/api/health` on a cloned workspace additionally carries a `workspace` block — `state`, `databaseReady`, `cloneDataReady`, `authHandoffReady`, `seedState`, `seedPhase`, `instanceId`, `executionWorkspaceId`, `failurePhase`. Public health stays redacted. Managed runtime start will not publish `running / healthy` unless that block agrees and names this exact instance and workspace, and runtime-service work products are refreshed from the live runtime row so a port change cannot leave a stale user-facing URL.
|
||
|
||
The workspace UI surfaces `Provisioning database`, `Validating clone`, `Ready`, `Degraded`, `Repairing`, and `Repair failed`, each with one safe action (open, start, repair, or read the log).
|
||
|
||
### Worktree CLI Reference
|
||
|
||
**`npx paperclipai worktree init [options]`** — Create repo-local config/env and an isolated instance for the current worktree.
|
||
|
||
| Option | Description |
|
||
|---|---|
|
||
| `--name <name>` | Display name used to derive the instance id |
|
||
| `--instance <id>` | Explicit isolated instance id |
|
||
| `--home <path>` | Home root for worktree instances (default: `~/.paperclip-worktrees`) |
|
||
| `--from-config <path>` | Source config.json to seed from |
|
||
| `--from-data-dir <path>` | Source PAPERCLIP_HOME used when deriving the source config |
|
||
| `--from-instance <id>` | Source instance id (default: `default`) |
|
||
| `--server-port <port>` | Preferred server port |
|
||
| `--db-port <port>` | Preferred embedded Postgres port |
|
||
| `--seed-mode <mode>` | Seed profile: `minimal` or `full` (default: `minimal`) |
|
||
| `--no-seed` | Skip database seeding from the source instance |
|
||
| `--force` | Replace existing repo-local config and isolated instance data |
|
||
|
||
Examples:
|
||
|
||
```sh
|
||
paperclipai worktree init --no-seed
|
||
paperclipai worktree init --seed-mode full
|
||
paperclipai worktree init --from-instance default
|
||
paperclipai worktree init --from-data-dir ~/.paperclip
|
||
paperclipai worktree init --force
|
||
```
|
||
|
||
Repair an already-created repo-managed worktree and reseed its isolated instance from the main default install. Point `--from-config` at the instance config:
|
||
|
||
```sh
|
||
cd /path/to/paperclip/.paperclip/worktrees/PAP-884-ai-commits-component
|
||
npx paperclipai worktree init --force --seed-mode minimal \
|
||
--name PAP-884-ai-commits-component \
|
||
--from-config ~/.paperclip/instances/default/config.json
|
||
```
|
||
|
||
That rewrites the worktree-local `.paperclip/config.json` + `.paperclip/.env`, recreates the isolated instance under `~/.paperclip-worktrees/instances/<worktree-id>/`, and preserves the git worktree contents themselves.
|
||
|
||
For an already-created worktree where you want the CLI to decide whether to rebuild missing worktree metadata or just reseed the isolated DB, use `worktree repair`.
|
||
|
||
**`npx paperclipai worktree repair [options]`** — Repair the current linked worktree by default, or create/repair a named linked worktree under `.paperclip/worktrees/` when `--branch` is provided. The command never targets the primary checkout unless you explicitly pass `--branch`.
|
||
|
||
| Option | Description |
|
||
|---|---|
|
||
| `--branch <name>` | Existing branch/worktree selector to repair, or a branch name to create under `.paperclip/worktrees` |
|
||
| `--home <path>` | Home root for worktree instances (default: `~/.paperclip-worktrees`) |
|
||
| `--from-config <path>` | Source config.json to seed from |
|
||
| `--from-data-dir <path>` | Source `PAPERCLIP_HOME` used when deriving the source config |
|
||
| `--from-instance <id>` | Source instance id when deriving the source config (default: `default`) |
|
||
| `--seed-mode <mode>` | Seed profile: `minimal` or `full` (default: `minimal`) |
|
||
| `--no-seed` | Repair metadata only when bootstrapping a missing worktree config |
|
||
| `--allow-live-target` | Override the guard that requires the target worktree DB to be stopped first |
|
||
|
||
Examples:
|
||
|
||
```sh
|
||
# From inside a linked worktree, rebuild missing .paperclip metadata and reseed it from the default instance.
|
||
cd /path/to/paperclip/.paperclip/worktrees/PAP-1132-assistant-ui-pap-1131-make-issues-comments-be-like-a-chat
|
||
pnpm paperclipai worktree repair
|
||
|
||
# From the primary checkout, create or repair a linked worktree for a branch under .paperclip/worktrees/.
|
||
# This command repairs the local checkout, so run the checked-out CLI through the direct-exec form.
|
||
cd /path/to/paperclip
|
||
node cli/node_modules/tsx/dist/cli.mjs cli/src/index.ts worktree repair --branch PAP-1132-assistant-ui-pap-1131-make-issues-comments-be-like-a-chat
|
||
```
|
||
|
||
For an already-created worktree where you want to keep the existing repo-local config/env and only overwrite the isolated database, use `worktree reseed` instead. Stop the target worktree's Paperclip server first so the command can replace the DB safely.
|
||
|
||
**`npx paperclipai worktree reseed [options]`** — Re-seed an existing worktree-local instance from another Paperclip instance or worktree while preserving the target worktree's current config, ports, and instance identity.
|
||
|
||
| Option | Description |
|
||
|---|---|
|
||
| `--from <worktree>` | Source worktree path, directory name, branch name, or `current` |
|
||
| `--to <worktree>` | Target worktree path, directory name, branch name, or `current` (defaults to `current`) |
|
||
| `--from-config <path>` | Source config.json to seed from |
|
||
| `--from-data-dir <path>` | Source `PAPERCLIP_HOME` used when deriving the source config |
|
||
| `--from-instance <id>` | Source instance id when deriving the source config |
|
||
| `--seed-mode <mode>` | Seed profile: `minimal` or `full` (default: `full`) |
|
||
| `--yes` | Skip the destructive confirmation prompt |
|
||
| `--allow-live-target` | Override the guard that requires the target worktree DB to be stopped first |
|
||
| `--backup-target` | Retain a recoverable full target-DB backup before reseeding |
|
||
|
||
Examples:
|
||
|
||
```sh
|
||
# From the main repo, reseed a worktree from the current default/master instance.
|
||
cd /path/to/paperclip
|
||
npx paperclipai worktree reseed \
|
||
--from current \
|
||
--to PAP-1132-assistant-ui-pap-1131-make-issues-comments-be-like-a-chat \
|
||
--seed-mode full \
|
||
--yes
|
||
|
||
# From inside a worktree, reseed it from the default instance config.
|
||
cd /path/to/paperclip/.paperclip/worktrees/PAP-1132-assistant-ui-pap-1131-make-issues-comments-be-like-a-chat
|
||
npx paperclipai worktree reseed \
|
||
--from-instance default \
|
||
--seed-mode full
|
||
```
|
||
|
||
Managed workspace repair uses this same verified full-reseed contract through `POST /api/execution-workspaces/:id/runtime-commands/repair`. The exclusive, audited operation stops managed services, writes a recoverable pre-repair database backup under the isolated instance's backup directory, performs the full seed/migration/quarantine/rebinding sequence, and restarts only after terminal manifest and service-health validation. It preserves the worktree filesystem. On failure, services remain stopped while the database backup, seed manifest, bounded phase diagnostics, and operation log are retained for inspection; repair never retries itself in a loop.
|
||
|
||
**`npx paperclipai worktree:make <name> [options]`** — Create `~/NAME` as a git worktree, then initialize an isolated Paperclip instance inside it. This combines `git worktree add` with `worktree init` in a single step.
|
||
|
||
| Option | Description |
|
||
|---|---|
|
||
| `--start-point <ref>` | Remote ref to base the new branch on (e.g. `origin/main`) |
|
||
| `--instance <id>` | Explicit isolated instance id |
|
||
| `--home <path>` | Home root for worktree instances (default: `~/.paperclip-worktrees`) |
|
||
| `--from-config <path>` | Source config.json to seed from |
|
||
| `--from-data-dir <path>` | Source PAPERCLIP_HOME used when deriving the source config |
|
||
| `--from-instance <id>` | Source instance id (default: `default`) |
|
||
| `--server-port <port>` | Preferred server port |
|
||
| `--db-port <port>` | Preferred embedded Postgres port |
|
||
| `--seed-mode <mode>` | Seed profile: `minimal` or `full` (default: `minimal`) |
|
||
| `--no-seed` | Skip database seeding from the source instance |
|
||
| `--force` | Replace existing repo-local config and isolated instance data |
|
||
|
||
Examples:
|
||
|
||
```sh
|
||
npx paperclipai worktree:make paperclip-pr-432
|
||
npx paperclipai worktree:make my-feature --start-point origin/main
|
||
npx paperclipai worktree:make experiment --no-seed
|
||
```
|
||
|
||
**`npx paperclipai worktree env [options]`** — Print shell exports for the current worktree-local Paperclip instance.
|
||
|
||
| Option | Description |
|
||
|---|---|
|
||
| `-c, --config <path>` | Path to config file |
|
||
| `--json` | Print JSON instead of shell exports |
|
||
|
||
Examples:
|
||
|
||
```sh
|
||
pnpm paperclipai worktree env
|
||
pnpm paperclipai worktree env --json
|
||
eval "$(npx paperclipai worktree env)"
|
||
```
|
||
|
||
For project execution worktrees, Paperclip can also run a project-defined provision command after it creates or reuses an isolated git worktree. Configure this on the project's execution workspace policy (`workspaceStrategy.provisionCommand`). The command runs inside the derived worktree and receives `PAPERCLIP_WORKSPACE_*`, `PAPERCLIP_PROJECT_ID`, `PAPERCLIP_AGENT_ID`, and `PAPERCLIP_ISSUE_*` environment variables so each repo can bootstrap itself however it wants.
|
||
|
||
An issue can pin its isolated worktree to an exact pre-existing branch instead of a template-derived one — the contract PR-preparation tasks use. Set the issue's `executionWorkspaceSettings` to `{ "mode": "isolated_workspace", "workspaceStrategy": { "type": "git_worktree", "existingBranch": "<branch>" } }`. The validator requires isolated mode plus a `git_worktree` strategy and rejects `branchTemplate` alongside `existingBranch`. At dispatch the runtime attaches (never creates, renames, fast-forwards, or resets) that branch: it reuses a registered worktree that already has the branch checked out (including legacy `.worktrees/` paths), otherwise it attaches the branch under the managed worktree parent. A missing branch, an occupied worktree path on another branch, or a non-worktree strategy fails closed with a `workspace_validation_failed` error instead of falling back to the shared checkout or a derived branch, and an inherited `reuse_existing` workspace binding on a different branch is ignored in favor of realizing the pinned branch.
|
||
|
||
Heavier setup that is only needed by a managed runtime service can use `workspaceStrategy.runtimeProvisionCommand`. Paperclip runs this command lazily before spawning the first service in a start batch, serializes concurrent provisioning for the same workspace, and records the attempt as `workspace_runtime_provision`. The command receives the same workspace environment as `provisionCommand` and should be idempotent because later service-start batches invoke it again.
|
||
|
||
Managed runtime control actions (`start`, `stop`, `restart`, and job `run`) are mutually exclusive per execution workspace. An overlapping control is rejected with `409 workspace_runtime_control_in_progress` instead of racing the active operation, and authorization is still checked first, so the conflict never widens who may control a workspace.
|
||
|
||
Local managed runtime services write stdout and stderr directly to append-only
|
||
files under the instance's `runtime-service-logs/` directory. The child inherits
|
||
the file descriptors rather than supervisor-owned pipes, so request-logging
|
||
servers remain responsive and adoptable when the control plane restarts.
|
||
|
||
Every managed control reaches a terminal operation state. Each one stamps the owning server process and pid on its `workspace_operations` row and heartbeats while it runs, and each one carries a wall-clock ceiling (30 minutes for lifecycle controls, 4 hours for workspace jobs) so a hung provider or listener fails the operation rather than leaving it active. When a start fails part-way, Paperclip tears the workspace's runtime services down through the ordinary stop path and records a stopped desired state, so the lane is retryable and a startup reconcile will not resurrect a service that never came up.
|
||
|
||
Recovery of stranded controls is bounded and cannot steal a live operation. A `running` control is only terminalized when its owning process is gone, when the owning request in this process no longer exists, or after 60 seconds without a heartbeat; the terminalizing write is a compare-and-swap on `updated_at`, so an owner that heartbeats concurrently keeps its operation. Recovery runs on server startup and before each managed control, appends reconciliation evidence to the workspace-operation log, and stays inside the requested workspace's scope.
|
||
|
||
Readiness probes and port allocation are bounded for the same reason. Each HTTP readiness probe is aborted after at most 5 seconds (never past the service's readiness budget), so a foreign listener that accepts a connection but never answers cannot park a start forever. Auto-allocated loopback ports are reserved in-process for the duration of a start and re-checked for a live owner, and a configured port already claimed by another in-flight start fails that start terminally — so two isolated workspaces asking for the same app/HMR pair either get distinct healthy ports or one fails cleanly and retryably.
|
||
|
||
Beyond that in-flight guard, `start`, `stop`, and `restart` also take a durable exclusivity lease on the execution workspace (`execution_workspace_runtime_leases`). The lease is keyed by execution workspace and owned by the controlling issue (or, when a run has no issue in scope, by the run or agent), so it survives across calls, heartbeats, and server processes. The owning issue can keep operating; a different issue or run is rejected with `409 workspace_runtime_lease_conflict` before any workspace operation is recorded and before any runtime service is touched. Board/operator actions bypass the lease entirely and never take the lane away from an agent.
|
||
|
||
Lease recovery is bounded and explicit. Another issue may reclaim the lane once the owner becomes ineligible — the owning issue reaches a terminal status, is hidden, or is deleted; the owning run reaches a terminal status — or once the lease's 30-minute TTL elapses without the owner touching it. Archiving the execution workspace releases the lease outright. Conflict responses carry the owning issue/run ids and the lease expiry so an operator can tell who holds the lane.
|
||
|
||
For Tailscale HTTPS exposure, readiness includes stable listener-ownership checks for every requested loopback port (the app and, when configured, its Vite HMR companion). Each listener must belong to the spawned managed process group; an unrelated listener that races onto either reserved port fails the start closed before the broker is asked to expose it.
|
||
|
||
Managed `paperclip-dev` worktree services enable `PAPERCLIP_UI_DEV_MIDDLEWARE=true` by default, so newly started worktrees hot-reload UI source changes. Managed HTTPS services use this default only when they publish the Paperclip Vite HMR companion listener, which is the default exposure configuration. A service or adapter can explicitly set the variable to `false` when it intentionally needs to exercise the built UI bundle.
|
||
|
||
In Vite middleware mode, Paperclip gives HMR a dedicated HTTP server bound to the managed runtime's loopback host. The browser still derives the HMR hostname from the public HTTPS page, and exposed runtimes use secure WebSockets, so listener containment does not break remote hot reload.
|
||
|
||
When a workspace service runs Paperclip for browser OAuth QA, configure its `expose.urlTemplate` with the canonical URL the browser can reach. Paperclip preserves explicit `PAPERCLIP_PUBLIC_URL` or `BETTER_AUTH_URL` settings; otherwise it uses a valid exposed HTTPS origin (or loopback HTTP) as the managed runtime fallback for Better Auth and `/api/tools/oauth/callback`. Internal service names such as `http://paperclip-dev:<port>` are rejected unless that hostname is genuinely the browser route. Use a unique origin per isolated worktree. See [Execution Workspaces And Runtime Services](../docs/guides/board-operator/execution-workspaces-and-runtime-services.md#browser-reachable-origins-for-oauth-qa) for configuration and verification.
|
||
|
||
## Paperclip Runner Adapter Conversion
|
||
|
||
The experimental Paperclip Runner offers native Codex, OpenCode, and **ACPX
|
||
Claude**. Converting an existing Claude, Codex, or OpenCode agent selects its
|
||
corresponding provider, preserves compatible models, credentials, workspace,
|
||
and instructions, and resets execution sessions while retaining run history.
|
||
Other adapters require an explicit provider choice. Legacy ACPX Codex agent
|
||
settings normalize to native Codex on configuration updates and before fresh
|
||
runs; immutable run descriptors remain readable. The **Paperclip Runner**
|
||
experimental setting and company access checks still apply.
|
||
|
||
Agent configuration uses the same section layout across adapters: model and
|
||
provider belong to **Adapter**, environment variables have their own section,
|
||
and command/extra arguments are folded under **Configuration → Advanced**.
|
||
Lifecycle, timeout, and interrupt grace settings live under **Advanced Run
|
||
Policy**. Permission selectors with a single valid mode are hidden; a saved
|
||
unsupported mode still exposes remediation.
|
||
|
||
Model catalogs and refresh follow the selected provider. ACPX Claude uses the
|
||
normal Claude catalog and accepts custom model IDs; the exact ID is sent to
|
||
Claude, which can reject unavailable models. Package/version verification is
|
||
independent of model selection. Environment tests verify runtime installation;
|
||
a successful provider run additionally verifies credentials and model access.
|
||
|
||
ACPX Claude supports Linux x64 and macOS ARM64/x64 with pinned SDK executables.
|
||
On macOS the launcher uses private verified module/executable snapshots instead
|
||
of Linux `/proc` descriptors. Dependency isolation, process ownership, and
|
||
cancellation remain enforced; the snapshot is removed when the provider exits.
|
||
|
||
Native Codex is qualified only with `codexPermissionMode: "never"`. The create
|
||
and edit surfaces do not offer `on-request` or `untrusted`, and a persisted
|
||
unsupported value fails with remediation instead of being silently coerced.
|
||
OpenCode retains `allow`, `ask`, and `deny`; ACPX retains `approve-all`,
|
||
`approve-reads`, and `deny-all`. Codex conversion keeps a non-empty model and
|
||
otherwise stores the shared `gpt-5.6-sol` default. The native execution boundary
|
||
applies the same default to older runner rows whose model is missing or blank.
|
||
|
||
For native Codex runs, Paperclip passes the resolved execution workspace as
|
||
`PAPERCLIP_WORKSPACE_CWD` and uses it as the provider containment boundary. A
|
||
workspace below the host `HOME` is valid, including the default projectless
|
||
agent workspace. The host `HOME` itself, a directory that contains it, a
|
||
filesystem root, a `CODEX_HOME` overlap, or a canonical path outside the
|
||
assigned workspace is rejected before provider startup.
|
||
|
||
### Preinstalled remote runner runtime
|
||
|
||
For fast sandbox startup, bake `paperclip-runnerd` and the latest stable agent
|
||
CLIs into the sandbox image. Keep one version of each CLI shared by native and
|
||
local adapters; never retain an older global CLI beside a newer private copy.
|
||
Pin the resolved releases at image build time for reproducibility and refresh
|
||
the runner's qualification versions and binary digests together with those pins.
|
||
The ACP bridges remain separately qualified protocol dependencies.
|
||
|
||
Native discovery checks `/opt/paperclip-runner/bin`, then `$HOME/.local/bin`,
|
||
then PATH. Any preferred-directory entry must launch the same shared CLI that
|
||
normal adapters use. Discovery picks the first executable; it does not compare
|
||
versions across directories. With these artifacts preinstalled, startup links
|
||
and verifies them without uploading a binary or installing packages. Deploy
|
||
the updated sandbox image with the matching runner qualification changes.
|
||
|
||
### Native runner restart recovery
|
||
|
||
Paperclip Runner keeps its heartbeat run, native session, logical runner, and
|
||
provider session identities across server restarts. A coordinated hot restart
|
||
registers a correlated recovery request before it signals the dev supervisor.
|
||
An uncoordinated server restart uses the same durable recovery classifier
|
||
without trusting a handoff marker.
|
||
|
||
Startup binds the HTTP and PRP listener before it classifies native runs. Public
|
||
health reports a startup state until every candidate is reattached, dispatched
|
||
for same-run resume, finalized from durable evidence, or held for explicit
|
||
ownership evidence. Scheduling and generic orphan recovery start only after
|
||
that classification finishes.
|
||
|
||
- A verified live runner re-registers its existing PRP authority and reconnects
|
||
with the same operating-system PID. Paperclip does not spawn a competing
|
||
runner.
|
||
- A verified dead runner starts a replacement from the same durable root and
|
||
resumes the same provider checkpoint. Only the operating-system PID changes.
|
||
- A runner that died before its first authenticated connection can restart on
|
||
the same run only when its durable root proves that no provider authority or
|
||
checkpoint exists. Paperclip quarantines the incomplete root first.
|
||
- A live but mismatched or unverifiable process fails closed. Paperclip does not
|
||
signal it or spawn a replacement.
|
||
- A persisted proposed or terminal result is reconciled before any runner or
|
||
provider work starts, so restart recovery cannot submit a duplicate turn.
|
||
- On the next run, a completed local Codex session whose warm controller died
|
||
before suspension is recovered automatically, including a uniquely verified
|
||
checkpoint quarantined by older controllers. Paperclip requires matching
|
||
database/session/provider identities, a settled terminal journal, no pending
|
||
commands or active provider turn, and a confirmed-dead process and process
|
||
group. It seals the old authority for normal epoch rotation and preserves the
|
||
Codex thread and goal state. Empty retry directories do not prevent recovery;
|
||
conflicting histories, changed profiles, and live or unverifiable owners do.
|
||
|
||
Run the credential-free real-process restart suite with:
|
||
|
||
```sh
|
||
pnpm --filter @paperclipai/paperclip-runner build:runner-binaries
|
||
pnpm exec vitest run server/src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
|
||
pnpm --filter @paperclipai/paperclip-runner exec vitest run src/live/runnerd-codex-transport.test.ts -t 'adopts a live runner'
|
||
```
|
||
|
||
The suite uses isolated PostgreSQL state, isolated `PAPERCLIP_HOME` roots, real
|
||
`runnerd` processes, and a deterministic fake Codex app server. It covers hot
|
||
and hard restarts with live and dead runners, the result-finalization race,
|
||
incomplete bootstrap, repeated crashes with steering, and fail-closed process
|
||
identity mismatches.
|
||
|
||
## App-Shipped Skills Catalog
|
||
|
||
The Paperclip app ships a curated catalog of company skills out of the box. The
|
||
catalog is a workspace package at `packages/skills-catalog`:
|
||
|
||
```text
|
||
packages/skills-catalog/
|
||
catalog/
|
||
bundled/<category>/<slug>/SKILL.md # recommended defaults
|
||
optional/<category>/<slug>/SKILL.md # role/domain-specific
|
||
generated/catalog.json # checked-in manifest
|
||
scripts/
|
||
build-catalog-manifest.ts # regenerate generated/catalog.json
|
||
validate-catalog.ts # validation only
|
||
src/ # builder + types consumed by server/CLI
|
||
```
|
||
|
||
Server and CLI import the generated manifest; they do not crawl repository
|
||
paths at request time. Root `skills/` remains reserved for Paperclip runtime
|
||
skills and is not part of the catalog.
|
||
|
||
Skill-capable legacy local adapters always select the bundled
|
||
`paperclipai/paperclip/paperclip` operational skill when it is present in the
|
||
runtime inventory. This applies to existing agents without a stored skill
|
||
preference and to explicit empty optional-skill selections. The operational
|
||
skill supplies the control-plane workflow that those adapters need for
|
||
heartbeats. Other runtime skills remain controlled by
|
||
`paperclipSkillSync.desiredSkills`. The native `paperclip_runner` does not use
|
||
this legacy default because its protocol supplies the control-plane contract.
|
||
|
||
Validate the catalog without writing the manifest:
|
||
|
||
```sh
|
||
pnpm --filter @paperclipai/skills-catalog validate
|
||
```
|
||
|
||
Regenerate `generated/catalog.json` after editing any catalog `SKILL.md`,
|
||
frontmatter, file inventory, category, or slug:
|
||
|
||
```sh
|
||
pnpm --filter @paperclipai/skills-catalog build:manifest
|
||
```
|
||
|
||
The package's `build` script runs `build:manifest` and then `tsc`; tests live
|
||
under `pnpm --filter @paperclipai/skills-catalog test`. Validation fails when:
|
||
|
||
- a catalog entry is not under `catalog/bundled/<category>/<slug>` or
|
||
`catalog/optional/<category>/<slug>`
|
||
- `SKILL.md` is missing or the frontmatter `name`/`description` is empty
|
||
- the frontmatter `key` disagrees with the generated canonical key
|
||
- two catalog entries share an `id`, `key`, or `slug`
|
||
- file inventory contains absolute paths, `..`, broken symlinks, or files
|
||
outside the skill directory
|
||
- the regenerated manifest differs from the checked-in
|
||
`generated/catalog.json`
|
||
|
||
Trust level is derived from inventory: `markdown_only` (markdown + references
|
||
only), `assets` (other non-script files), or `scripts_executables` (any
|
||
executable script). The build contract is documented in
|
||
`doc/plans/2026-05-26-skills-cli-catalog-contract.md`.
|
||
|
||
CI runs `pnpm --filter @paperclipai/skills-catalog validate` and the package's
|
||
vitest suite, so always regenerate the manifest in the same commit as the
|
||
catalog change.
|
||
|
||
## App-Shipped Teams Catalog
|
||
|
||
The team catalog package mirrors the skills catalog workflow for
|
||
agentcompanies/v1 team packages:
|
||
|
||
```text
|
||
packages/teams-catalog/
|
||
catalog/
|
||
bundled/<category>/<slug>/TEAM.md
|
||
optional/<category>/<slug>/TEAM.md
|
||
generated/catalog.json
|
||
scripts/
|
||
build-catalog-manifest.ts
|
||
validate-catalog.ts
|
||
```
|
||
|
||
Validate without writing the manifest:
|
||
|
||
```sh
|
||
pnpm --filter @paperclipai/teams-catalog validate
|
||
```
|
||
|
||
Regenerate `generated/catalog.json` after editing catalog team files:
|
||
|
||
```sh
|
||
pnpm --filter @paperclipai/teams-catalog build:manifest
|
||
```
|
||
|
||
Team install/preview APIs enforce source policy. External skill sources require
|
||
explicit approval flags, and local-path skill sources are development-only
|
||
unless `allowLocalPathSources` is set by the caller.
|
||
|
||
## Quick Health Checks
|
||
|
||
In another terminal:
|
||
|
||
```sh
|
||
curl http://localhost:3100/api/health
|
||
curl http://localhost:3100/api/companies
|
||
```
|
||
|
||
Expected:
|
||
|
||
- `/api/health` returns `{"status":"ok"}`
|
||
- `/api/companies` returns a JSON array
|
||
|
||
## Reset Local Dev Database
|
||
|
||
To wipe local dev data and start fresh:
|
||
|
||
```sh
|
||
rm -rf ~/.paperclip/instances/default/db
|
||
pnpm dev
|
||
```
|
||
|
||
## Optional: Use External Postgres
|
||
|
||
If you set `DATABASE_URL`, the server will use that instead of embedded PostgreSQL.
|
||
|
||
## Automatic DB Backups
|
||
|
||
Paperclip can run automatic logical database backups on a timer. These backups cover
|
||
non-system database schemas, including migration history and plugin-owned database
|
||
schemas. Defaults:
|
||
|
||
- enabled
|
||
- every 60 minutes
|
||
- retain 30 days
|
||
- backup dir: `~/.paperclip/instances/default/data/backups`
|
||
|
||
Automatic backups are disabled for isolated worktree instances created with
|
||
`paperclipai worktree init` or `paperclipai worktree:make`. Existing worktree
|
||
configs are migrated to the disabled setting when their server next starts. The
|
||
main/default instance keeps the normal enabled-by-default behavior.
|
||
|
||
Configure these in:
|
||
|
||
```sh
|
||
pnpm paperclipai configure --section database
|
||
```
|
||
|
||
Run a one-off backup manually:
|
||
|
||
```sh
|
||
pnpm paperclipai db:backup
|
||
# or:
|
||
pnpm db:backup
|
||
```
|
||
|
||
Environment overrides:
|
||
|
||
- `PAPERCLIP_DB_BACKUP_ENABLED=true|false`
|
||
- `PAPERCLIP_DB_BACKUP_INTERVAL_MINUTES=<minutes>`
|
||
- `PAPERCLIP_DB_BACKUP_RETENTION_DAYS=<days>`
|
||
- `PAPERCLIP_DB_BACKUP_DIR=/absolute/or/~/path`
|
||
- `PAPERCLIP_DB_BACKUP_MAX_AGE_HOURS=<hours>` controls the `/api/health`
|
||
stale-backup warning threshold
|
||
- `PAPERCLIP_DB_BACKUP_ALERT_FILE=/path/to/failure-marker` lets external cron
|
||
wrappers surface the last failed backup in `/api/health`
|
||
- `PAPERCLIP_WORKSPACE_REAPER_COOLDOWN_DAYS=<days>` sets how long the
|
||
terminal-workspace reaper waits after an issue tree becomes terminal before it
|
||
archives the execution workspace and deletes the worktree. A person can reopen
|
||
the work inside this window. The default is `7`. A value of `0` disables the
|
||
cooldown and restores immediate reaping. A negative or non-numeric value falls
|
||
back to the default.
|
||
|
||
Without `PAPERCLIP_DB_BACKUP_ALERT_FILE`, health checks look for
|
||
`db-backup-to-s3.failure` in the backup directory, beside the backup directory,
|
||
and in the default sibling `health/` directory.
|
||
|
||
DB backups are not full instance filesystem backups. For full local disaster
|
||
recovery, also back up local storage files and the local encrypted secrets key if
|
||
those providers are enabled.
|
||
|
||
## Secrets in Dev
|
||
|
||
Agent env vars now support secret references. By default, secret values are stored with local encryption and only secret refs are persisted in agent config.
|
||
|
||
- Default local key path: `~/.paperclip/instances/default/secrets/master.key`
|
||
- Override key material directly: `PAPERCLIP_SECRETS_MASTER_KEY`
|
||
- Override key file path: `PAPERCLIP_SECRETS_MASTER_KEY_FILE`
|
||
- Back up the key file and database together; either one alone is not enough to restore local encrypted secrets.
|
||
|
||
Strict mode (recommended outside local trusted machines):
|
||
|
||
```sh
|
||
PAPERCLIP_SECRETS_STRICT_MODE=true
|
||
```
|
||
|
||
When strict mode is enabled, sensitive env keys (for example `*_API_KEY`, `*_TOKEN`, `*_SECRET`) must use secret references instead of inline plain values.
|
||
Authenticated deployments default strict mode on unless explicitly overridden.
|
||
|
||
CLI configuration support:
|
||
|
||
- `pnpm paperclipai onboard` writes a default `secrets` config section (`local_encrypted`, strict mode off, key file path set) and creates a local key file when needed.
|
||
- `pnpm paperclipai configure --section secrets` lets you update provider/strict mode/key path and creates the local key file when needed.
|
||
- `pnpm paperclipai doctor` validates secrets adapter configuration, can create a missing local key file with `--repair`, and reports missing AWS Secrets Manager bootstrap env when that provider is selected.
|
||
- Provider health is available at `GET /api/companies/:companyId/secret-providers/health` and reports local key permission warnings plus backup guidance.
|
||
|
||
Per-company provider vaults are configured in the board UI under
|
||
`Company Settings → Secrets → Provider vaults`, backed by
|
||
`/api/companies/{companyId}/secret-provider-configs`. The CLI does not own
|
||
vault lifecycle today. See `docs/deploy/secrets.md` (`Provider Vaults` section)
|
||
for the operator model.
|
||
|
||
Migration helper for existing inline env secrets:
|
||
|
||
```sh
|
||
pnpm secrets:migrate-inline-env # dry run
|
||
pnpm secrets:migrate-inline-env --apply # apply migration
|
||
```
|
||
|
||
## Internal Connection Token Brokers
|
||
|
||
Connection token exchanges carry a resolved parent credential, so their URLs
|
||
are public-only by default even in local/private deployments. To intentionally
|
||
use an internal broker, configure an exact hostname allowlist:
|
||
|
||
```sh
|
||
PAPERCLIP_TOKEN_BROKER_ALLOWED_HOSTS=broker.internal.example,10.0.0.42
|
||
```
|
||
|
||
Entries are comma- or whitespace-separated exact hostnames (no wildcards). The
|
||
host configured by `PAPERCLIP_PAGES_API_URL` is included automatically. Every
|
||
broker hostname is resolved once and the request is pinned to the approved
|
||
address; IPv4 and IPv6 link-local destinations remain denied even when their
|
||
host is allowlisted.
|
||
|
||
## HTTP Adapter Private Endpoints
|
||
|
||
HTTP adapters can call public HTTP(S) endpoints by default. Requests use the
|
||
same DNS-pinning guard as remote connections, do not follow redirects, and
|
||
reject loopback, RFC1918/private, and link-local or cloud-metadata destinations.
|
||
|
||
Server owners can opt a trusted private service in with a comma-separated list
|
||
of exact origins:
|
||
|
||
```sh
|
||
PAPERCLIP_HTTP_ADAPTER_PRIVATE_ENDPOINT_ALLOWLIST=http://hooks.internal.example:8080,https://10.0.0.42
|
||
```
|
||
|
||
Each entry must contain only a scheme, hostname, and optional port. Paths,
|
||
credentials, query strings, fragments, and wildcards are ignored. Matching is
|
||
by exact normalized origin, so allowing one port does not allow another.
|
||
Link-local destinations remain denied even when explicitly listed.
|
||
|
||
## Company Deletion Toggle
|
||
|
||
Company deletion is intended as a dev/debug capability and can be disabled at runtime:
|
||
|
||
```sh
|
||
PAPERCLIP_ENABLE_COMPANY_DELETION=false
|
||
```
|
||
|
||
Default behavior:
|
||
|
||
- `local_trusted`: enabled
|
||
- `authenticated`: disabled
|
||
|
||
## CLI Client Operations
|
||
|
||
Paperclip CLI now includes client-side control-plane commands in addition to setup commands.
|
||
|
||
Quick examples:
|
||
|
||
```sh
|
||
npx paperclipai issue list --company-id <company-id>
|
||
npx paperclipai issue create --company-id <company-id> --title "Investigate checkout conflict"
|
||
npx paperclipai issue update <issue-id> --status in_progress --comment "Started triage"
|
||
```
|
||
|
||
Set defaults once with context profiles:
|
||
|
||
```sh
|
||
npx paperclipai context set --api-base http://localhost:3100 --company-id <company-id>
|
||
```
|
||
|
||
Then run commands without repeating flags:
|
||
|
||
```sh
|
||
pnpm paperclipai issue list
|
||
pnpm paperclipai dashboard get
|
||
```
|
||
|
||
See full command reference in `doc/CLI.md`.
|
||
|
||
## Agent Invite Onboarding Endpoints
|
||
|
||
Agent-oriented invite onboarding now exposes machine-readable API docs:
|
||
|
||
The board UI generates agent onboarding prompts from the add-agent modal (`+` in the agent sidebar), so agent onboarding sits with the rest of agent creation rather than company member invite settings.
|
||
|
||
- `GET /api/invites/:token` returns invite summary plus onboarding and skills index links.
|
||
- `GET /api/invites/:token/onboarding` returns onboarding manifest details (registration endpoint, claim endpoint template, skill install hints).
|
||
- `GET /api/invites/:token/onboarding.txt` returns a plain-text onboarding doc intended for both human operators and agents (llm.txt-style handoff), including optional inviter message and suggested network host candidates.
|
||
- `GET /api/skills/index` lists available skill documents.
|
||
- `GET /api/skills/paperclip` returns the Paperclip heartbeat skill markdown.
|
||
|
||
Hermes gateway agents use this same generic agent invite flow with
|
||
`adapterType=hermes_gateway` and `agentDefaultsPayload.apiBaseUrl` /
|
||
`agentDefaultsPayload.apiKey`. See
|
||
[HERMES_GATEWAY_ONBOARDING.md](./HERMES_GATEWAY_ONBOARDING.md) for the full
|
||
operator path, including Hermes credentials, invite approval, key claim, and
|
||
fresh-state Docker smoke setup.
|
||
|
||
## OpenClaw Join Smoke Test
|
||
|
||
Run the end-to-end OpenClaw join smoke harness:
|
||
|
||
```sh
|
||
pnpm smoke:openclaw-join
|
||
```
|
||
|
||
What it validates:
|
||
|
||
- invite creation for agent-only join
|
||
- agent join request using `adapterType=openclaw_gateway`
|
||
- board approval + one-time API key claim semantics
|
||
- callback delivery on wakeup to a dockerized OpenClaw-style webhook receiver
|
||
|
||
Required permissions:
|
||
|
||
- This script performs board-governed actions (create invite, approve join, wakeup another agent).
|
||
- In authenticated mode, run with board auth via `PAPERCLIP_AUTH_HEADER` or `PAPERCLIP_COOKIE`.
|
||
|
||
Optional auth flags (for authenticated mode):
|
||
|
||
- `PAPERCLIP_AUTH_HEADER` (for example `Bearer ...`)
|
||
- `PAPERCLIP_COOKIE` (session cookie header value)
|
||
|
||
## PostHog MCP Live Smoke Test
|
||
|
||
The PostHog smoke targets an already-running authenticated Paperclip instance.
|
||
It is deliberately separate from `pnpm test` and `pnpm test:e2e` because it
|
||
uses a live vendor OAuth flow and creates a short-lived connection plus one
|
||
fresh-run proof issue.
|
||
|
||
```sh
|
||
INTEGRATIONS_POSTHOG_PAPERCLIP_E2E_EMAIL=operator@example.test \
|
||
INTEGRATIONS_POSTHOG_PAPERCLIP_DEV_LOGIN_PASSWORD='<environment-delivered>' \
|
||
INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID=483530 \
|
||
pnpm smoke:posthog-live https://paperclip.example.test
|
||
```
|
||
|
||
The command fails before browser launch unless all three integration bindings
|
||
are present, and never prints their values. A Paperclip heartbeat derives the
|
||
target origin from its injected `PAPERCLIP_API_URL`; the positional URL (or
|
||
`--base-url <url>`) selects the running instance for a manual invocation. It
|
||
creates no trace, video, or HAR, begins
|
||
screenshots only after OAuth returns to Paperclip, enables read actions only,
|
||
installs the connection on `CodexCoderPro` only, runs `project-get` with `{}`
|
||
from the board Test panel and a fresh agent run, then removes the connection.
|
||
Sanitized JSON and PNG evidence defaults to `PAPERCLIP_RUN_SCRATCH_DIR` when the
|
||
command runs in a heartbeat; set `POSTHOG_EVIDENCE_DIR` for a different output
|
||
directory.
|
||
|
||
Run the focused harness checks without contacting Paperclip or PostHog:
|
||
|
||
```sh
|
||
node --test scripts/smoke/posthog-live.test.mjs
|
||
```
|
||
|
||
## OpenClaw Docker UI One-Command Script
|
||
|
||
To boot OpenClaw in Docker and print a host-browser dashboard URL in one command:
|
||
|
||
```sh
|
||
pnpm smoke:openclaw-docker-ui
|
||
```
|
||
|
||
This script lives at `scripts/smoke/openclaw-docker-ui.sh` and automates clone/build/config/start for Compose-based local OpenClaw UI testing.
|
||
|
||
Pairing behavior for this smoke script:
|
||
|
||
- default `OPENCLAW_DISABLE_DEVICE_AUTH=1` (no Control UI pairing prompt for local smoke; no extra pairing env vars required)
|
||
- set `OPENCLAW_DISABLE_DEVICE_AUTH=0` to require standard device pairing
|
||
|
||
Model behavior for this smoke script:
|
||
|
||
- defaults to OpenAI models (`openai/gpt-5.2` + OpenAI fallback) so it does not require Anthropic auth by default
|
||
|
||
State behavior for this smoke script:
|
||
|
||
- defaults to isolated config dir `~/.openclaw-paperclip-smoke`
|
||
- resets smoke agent state each run by default (`OPENCLAW_RESET_STATE=1`) to avoid stale provider/auth drift
|
||
|
||
Networking behavior for this smoke script:
|
||
|
||
- auto-detects and prints a Paperclip host URL reachable from inside OpenClaw Docker
|
||
- default container-side host alias is `host.docker.internal` (override with `PAPERCLIP_HOST_FROM_CONTAINER` / `PAPERCLIP_HOST_PORT`)
|
||
- if Paperclip rejects container hostnames in authenticated/private mode, allow `host.docker.internal` via `npx paperclipai allowed-hostname host.docker.internal` and restart Paperclip
|
||
|
||
### GitHub identity for shared agents
|
||
|
||
See [execution GitHub identity](execution-github-identity.md) for the operation-time credential contract, continuation rules, runtime rollout, and acceptance-test requirements.
|
||
|
||
|
||
### Investigating polling load
|
||
|
||
The company heartbeat-run and live-run lists load secret registries in one
|
||
company-scoped query per response. Registry reads project only
|
||
`paperclipSecretRedactions` from the run context. They do not load the full
|
||
prompt/context JSON. Decrypted values live only for that request and each run
|
||
uses its own registry.
|
||
|
||
Hidden browser tabs suspend the company live-events connection and transcript
|
||
log reads. Returning to a visible tab refreshes active queries once and resumes
|
||
transcript reads from their retained offsets. A queued live-event invalidation
|
||
that flushes after the tab hides marks data stale without starting a refetch.
|
||
The developer-server health poll also stops in hidden tabs.
|
||
|
||
Workspace detail responses share concurrent Git inspections and reuse their
|
||
results for up to five seconds after completion. The cache holds at most 256
|
||
entries. Close-readiness checks, the terminal-workspace reaper, and the final
|
||
cleanup validation still inspect Git afresh. A display result never authorizes
|
||
worktree removal.
|
||
|
||
The connection-health sweep selects only due IDs in SQL before applying its
|
||
limit. Legacy `paperclip_plugin` placeholder connections are excluded: their
|
||
tools run in plugin workers and do not have remote MCP endpoints. These rows
|
||
remain available; the sweep does not disable or delete plugin connections.
|
||
|
||
When investigating an overloaded instance, distinguish request amplification
|
||
from stored configuration problems. Verify connection transport and endpoint
|
||
fields before disabling a connection. Verify workspace ownership, active runs,
|
||
Git state, and runtime-service readiness before closing a workspace. A missing
|
||
URL or old workspace timestamp alone does not prove that a row is disposable.
|