mirror of
https://github.com/paperclipai/paperclip-docs.git
synced 2026-11-07 06:16:07 -10:00
Release docs for paperclip v2026.720.0 (#67)
Ships the docs for **paperclip v2026.720.0** to `docs.paperclip.ing`. Window: `v2026.707.0...v2026.720.0` — 196 commits, 1,523 changed files, 0 truncated leaves. Most of this release was already drafted on `nightly` over the past cycle (Cases, Folders, Decision Training, Attention, Built-in Agents, company skill policy, inbox agent policy, `costStatus`, the ACP engine graduation and `acpx_local` retirement, and `PAPERCLIP_TOOL_ACTION_SIGNING_SECRET`). This PR adds the surfaces nightly had **not** yet covered, then stamps the release version across everything touched this cycle. ## New pages **`docs/reference/api/tool-gateway.md`** — the MCP Tool Gateway, the headline experimental feature. Covers applications and connections, catalog entries and risk levels, profiles/entries/bindings, the tool-access policy and its decision outcomes, named MCP gateways and their tokens, the audit feed, and the Smoke Lab. Documents the five `tools:*` permission keys and the viewer/membership rules on routes that don't use a permission key. Flags both experimental gates (`enableApps`, and `enableSmokeLab` separately). **`docs/reference/api/summary-slots.md`** — the built-in Summarizer and summary slots. Covers slot addressing (`scopeKind` / `scopeId` / `slotKey`), generation, revisions, and the `enableSummaries` gate. Cross-references Built-in Agents rather than duplicating it. ## Updated **`docs/guides/org/skills.md`** — Skill Studio (the three-pane authoring workspace, saved inputs, test runs and their unavailability reasons, run templates, version history), nested skill folders and the My Skills view, importing skills straight from a project, and company skill forks. Cross-references the existing Folders and Company Skill Policy API pages instead of restating them. ## Release stamping `paperclip_version: v2026.720.0` applied to all 26 pages touched this cycle — bumped on 10 that already had frontmatter, added to 13 that had none (including the API pages nightly authored, which were intentionally versionless until they merged to `main`). ## ↻ Renames detected `detect-renames.mjs` reports no directory renames. It flags one `removed_dirs_no_match`: - `packages/adapters/acpx-local` — **already reconciled on nightly.** The adapter was retired upstream (ACP became a capability of each harness adapter rather than a standalone adapter). `docs/reference/adapters/acpx-local.md` is now a retired stub that points at Claude Code / Codex / Gemini CLI and documents the automatic migration. No action needed. ## Screenshots — full overhaul recommended **248 of 298** registry entries are stale. This release landed a design-system convergence (single-source design tokens, retuned theme, broad Card/Badge adoption, a multiplicative radius ladder, unified list surfaces, Lucide task-status glyphs), so nearly every captured surface moved. This is an overhaul cycle, not a targeted refresh: ```sh npm run screenshots:refresh:all ``` Full per-entry breakdown in `SCREENSHOTS_PENDING.md`. Captures go to their own PR for review — nothing is auto-pushed. ## Verification - **Drift:** clean — 0 records against `903bd15` (8 parent paths, 57 CLI commands, 66 env vars, 389 REST routes, 18 permission keys, 4 roles). - **Claim verification:** `verify-edit.mjs` against `v2026.720.0` — **0 unverified, 0 suspicious** across all three authored files. - **Reconciliation:** no disappeared manifest entries. - **`npm run docs:build`:** passes — 169 markdown files, sitemap and robots generated. - **`npm run sync:check`:** links clean (172 files), nav clean, screenshots indexed, drift clean. - **Nav orphans:** `docs/reference/cli/commands.md` and `docs/reference/cli/control-plane-commands.md` — intentional redirect anchors, expected. `.sync-state.json` bumped to `base_release_tag: v2026.720.0` / `base_release_sha: 903bd157…`, which also repoints the drift checker's pinned ref (the previous `f12bb27…` no longer resolves upstream). No open `nightly-draft/*` PRs to fold in. ## Checklist - [ ] Tool Gateway page reads correctly for an operator enabling `enableApps` for the first time - [ ] Summary Slots experimental gating is accurate - [ ] Skill Studio guide matches what you see in the running UI - [ ] Decide whether to run the screenshot overhaul before or after merging this - [ ] **After squash-merging: run `node scripts/sync/realign-nightly.mjs release/v2026.720.0 --push` before the next nightly sync.** The squash severs `main`↔`nightly` ancestry; the realign (shipped in this PR) prevents add/add conflicts at the next nightly run. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: DevRel Gemini <[email protected]> Co-authored-by: Paperclip <[email protected]>
This commit is contained in:
35 files changed
+3477
-190
No files matched your search
+5
-5
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"branch_mode": "release",
|
||||
"base_release_tag": "v2026.707.0",
|
||||
"base_release_sha": "390627b46eb333309d357004384b220ecf8a65af",
|
||||
"last_seen_parent_sha": "555391fed77d3a10949b859dc3e84f108dc052d6",
|
||||
"last_applied_manifest_hash": "36a4f5526b5a93027a689d26f37bca8ca95307aea4b55e8410804d25a5cfbd9b",
|
||||
"last_run_at": "2026-07-08T09:09:48Z",
|
||||
"base_release_tag": "v2026.720.0",
|
||||
"base_release_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65",
|
||||
"last_seen_parent_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65",
|
||||
"last_applied_manifest_hash": "a9edb1817d88e801100732445407f1bc75539491e5a19e3d2bd113370009be12",
|
||||
"last_run_at": "2026-07-20T19:40:00Z",
|
||||
"last_run_outcome": "applied"
|
||||
}
|
||||
+75
-30
@@ -1,44 +1,89 @@
|
||||
# PENDING — release-mode docs sync
|
||||
# PENDING — nightly docs sync
|
||||
|
||||
Regenerated by `/sync-docs` (**release mode**, staged on `nightly` for review — not merged, not pushed). Cumulative window: parent release `v2026.626.0` (`4c6c0c6`) → `v2026.707.0` (`390627b`). 93 commits, 577 changed parent files, `truncated_leaves=0`.
|
||||
Regenerated by `/sync-docs` in **nightly mode**. This is the current cumulative view from the last parent release; it retains already-applied work for context and only treats the newly-settled delta as authoring scope.
|
||||
|
||||
This file is overwritten each run — it always reflects the current run, not an append log.
|
||||
Cumulative window: parent release `v2026.707.0` (`390627b`) → parent `master` HEAD `f12bb27`. The compare helper saw **195 commits / 1522 changed files** with **0 truncated leaves**. Every commit in the window predates the 24-hour quarantine cutoff (newest is `2026-07-18T19:39:37Z`), so nothing was held back this run.
|
||||
|
||||
Last run: 2026-07-08 · outcome: applied (staged on nightly)
|
||||
Newly settled since the prior run: `7947308…f12bb27` (**60 commits / 397 files**), headlined by four new REST surfaces and the summary-slot feature.
|
||||
|
||||
## ✅ Applied this run
|
||||
## ✅ Applied in earlier nightly drafts
|
||||
|
||||
- **The Work Timeline guide** · `docs/guides/day-to-day/work-timeline.md` (merged from PR #48)
|
||||
- Company-scoped Gantt-style timeline of when agents worked, handoffs, and overlap; boundary vs the Activity Log. Backed by parent `server/src/services/work-timeline.ts` + `packages/shared/src/types/work-timeline.ts` (release Highlight, #8938/#8880/#8875/#8923).
|
||||
- **Cases API** — `docs/reference/api/cases.md`.
|
||||
- **Built-in Agents API** — `docs/reference/api/built-in-agents.md`.
|
||||
- **Human permission keys** — `docs/administration/roles-and-permissions.md`.
|
||||
- **Codex `gpt-5.4-mini`** — `docs/reference/adapters/codex.md`.
|
||||
- **Decisions, ACP, inbox, and interaction contracts** — API, guide, adapter, and CLI pages updated in PR #57.
|
||||
- **Redundant issue re-wake throttling** (`5dff526`) — `docs/how-to/debug-stuck-heartbeat.md`.
|
||||
- **`costStatus` on cost events** (`efcce9c`) — `docs/reference/api/costs.md`.
|
||||
- **The six `tools:*` permission keys** — `docs/administration/roles-and-permissions.md`.
|
||||
- **`PAPERCLIP_TOOL_ACTION_SIGNING_SECRET`** — `docs/reference/deploy/environment-variables.md`.
|
||||
|
||||
- **Secret scopes and the responsible user** · `docs/administration/secret-scopes.md` (merged from PR #47)
|
||||
- Company vs user secret scope, responsible-user run attribution, and the pre-dispatch secret check. Backed by parent `user_secret_declarations`/`user_secret_definitions` schemas + `responsible-user-denial` (release Highlight, #8825).
|
||||
## ✍️ Newly settled, applied
|
||||
|
||||
- **Custom sandbox images** · `docs/guides/projects-workflow/custom-sandbox-images.md` (new)
|
||||
- Operator-facing guide for the "custom sandbox images with a built-in SSH terminal" Highlight (#8911/#8794/#8850): turning on the experimental **Enable Environments** gate, the build → browser-terminal prep → capture flow, runtime use of the active image, and Refresh/Rollback/Disable management. **Fulfils the operator-guide item deferred by the 2026-07-04 nightly run** (the plugin-SDK developer surface was already documented then). Every concrete claim traced to parent `server/src/services/environment-custom-images.ts`, `environment-custom-image-runtime.ts`, `realtime/environment-custom-image-terminal-ws.ts`, `ui/src/pages/CompanyEnvironments.tsx`, `packages/shared/src/constants.ts`, migrations `0125`/`0127`. Stamped `paperclip_version: v2026.707.0`.
|
||||
- **Folders API** — new page `docs/reference/api/folders.md`. `server/src/routes/folders.ts` mounts under `/api` and covers list, create, rename/update, move-item, move-folder, and delete. Documented against the real zod schemas; `MAX_FOLDER_DEPTH = 4` and the reserved root slugs (`bundled`, `my`, `projects`) are service constants, and `path`/`depth` are computed in `buildFolderViews` rather than stored columns. Verification: 7/7 claims confirmed.
|
||||
|
||||
- **Ramp optional skill** · `docs/reference/skills/optional/finance/ramp.md` (new) + `optional.md` + `content.json`
|
||||
- New optional catalog skill (release Improvement, #9157): wraps Ramp's official agent playbooks behind Paperclip's approval gates and host allowlist. New **Finance** category in the optional-skills nav. Metadata verified against parent `packages/skills-catalog/generated/catalog.json`.
|
||||
- **Decision Training API** — new page `docs/reference/api/decision-training.md`. Covers create, preview, list, the `export.jsonl` endpoint, get, update-notes, and delete. Source kinds are exactly `interaction`, `approval`, `execution_decision`. The retention policy constant `scrub_deleted_comments_v1` was confirmed directly in `packages/shared/src/types/decision-training.ts`. Verification: 7/7 claims confirmed.
|
||||
|
||||
## 🕒 Backlog — release Improvements not given dedicated pages
|
||||
- **Company Skill Policy API** — new page `docs/reference/api/company-skill-policy.md`. Get / put / delete / evaluate under `/api/companies/{companyId}/skill-policy`. Note the policy routes wrap zod failures themselves into `422 skill_policy_validation_failed` rather than the usual 400. Verification: 4/4 claims confirmed.
|
||||
|
||||
These v2026.707.0 changes are refinements to existing (or UI-only) surfaces; noted here rather than silently dropped:
|
||||
- **Inbox Agent Policy API** — new page `docs/reference/api/inbox-agent-policy.md`. Documents both the self routes (`/users/me/inbox-agent-policy`) and the admin-on-behalf routes (`/users/{userId}/inbox-agent-policy`). Verification: 4/4 claims confirmed.
|
||||
|
||||
- **Redesigned environment variables editor** (#8930) — single reusable editor replacing the legacy `EnvVarEditor`. UI refinement of existing env-var config; no new page, but drives screenshot staleness (see below).
|
||||
- **Pipeline automation** (#8787 title templates, #9090 health-warning dedup) — refinements to a **pre-existing, still-undocumented** pipelines feature. A first-class pipelines guide remains unwritten — carry forward as a standalone authoring task, not a v2026.707.0 delta.
|
||||
- **Starred sidebar resources** (#9085), **one-click recovery for diverged work** (#9136), **live heartbeat progress in run logs** (#8965), **live descendant status in inbox rows** (#8876) — UI polish; candidates for brief mentions in existing guides on a future pass.
|
||||
- **Gemini 3.1 Pro models** on `gemini-local` (#1602) — adapter model list; fold into `docs/reference/adapters/gemini-cli.md` next pass.
|
||||
- **`inbox:manage` permission key** (drift fix) — `docs/administration/roles-and-permissions.md`. `PERMISSION_KEYS` is now **twenty** keys; the count carrying a role default stays at **8**, so the explicit-only set goes from eleven to twelve. The important subtlety, confirmed in `server/src/services/authorization.ts`: `inbox:manage` is **not** checked by the inbox-agent-policy *routes* (those check `users:manage_permissions`). It gates an **agent acting on an inbox** — an agent may work its own responsible user's inbox without it, but cross-user inbox action needs an explicit scoped grant, and the scope must cover the target user. Agents on the low-trust review preset are denied it regardless of grant.
|
||||
|
||||
- **`summarize-status` bundled skill** — `docs/reference/skills.md`. New `paperclip-operations`-category bundled skill, `defaultInstall: false`, recommended for `general` and `manager`. Added to the bundled examples row and given a short paragraph.
|
||||
|
||||
## 🚧 Newly settled, surfaced but NOT applied
|
||||
|
||||
- **Summary slots** — `packages/db/src/schema/summary_slots.ts`, `server/src/routes/summary-slots.ts` (mounted `api.use(summarySlotRoutes(db))`), and `ui/src/api/summarySlots.ts`. This is the feature the new `summarize-status` skill writes into: a scope (project, workspaces overview, or a single project workspace) carries a summary slot that accumulates Markdown **revisions**. It deserves its own API page plus a short guide section on where the summary renders next to the board — not a one-line mention. Currently only referenced obliquely via the skill paragraph in `docs/reference/skills.md`.
|
||||
|
||||
- **The Tool access / MCP gateway / Smoke Lab REST surface (~60 endpoints)** — still outstanding from the prior run. `server/src/routes/tool-access.ts`, `tool-gateway.ts`, `smoke-lab.ts`. Groundwork retained:
|
||||
- **Mount prefixes differ, and this is a trap.** `toolAccessRoutes`, `toolGatewayRoutes`, and `smokeLabRoutes` mount on the `api` router (`app.use("/api", api)`), so they are `/api/...`. But `mcpGatewayProtocolRoutes(toolGateway)` mounts on `app` **directly** — so `GET|POST /mcp/gateways/{gatewayPublicId}` has **no** `/api` prefix.
|
||||
- `tool-gateway.ts` is **not** exported from `server/src/routes/index.ts`; it is imported straight into `app.ts`.
|
||||
- Vocabulary in `packages/shared/src/constants.ts`: `TOOL_APPLICATION_TYPES` (`mcp_http`, `mcp_stdio`, `paperclip_plugin`, `a2a`), `TOOL_APPLICATION_STATUSES` (`draft`, `active`, `disabled`, `archived`), `TOOL_CONNECTION_HEALTH_STATUSES`, `TOOL_MCP_GATEWAY_DEFAULT_PROFILE_MODES`.
|
||||
|
||||
- **Tools, Profiles, and Apps board UI** — carried over, plus this window's UI work. New end-user surfaces needing guide coverage and screenshots.
|
||||
|
||||
- **Decision Training UI** — `ui/src/components/DecisionTrainingDrawer.tsx` and `ui/src/api/decisionTraining.ts`. The API is now documented; the board-side drawer that produces the examples is not, and needs a guide page with screenshots.
|
||||
|
||||
- **Folders UI** — `ui/src/components/folders/*` (`SkillFolderTree`, `FolderControls`, `skill-folder-tree.ts`). The API is documented; the skill-library folder tree the user actually clicks is not.
|
||||
|
||||
## ✅ Reconcile — resolved this run
|
||||
|
||||
- **`acpx_local` retired to a live tombstone.** `docs/reference/adapters/acpx-local.md` has been rewritten as a stub: it states the adapter is retired, points at `claude-code.md` / `codex.md` / `gemini-cli.md`, documents the automatic `0136` migration table (`codex` → `codex_local`, everything else → `claude_local`, both with `engine: "acp"`), and explains what to do if the `acpx_local` string still shows up in a run error. The `content.json` entry is kept, retitled **ACPX Local (retired)** so it stays findable when browsing the adapter list. The page was **not** deleted and its body was **not** duplicated forward — the old config fields (`agent`, `effort`, `reasoningEffort`, `thinkingEffort`) are exactly what the migration rewrites away.
|
||||
|
||||
The investigation that produced this, retained because the helper gap is the reusable lesson:
|
||||
|
||||
`scripts/sync/detect-renames.mjs` reported `packages/adapters/acpx-local` under `removed_dirs_no_match`, which reads as a plain deletion. **That verdict is wrong.** Verified against parent `eedc7dd` ("Make ACP the default engine for local adapters", #9238):
|
||||
|
||||
1. **The runtime moved, it did not vanish.** `packages/adapters/acpx-local/` → `packages/adapter-utils/src/acpx-engine/` (module `@paperclipai/acpx-engine`), now imported by `claude-local`, `codex-local`, and `gemini-local`. ACP stopped being a separate *adapter choice* and became an *execution capability* of each harness adapter.
|
||||
2. **The `acpx_local` adapter type is still registered, deliberately.** `server/src/adapters/registry.ts:215` keeps an `acpxLocalAdapter` whose `execute` only emits a retirement message. Its own comment: *"Paperclip keeps this tombstone registered so stale `acpx_local` rows fail clearly instead of falling back to the process adapter."* The type is gone from `AGENT_ADAPTER_TYPES` (so it is unselectable for new agents) but still resolvable at runtime.
|
||||
3. **Existing rows were migrated.** `packages/db/src/migrations/0136_acpx_default_engine_migration.sql` rewrites `acpx_local` agents to `claude_local` or `codex_local` (branching on `adapter_config->>'agent'`) with `engine: 'acp'`, and consolidates the Codex reasoning-effort fields onto `modelReasoningEffort`.
|
||||
|
||||
Note also that `@paperclipai/acpx-engine` is an **internal library**, not a successor adapter — it exports `createAcpxEngineExecutor`, `execute`, `sessionCodec`, `printAcpxStreamEvent`, `parseAcpxStdoutLine`, and appears nowhere in `packages/shared/src/constants.ts`. There is no adapter type to select, so it gets no adapter page. (`grok_local`, the only type added alongside `acpx_local`'s removal, is unrelated: `packages/adapters/grok-local` already existed at the base tag and was simply registered late.)
|
||||
|
||||
**Helper gap — still open.** `detect-renames.mjs` matches top-level directories under watched roots, so it cannot see a move that crosses a package boundary into a nested path (`packages/adapters/<x>` → `packages/adapter-utils/src/<x>`). Any future cross-package extraction will be misreported the same way, and the failure mode points toward deleting docs that should be kept. Documented as a gotcha in [`maintenance/maintenance.md`](maintenance/maintenance.md#gotcha-detect-renamesmjs-cannot-see-cross-package-moves) with the verification commands. A code fix is still worth doing: match on file-content similarity, or at minimum scan for the removed dir's basename appearing as a new nested path elsewhere in the window.
|
||||
|
||||
## ↻ Renames detected
|
||||
|
||||
None. `detect-renames.mjs` returned `renames: []` and `added_dirs_genuinely_new: []` for this window.
|
||||
|
||||
## ⚠ Verification
|
||||
|
||||
All six applied edits passed `verify-edit.mjs` against parent `master` (`f12bb27`) with **zero** unverified and **zero** suspicious records. `roles-and-permissions.md` extracts 0 machine-checkable claims (the helper does not parse permission-key tables), so its facts were confirmed by hand against `packages/shared/src/constants.ts` (`PERMISSION_KEYS`, 20 entries), `server/src/services/company-member-roles.ts` (`grantsForHumanRole`), and `server/src/services/authorization.ts` (the `inbox:manage` enforcement branches).
|
||||
|
||||
## 🩺 Housekeeping
|
||||
|
||||
- **Stale `last_seen_parent_sha`.** The value stored by the prior run, `992389480a243b97bda214227e0767eb8c3672af`, **no longer resolves** in `paperclipai/paperclip` — `npm run sync:check` fails its drift step on it. The commit was most likely rebased away or GC'd upstream. This run's state update to `f12bb27` clears it. The field is informational only, so nothing else was affected; the Phase 1.5 drift check ran cleanly against `master` explicitly.
|
||||
- **Orphan nav files** — `docs/reference/cli/commands.md` and `docs/reference/cli/control-plane-commands.md` remain intentional redirect anchors, not missed registrations. Expected warning.
|
||||
|
||||
## ↻ Newly settled, context-only
|
||||
|
||||
- **Codex auth precedence and copyback** — `packages/adapters/codex-local/src/server/auth-precedence.ts`, `codex-auth-copyback.ts`, `codex-auth-merge-scripts.ts` and friends. Internal auth-resolution behaviour; no documented adapter config field changed.
|
||||
- **Claude adapter parse changes** — `packages/adapters/claude-local/src/server/parse.ts`. Transcript parsing internals.
|
||||
- **Hot restart** — `server/src/services/hot-restart.ts` (new). No documented contract or env var yet.
|
||||
- **Company search extract** — `server/src/services/company-search-extract.ts` (new). Internal extraction split out of `company-search.ts`.
|
||||
- **Smoke Lab fixture demo servers** — `packages/google-sheets-mcp-server` and `packages/kv-demo-mcp-server` match the env-vars watcher via `packages/**/config.ts`, but their config is fixture-local, not instance configuration. Not documented.
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
- **134 of 284** registry screenshots are stale for this window (36 routes) — see `SCREENSHOTS_PENDING.md`. Not recaptured; capture lands in a separate PR for review (`npm run screenshots:refresh`).
|
||||
- The four new pages this run ship without captures (custom-sandbox-images has no screenshots yet — highest-value shot: the setup session with the browser terminal open).
|
||||
|
||||
## ⚠ Drift
|
||||
|
||||
- None. `check-drift.mjs --against master` reported **0 drift records** — every documented parent path, CLI command, env var, REST route, and permission key still resolves upstream.
|
||||
|
||||
## ⚠ Reconcile
|
||||
|
||||
- **Navigation-path inconsistency (doc-internal, not upstream drift).** The new custom-sandbox-images page follows the parent UI breadcrumb **Settings → Instance settings → Environments** (from `CompanyEnvironments.tsx`), but the existing `docs/reference/adapters/sandbox-providers.md` says **"Company Settings → Environments."** Reconcile the two to one wording repo-wide (the code supports "Settings → Instance settings → Environments").
|
||||
- No previously-applied entries disappeared from the cumulative manifest.
|
||||
**248 of 298** registry entries are stale — see `SCREENSHOTS_PENDING.md` for the per-area breakdown. The cumulative window touched **614** files under `ui/src/**`, so this is closer to a full-surface refresh than a set of targeted recaptures. Consider `npm run screenshots:refresh:all` rather than the stale-only pass. Output PNGs go to a PR for human review; the skill never pushes them.
|
||||
+384
-29
@@ -1,43 +1,398 @@
|
||||
# Screenshots pending recapture
|
||||
|
||||
Regenerated by the release-mode `/sync-docs` run for parent **v2026.707.0**
|
||||
(cumulative window `v2026.626.0` → `v2026.707.0`, parent tag sha
|
||||
`390627b46eb333309d357004384b220ecf8a65af`).
|
||||
Generated by `/sync-docs` release run for **paperclip v2026.720.0** (window v2026.707.0 to v2026.720.0).
|
||||
|
||||
**134 of 284** registry screenshots have a `depends_on` parent path that changed
|
||||
in this window, spread across **36 routes**. They are *flagged*, not recaptured —
|
||||
capture always lands in a separate PR for human review and is never auto-pushed.
|
||||
**248 of 298** registry entries are stale.
|
||||
|
||||
Recapture with the built-in pipeline (stale only), then review the resulting PNGs:
|
||||
This release landed a design-system convergence (single-source design tokens, a retuned theme, broad Card/Badge adoption, a multiplicative radius ladder, unified list surfaces, and Lucide task-status glyphs). That touches nearly every surface, so this is a full-overhaul cycle rather than a targeted refresh:
|
||||
|
||||
```sh
|
||||
npm run screenshots:refresh
|
||||
npm run screenshots:refresh:all
|
||||
```
|
||||
|
||||
## Stale routes (shot count)
|
||||
The captured PNGs go to a PR for human review; they are never auto-pushed.
|
||||
|
||||
Highest-churn first — these routes' underlying `ui/src/**` or `server/src/**`
|
||||
sources moved in the release (env-var editor redesign, adapter label
|
||||
normalization, issue-detail slimming, costs, inbox descendant status, etc.):
|
||||
---
|
||||
|
||||
| Route | Shots |
|
||||
|---|---|
|
||||
| `/ACME/costs` | 16 |
|
||||
| `/ACME/issues/{issueId}` | 14 |
|
||||
| `/ACME/agents/new` | 12 |
|
||||
| `/instance/settings/adapters` | 10 |
|
||||
| `/ACME/agents/{agentId}/configuration` | 8 |
|
||||
| `/ACME/inbox/mine` | 8 |
|
||||
| `/ACME/agents/{agentId}/budget` | 4 |
|
||||
| `/ACME/agents/{agentId}/runs` | 4 |
|
||||
| `/ACME/inbox/requests` | 4 |
|
||||
| … 27 more routes | 2 each |
|
||||
## Stale entries by area
|
||||
|
||||
## Newly-authored pages with no screenshots yet
|
||||
### `dark/activity` (3)
|
||||
|
||||
These v2026.707.0 pages ship without captures — capture on the next screenshot pass:
|
||||
- `dark/activity/activity-filtered-by-agent.png` — deps changed: `ui/src/pages/Activity.tsx`
|
||||
- `dark/activity/activity-filters.png` — deps changed: `ui/src/pages/Activity.tsx`
|
||||
- `dark/activity/activity-log-full.png` — deps changed: `ui/src/pages/Activity.tsx`
|
||||
|
||||
- `docs/guides/projects-workflow/custom-sandbox-images.md` — environment configuration flow, custom-image build/setup states, embedded SSH terminal.
|
||||
### `dark/adapters` (4)
|
||||
|
||||
- `dark/adapters/detail.png` — deps changed: `ui/src/pages/AdapterManager.tsx`
|
||||
- `dark/adapters/health.png` — deps changed: `ui/src/pages/AdapterManager.tsx`
|
||||
- `dark/adapters/install.png` — deps changed: `ui/src/pages/AdapterManager.tsx`
|
||||
- `dark/adapters/list.png` — deps changed: `ui/src/pages/AdapterManager.tsx`
|
||||
|
||||
### `dark/agents` (23)
|
||||
|
||||
- `dark/agents/adapter-type-dropdown.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `dark/agents/agent-detail-idle.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `dark/agents/agent-detail-run-history.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `dark/agents/agent-status-running.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `dark/agents/agents-list-empty.png` — deps changed: `ui/src/pages/Agents.tsx`
|
||||
- `dark/agents/budget-and-heartbeat-fields.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `dark/agents/budget.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `dark/agents/claude-local-config-filled.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `dark/agents/codex-local-config.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `dark/agents/configuration.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `dark/agents/dashboard.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `dark/agents/heartbeat-toggle-disabled.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `dark/agents/heartbeat-toggle-enabled.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `dark/agents/http-adapter-config.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `dark/agents/instructions.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `dark/agents/list.png` — deps changed: `ui/src/pages/Agents.tsx`
|
||||
- `dark/agents/new.png` — deps changed: `ui/src/pages/Agents.tsx`
|
||||
- `dark/agents/run-history-in-progress.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `dark/agents/run-transcript-view.png` — deps changed: `ui/src/components/transcript/RunTranscriptView.tsx`, `ui/src/pages/AgentDetail.tsx`
|
||||
- `dark/agents/runs.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `dark/agents/skills.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `dark/agents/test-environment-fail.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `dark/agents/test-environment-success.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
|
||||
### `dark/approvals` (9)
|
||||
|
||||
- `dark/approvals/approvals-list.png` — deps changed: `ui/src/pages/Approvals.tsx`, `ui/src/components/ApprovalCard.tsx`
|
||||
- `dark/approvals/approvals-queue-strategy.png` — deps changed: `ui/src/pages/Approvals.tsx`, `ui/src/components/ApprovalPayload.tsx`
|
||||
- `dark/approvals/approve-reject-buttons.png` — deps changed: `ui/src/pages/ApprovalDetail.tsx`
|
||||
- `dark/approvals/approve-reject-revision-buttons.png` — deps changed: `ui/src/pages/ApprovalDetail.tsx`
|
||||
- `dark/approvals/approved-approval.png` — deps changed: `ui/src/pages/ApprovalDetail.tsx`
|
||||
- `dark/approvals/hire-approval-detail.png` — deps changed: `ui/src/components/ApprovalPayload.tsx`, `ui/src/pages/ApprovalDetail.tsx`
|
||||
- `dark/approvals/queue-filters.png` — deps changed: `ui/src/pages/Approvals.tsx`
|
||||
- `dark/approvals/revision-request-input.png` — deps changed: `ui/src/pages/ApprovalDetail.tsx`
|
||||
- `dark/approvals/strategy-approval-detail.png` — deps changed: `ui/src/components/ApprovalPayload.tsx`, `ui/src/pages/ApprovalDetail.tsx`
|
||||
|
||||
### `dark/company` (6)
|
||||
|
||||
- `dark/company/access.png` — deps changed: `ui/src/pages/CompanyAccess.tsx`
|
||||
- `dark/company/export.png` — deps changed: `ui/src/pages/CompanyExport.tsx`
|
||||
- `dark/company/import.png` — deps changed: `ui/src/pages/CompanyImport.tsx`
|
||||
- `dark/company/invites.png` — deps changed: `ui/src/pages/CompanyInvites.tsx`
|
||||
- `dark/company/join-requests.png` — deps changed: `ui/src/pages/Inbox.tsx`
|
||||
- `dark/company/settings.png` — deps changed: `ui/src/pages/CompanySettings.tsx`
|
||||
|
||||
### `dark/costs` (13)
|
||||
|
||||
- `dark/costs/agent-budget-100pct-paused.png` — deps changed: `ui/src/components/BudgetPolicyCard.tsx`
|
||||
- `dark/costs/agent-budget-50pct.png` — deps changed: `ui/src/components/BudgetPolicyCard.tsx`
|
||||
- `dark/costs/agent-budget-80pct-warning.png` — deps changed: `ui/src/components/BudgetPolicyCard.tsx`
|
||||
- `dark/costs/agent-budget-bar.png` — deps changed: `ui/src/components/BudgetPolicyCard.tsx`
|
||||
- `dark/costs/agent-budget-field.png` — deps changed: `ui/src/pages/AgentDetail.tsx`, `ui/src/components/BudgetPolicyCard.tsx`
|
||||
- `dark/costs/billers.png` — deps changed: `ui/src/pages/Costs.tsx`
|
||||
- `dark/costs/budgets.png` — deps changed: `ui/src/pages/Costs.tsx`
|
||||
- `dark/costs/company-budget-field.png` — deps changed: `ui/src/pages/Costs.tsx`, `ui/src/components/BudgetPolicyCard.tsx`
|
||||
- `dark/costs/costs-dashboard-overview.png` — deps changed: `ui/src/pages/Costs.tsx`
|
||||
- `dark/costs/finance.png` — deps changed: `ui/src/pages/Costs.tsx`
|
||||
- `dark/costs/overview.png` — deps changed: `ui/src/pages/Costs.tsx`
|
||||
- `dark/costs/per-run-cost-detail.png` — deps changed: `ui/src/pages/Costs.tsx`
|
||||
- `dark/costs/providers.png` — deps changed: `ui/src/pages/Costs.tsx`
|
||||
|
||||
### `dark/dashboard` (4)
|
||||
|
||||
- `dark/dashboard/activity-feed.png` — deps changed: `ui/src/pages/Dashboard.tsx`
|
||||
- `dark/dashboard/agent-status-panel.png` — deps changed: `ui/src/pages/Dashboard.tsx`
|
||||
- `dark/dashboard/dashboard-overview.png` — deps changed: `ui/src/pages/Dashboard.tsx`
|
||||
- `dark/dashboard/task-breakdown-panel.png` — deps changed: `ui/src/pages/Dashboard.tsx`
|
||||
|
||||
### `dark/interactions` (1)
|
||||
|
||||
- `dark/interactions/checkbox-confirmation-card.png` — deps changed: `ui/src/pages/IssueDetail.tsx`, `ui/src/components/IssueThreadInteractionCard.tsx`
|
||||
|
||||
### `dark/issues` (8)
|
||||
|
||||
- `dark/issues/detail-activity.png` — deps changed: `ui/src/pages/IssueDetail.tsx`
|
||||
- `dark/issues/detail-chat.png` — deps changed: `ui/src/pages/IssueDetail.tsx`
|
||||
- `dark/issues/detail-sidebar.png` — deps changed: `ui/src/pages/IssueDetail.tsx`
|
||||
- `dark/issues/inbox-requests.png` — deps changed: `ui/src/pages/Inbox.tsx`
|
||||
- `dark/issues/inbox-unread.png` — deps changed: `ui/src/pages/Inbox.tsx`
|
||||
- `dark/issues/inbox.png` — deps changed: `ui/src/pages/Inbox.tsx`
|
||||
- `dark/issues/list.png` — deps changed: `ui/src/pages/IssueDetail.tsx`
|
||||
- `dark/issues/my-issues.png` — deps changed: `ui/src/pages/Inbox.tsx`
|
||||
|
||||
### `dark/onboarding` (4)
|
||||
|
||||
- `dark/onboarding/empty-dashboard.png` — deps changed: `ui/src/pages/Dashboard.tsx`
|
||||
- `dark/onboarding/goal-field.png` — deps changed: `ui/src/components/OnboardingWizard.tsx`
|
||||
- `dark/onboarding/new-company-modal-empty.png` — deps changed: `ui/src/components/OnboardingWizard.tsx`, `ui/src/components/SidebarCompanyMenu.tsx`
|
||||
- `dark/onboarding/sidebar-new-company-button.png` — deps changed: `ui/src/components/SidebarCompanyMenu.tsx`
|
||||
|
||||
### `dark/org` (5)
|
||||
|
||||
- `dark/org/chart.png` — deps changed: `ui/src/pages/OrgChart.tsx`
|
||||
- `dark/org/org-chart-small-team.png` — deps changed: `ui/src/pages/OrgChart.tsx`
|
||||
- `dark/org/org-chart-view.png` — deps changed: `ui/src/pages/OrgChart.tsx`
|
||||
- `dark/org/reassign.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `dark/org/skills-list.png` — deps changed: `ui/src/pages/CompanySkills.tsx`
|
||||
|
||||
### `dark/plugins` (5)
|
||||
|
||||
- `dark/plugins/detail.png` — deps changed: `ui/src/pages/PluginPage.tsx`
|
||||
- `dark/plugins/install.png` — deps changed: `ui/src/pages/PluginManager.tsx`
|
||||
- `dark/plugins/jobs-log.png` — deps changed: `ui/src/pages/PluginSettings.tsx`
|
||||
- `dark/plugins/list.png` — deps changed: `ui/src/pages/PluginManager.tsx`
|
||||
- `dark/plugins/settings.png` — deps changed: `ui/src/pages/PluginSettings.tsx`
|
||||
|
||||
### `dark/projects` (6)
|
||||
|
||||
- `dark/projects/budget.png` — deps changed: `ui/src/pages/ProjectDetail.tsx`
|
||||
- `dark/projects/configuration.png` — deps changed: `ui/src/pages/ProjectDetail.tsx`
|
||||
- `dark/projects/issues.png` — deps changed: `ui/src/pages/ProjectDetail.tsx`
|
||||
- `dark/projects/list.png` — deps changed: `ui/src/pages/Projects.tsx`
|
||||
- `dark/projects/overview.png` — deps changed: `ui/src/pages/ProjectDetail.tsx`
|
||||
- `dark/projects/workspaces.png` — deps changed: `ui/src/pages/ProjectDetail.tsx`
|
||||
|
||||
### `dark/routines` (4)
|
||||
|
||||
- `dark/routines/cron-picker.png` — deps changed: `ui/src/pages/RoutineDetail.tsx`, `ui/src/components/ScheduleEditor.tsx`
|
||||
- `dark/routines/detail.png` — deps changed: `ui/src/pages/RoutineDetail.tsx`
|
||||
- `dark/routines/list.png` — deps changed: `ui/src/pages/Routines.tsx`
|
||||
- `dark/routines/run-history.png` — deps changed: `ui/src/pages/RoutineDetail.tsx`
|
||||
|
||||
### `dark/secrets` (3)
|
||||
|
||||
- `dark/secrets/user-secret-definition.png` — deps changed: `ui/src/pages/Secrets.tsx`, `ui/src/pages/secrets/UserSecretDefinitionsTab.tsx`
|
||||
- `dark/secrets/per-user-value-entry.png` — deps changed: `ui/src/pages/Secrets.tsx`, `ui/src/pages/secrets/SetMyUserSecretDialog.tsx`, `ui/src/pages/secrets/MyUserSecretsTab.tsx`
|
||||
- `dark/secrets/dispatch-check.png` — deps changed: `ui/src/pages/secrets/MissingUserSecretsBanner.tsx`, `server/src/services/secrets.ts`
|
||||
|
||||
### `dark/settings` (6)
|
||||
|
||||
- `dark/settings/experimental.png` — deps changed: `ui/src/pages/InstanceExperimentalSettings.tsx`
|
||||
- `dark/settings/instance-access.png` — deps changed: `ui/src/pages/InstanceAccess.tsx`
|
||||
- `dark/settings/instance-adapters.png` — deps changed: `ui/src/pages/AdapterManager.tsx`
|
||||
- `dark/settings/instance-general.png` — deps changed: `ui/src/pages/InstanceGeneralSettings.tsx`
|
||||
- `dark/settings/profile.png` — deps changed: `ui/src/pages/ProfileSettings.tsx`
|
||||
- `dark/settings/scheduler-heartbeats.png` — deps changed: `ui/src/pages/InstanceSettings.tsx`
|
||||
|
||||
### `dark/skills` (5)
|
||||
|
||||
- `dark/skills/add-skill-dialog-confirm.png` — deps changed: `ui/src/pages/CompanySkills.tsx`
|
||||
- `dark/skills/add-skill-dialog.png` — deps changed: `ui/src/pages/CompanySkills.tsx`
|
||||
- `dark/skills/assign-to-agent.png` — deps changed: `ui/src/pages/CompanySkills.tsx`
|
||||
- `dark/skills/file-inventory.png` — deps changed: `ui/src/pages/CompanySkills.tsx`
|
||||
- `dark/skills/list.png` — deps changed: `ui/src/pages/CompanySkills.tsx`
|
||||
|
||||
### `dark/tasks` (3)
|
||||
|
||||
- `dark/tasks/inbox-newly-created-tasks.png` — deps changed: `ui/src/pages/Inbox.tsx`
|
||||
- `dark/tasks/inbox-view.png` — deps changed: `ui/src/pages/Inbox.tsx`
|
||||
- `dark/tasks/task-detail-with-comments.png` — deps changed: `ui/src/pages/IssueDetail.tsx`
|
||||
|
||||
### `dark/watchdogs` (1)
|
||||
|
||||
- `dark/watchdogs/watchdog-thread-outcome.png` — deps changed: `ui/src/pages/IssueDetail.tsx`, `server/src/services/task-watchdogs.ts`
|
||||
|
||||
### `dark/work-modes` (4)
|
||||
|
||||
- `dark/work-modes/ask-mode-answer.png` — deps changed: `ui/src/pages/IssueDetail.tsx`, `ui/src/components/IssueChatThread.tsx`
|
||||
- `dark/work-modes/plan-mode-plan.png` — deps changed: `ui/src/pages/IssueDetail.tsx`, `ui/src/components/IssueDocumentsSection.tsx`
|
||||
- `dark/work-modes/standard-mode-run.png` — deps changed: `ui/src/pages/IssueDetail.tsx`, `ui/src/components/IssueChatThread.tsx`
|
||||
- `dark/work-modes/work-mode-picker.png` — deps changed: `ui/src/components/NewIssueDialog.tsx`
|
||||
|
||||
### `dark/work-timeline` (2)
|
||||
|
||||
- `dark/work-timeline/work-timeline-overview.png` — deps changed: `ui/src/pages/Timeline.tsx`, `server/src/services/work-timeline.ts`
|
||||
- `dark/work-timeline/work-timeline-handoff.png` — deps changed: `ui/src/pages/Timeline.tsx`, `server/src/services/work-timeline.ts`
|
||||
|
||||
### `dark/workspaces` (5)
|
||||
|
||||
- `dark/workspaces/configuration.png` — deps changed: `ui/src/pages/ExecutionWorkspaceDetail.tsx`
|
||||
- `dark/workspaces/issues.png` — deps changed: `ui/src/pages/ExecutionWorkspaceDetail.tsx`
|
||||
- `dark/workspaces/list.png` — deps changed: `ui/src/pages/Workspaces.tsx`
|
||||
- `dark/workspaces/runtime-logs.png` — deps changed: `ui/src/pages/ExecutionWorkspaceDetail.tsx`
|
||||
- `dark/workspaces/services.png` — deps changed: `ui/src/pages/ExecutionWorkspaceDetail.tsx`
|
||||
|
||||
### `light/activity` (3)
|
||||
|
||||
- `light/activity/activity-filtered-by-agent.png` — deps changed: `ui/src/pages/Activity.tsx`
|
||||
- `light/activity/activity-filters.png` — deps changed: `ui/src/pages/Activity.tsx`
|
||||
- `light/activity/activity-log-full.png` — deps changed: `ui/src/pages/Activity.tsx`
|
||||
|
||||
### `light/adapters` (4)
|
||||
|
||||
- `light/adapters/detail.png` — deps changed: `ui/src/pages/AdapterManager.tsx`
|
||||
- `light/adapters/health.png` — deps changed: `ui/src/pages/AdapterManager.tsx`
|
||||
- `light/adapters/install.png` — deps changed: `ui/src/pages/AdapterManager.tsx`
|
||||
- `light/adapters/list.png` — deps changed: `ui/src/pages/AdapterManager.tsx`
|
||||
|
||||
### `light/agents` (23)
|
||||
|
||||
- `light/agents/adapter-type-dropdown.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `light/agents/agent-detail-idle.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `light/agents/agent-detail-run-history.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `light/agents/agent-status-running.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `light/agents/agents-list-empty.png` — deps changed: `ui/src/pages/Agents.tsx`
|
||||
- `light/agents/budget-and-heartbeat-fields.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `light/agents/budget.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `light/agents/claude-local-config-filled.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `light/agents/codex-local-config.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `light/agents/configuration.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `light/agents/dashboard.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `light/agents/heartbeat-toggle-disabled.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `light/agents/heartbeat-toggle-enabled.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `light/agents/http-adapter-config.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `light/agents/instructions.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `light/agents/list.png` — deps changed: `ui/src/pages/Agents.tsx`
|
||||
- `light/agents/new.png` — deps changed: `ui/src/pages/Agents.tsx`
|
||||
- `light/agents/run-history-in-progress.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `light/agents/run-transcript-view.png` — deps changed: `ui/src/components/transcript/RunTranscriptView.tsx`, `ui/src/pages/AgentDetail.tsx`
|
||||
- `light/agents/runs.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `light/agents/skills.png` — deps changed: `ui/src/pages/AgentDetail.tsx`
|
||||
- `light/agents/test-environment-fail.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `light/agents/test-environment-success.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
|
||||
### `light/approvals` (9)
|
||||
|
||||
- `light/approvals/approvals-list.png` — deps changed: `ui/src/pages/Approvals.tsx`, `ui/src/components/ApprovalCard.tsx`
|
||||
- `light/approvals/approvals-queue-strategy.png` — deps changed: `ui/src/pages/Approvals.tsx`, `ui/src/components/ApprovalPayload.tsx`
|
||||
- `light/approvals/approve-reject-buttons.png` — deps changed: `ui/src/pages/ApprovalDetail.tsx`
|
||||
- `light/approvals/approve-reject-revision-buttons.png` — deps changed: `ui/src/pages/ApprovalDetail.tsx`
|
||||
- `light/approvals/approved-approval.png` — deps changed: `ui/src/pages/ApprovalDetail.tsx`
|
||||
- `light/approvals/hire-approval-detail.png` — deps changed: `ui/src/components/ApprovalPayload.tsx`, `ui/src/pages/ApprovalDetail.tsx`
|
||||
- `light/approvals/queue-filters.png` — deps changed: `ui/src/pages/Approvals.tsx`
|
||||
- `light/approvals/revision-request-input.png` — deps changed: `ui/src/pages/ApprovalDetail.tsx`
|
||||
- `light/approvals/strategy-approval-detail.png` — deps changed: `ui/src/components/ApprovalPayload.tsx`, `ui/src/pages/ApprovalDetail.tsx`
|
||||
|
||||
### `light/company` (6)
|
||||
|
||||
- `light/company/access.png` — deps changed: `ui/src/pages/CompanyAccess.tsx`
|
||||
- `light/company/export.png` — deps changed: `ui/src/pages/CompanyExport.tsx`
|
||||
- `light/company/import.png` — deps changed: `ui/src/pages/CompanyImport.tsx`
|
||||
- `light/company/invites.png` — deps changed: `ui/src/pages/CompanyInvites.tsx`
|
||||
- `light/company/join-requests.png` — deps changed: `ui/src/pages/Inbox.tsx`
|
||||
- `light/company/settings.png` — deps changed: `ui/src/pages/CompanySettings.tsx`
|
||||
|
||||
### `light/costs` (13)
|
||||
|
||||
- `light/costs/agent-budget-100pct-paused.png` — deps changed: `ui/src/components/BudgetPolicyCard.tsx`
|
||||
- `light/costs/agent-budget-50pct.png` — deps changed: `ui/src/components/BudgetPolicyCard.tsx`
|
||||
- `light/costs/agent-budget-80pct-warning.png` — deps changed: `ui/src/components/BudgetPolicyCard.tsx`
|
||||
- `light/costs/agent-budget-bar.png` — deps changed: `ui/src/components/BudgetPolicyCard.tsx`
|
||||
- `light/costs/agent-budget-field.png` — deps changed: `ui/src/pages/AgentDetail.tsx`, `ui/src/components/BudgetPolicyCard.tsx`
|
||||
- `light/costs/billers.png` — deps changed: `ui/src/pages/Costs.tsx`
|
||||
- `light/costs/budgets.png` — deps changed: `ui/src/pages/Costs.tsx`
|
||||
- `light/costs/company-budget-field.png` — deps changed: `ui/src/pages/Costs.tsx`, `ui/src/components/BudgetPolicyCard.tsx`
|
||||
- `light/costs/costs-dashboard-overview.png` — deps changed: `ui/src/pages/Costs.tsx`
|
||||
- `light/costs/finance.png` — deps changed: `ui/src/pages/Costs.tsx`
|
||||
- `light/costs/overview.png` — deps changed: `ui/src/pages/Costs.tsx`
|
||||
- `light/costs/per-run-cost-detail.png` — deps changed: `ui/src/pages/Costs.tsx`
|
||||
- `light/costs/providers.png` — deps changed: `ui/src/pages/Costs.tsx`
|
||||
|
||||
### `light/dashboard` (4)
|
||||
|
||||
- `light/dashboard/activity-feed.png` — deps changed: `ui/src/pages/Dashboard.tsx`
|
||||
- `light/dashboard/agent-status-panel.png` — deps changed: `ui/src/pages/Dashboard.tsx`
|
||||
- `light/dashboard/dashboard-overview.png` — deps changed: `ui/src/pages/Dashboard.tsx`
|
||||
- `light/dashboard/task-breakdown-panel.png` — deps changed: `ui/src/pages/Dashboard.tsx`
|
||||
|
||||
### `light/interactions` (1)
|
||||
|
||||
- `light/interactions/checkbox-confirmation-card.png` — deps changed: `ui/src/pages/IssueDetail.tsx`, `ui/src/components/IssueThreadInteractionCard.tsx`
|
||||
|
||||
### `light/issues` (8)
|
||||
|
||||
- `light/issues/detail-activity.png` — deps changed: `ui/src/pages/IssueDetail.tsx`
|
||||
- `light/issues/detail-chat.png` — deps changed: `ui/src/pages/IssueDetail.tsx`
|
||||
- `light/issues/detail-sidebar.png` — deps changed: `ui/src/pages/IssueDetail.tsx`
|
||||
- `light/issues/inbox-requests.png` — deps changed: `ui/src/pages/Inbox.tsx`
|
||||
- `light/issues/inbox-unread.png` — deps changed: `ui/src/pages/Inbox.tsx`
|
||||
- `light/issues/inbox.png` — deps changed: `ui/src/pages/Inbox.tsx`
|
||||
- `light/issues/list.png` — deps changed: `ui/src/pages/IssueDetail.tsx`
|
||||
- `light/issues/my-issues.png` — deps changed: `ui/src/pages/Inbox.tsx`
|
||||
|
||||
### `light/onboarding` (4)
|
||||
|
||||
- `light/onboarding/empty-dashboard.png` — deps changed: `ui/src/pages/Dashboard.tsx`
|
||||
- `light/onboarding/goal-field.png` — deps changed: `ui/src/components/OnboardingWizard.tsx`
|
||||
- `light/onboarding/new-company-modal-empty.png` — deps changed: `ui/src/components/OnboardingWizard.tsx`, `ui/src/components/SidebarCompanyMenu.tsx`
|
||||
- `light/onboarding/sidebar-new-company-button.png` — deps changed: `ui/src/components/SidebarCompanyMenu.tsx`
|
||||
|
||||
### `light/org` (5)
|
||||
|
||||
- `light/org/chart.png` — deps changed: `ui/src/pages/OrgChart.tsx`
|
||||
- `light/org/org-chart-small-team.png` — deps changed: `ui/src/pages/OrgChart.tsx`
|
||||
- `light/org/org-chart-view.png` — deps changed: `ui/src/pages/OrgChart.tsx`
|
||||
- `light/org/reassign.png` — deps changed: `ui/src/components/AgentConfigForm.tsx`
|
||||
- `light/org/skills-list.png` — deps changed: `ui/src/pages/CompanySkills.tsx`
|
||||
|
||||
### `light/plugins` (5)
|
||||
|
||||
- `light/plugins/detail.png` — deps changed: `ui/src/pages/PluginPage.tsx`
|
||||
- `light/plugins/install.png` — deps changed: `ui/src/pages/PluginManager.tsx`
|
||||
- `light/plugins/jobs-log.png` — deps changed: `ui/src/pages/PluginSettings.tsx`
|
||||
- `light/plugins/list.png` — deps changed: `ui/src/pages/PluginManager.tsx`
|
||||
- `light/plugins/settings.png` — deps changed: `ui/src/pages/PluginSettings.tsx`
|
||||
|
||||
### `light/projects` (6)
|
||||
|
||||
- `light/projects/budget.png` — deps changed: `ui/src/pages/ProjectDetail.tsx`
|
||||
- `light/projects/configuration.png` — deps changed: `ui/src/pages/ProjectDetail.tsx`
|
||||
- `light/projects/issues.png` — deps changed: `ui/src/pages/ProjectDetail.tsx`
|
||||
- `light/projects/list.png` — deps changed: `ui/src/pages/Projects.tsx`
|
||||
- `light/projects/overview.png` — deps changed: `ui/src/pages/ProjectDetail.tsx`
|
||||
- `light/projects/workspaces.png` — deps changed: `ui/src/pages/ProjectDetail.tsx`
|
||||
|
||||
### `light/routines` (4)
|
||||
|
||||
- `light/routines/cron-picker.png` — deps changed: `ui/src/pages/RoutineDetail.tsx`, `ui/src/components/ScheduleEditor.tsx`
|
||||
- `light/routines/detail.png` — deps changed: `ui/src/pages/RoutineDetail.tsx`
|
||||
- `light/routines/list.png` — deps changed: `ui/src/pages/Routines.tsx`
|
||||
- `light/routines/run-history.png` — deps changed: `ui/src/pages/RoutineDetail.tsx`
|
||||
|
||||
### `light/secrets` (3)
|
||||
|
||||
- `light/secrets/user-secret-definition.png` — deps changed: `ui/src/pages/Secrets.tsx`, `ui/src/pages/secrets/UserSecretDefinitionsTab.tsx`
|
||||
- `light/secrets/per-user-value-entry.png` — deps changed: `ui/src/pages/Secrets.tsx`, `ui/src/pages/secrets/SetMyUserSecretDialog.tsx`, `ui/src/pages/secrets/MyUserSecretsTab.tsx`
|
||||
- `light/secrets/dispatch-check.png` — deps changed: `ui/src/pages/secrets/MissingUserSecretsBanner.tsx`, `server/src/services/secrets.ts`
|
||||
|
||||
### `light/settings` (6)
|
||||
|
||||
- `light/settings/experimental.png` — deps changed: `ui/src/pages/InstanceExperimentalSettings.tsx`
|
||||
- `light/settings/instance-access.png` — deps changed: `ui/src/pages/InstanceAccess.tsx`
|
||||
- `light/settings/instance-adapters.png` — deps changed: `ui/src/pages/AdapterManager.tsx`
|
||||
- `light/settings/instance-general.png` — deps changed: `ui/src/pages/InstanceGeneralSettings.tsx`
|
||||
- `light/settings/profile.png` — deps changed: `ui/src/pages/ProfileSettings.tsx`
|
||||
- `light/settings/scheduler-heartbeats.png` — deps changed: `ui/src/pages/InstanceSettings.tsx`
|
||||
|
||||
### `light/skills` (5)
|
||||
|
||||
- `light/skills/add-skill-dialog-confirm.png` — deps changed: `ui/src/pages/CompanySkills.tsx`
|
||||
- `light/skills/add-skill-dialog.png` — deps changed: `ui/src/pages/CompanySkills.tsx`
|
||||
- `light/skills/assign-to-agent.png` — deps changed: `ui/src/pages/CompanySkills.tsx`
|
||||
- `light/skills/file-inventory.png` — deps changed: `ui/src/pages/CompanySkills.tsx`
|
||||
- `light/skills/list.png` — deps changed: `ui/src/pages/CompanySkills.tsx`
|
||||
|
||||
### `light/tasks` (3)
|
||||
|
||||
- `light/tasks/inbox-newly-created-tasks.png` — deps changed: `ui/src/pages/Inbox.tsx`
|
||||
- `light/tasks/inbox-view.png` — deps changed: `ui/src/pages/Inbox.tsx`
|
||||
- `light/tasks/task-detail-with-comments.png` — deps changed: `ui/src/pages/IssueDetail.tsx`
|
||||
|
||||
### `light/watchdogs` (1)
|
||||
|
||||
- `light/watchdogs/watchdog-thread-outcome.png` — deps changed: `ui/src/pages/IssueDetail.tsx`, `server/src/services/task-watchdogs.ts`
|
||||
|
||||
### `light/work-modes` (4)
|
||||
|
||||
- `light/work-modes/ask-mode-answer.png` — deps changed: `ui/src/pages/IssueDetail.tsx`, `ui/src/components/IssueChatThread.tsx`
|
||||
- `light/work-modes/plan-mode-plan.png` — deps changed: `ui/src/pages/IssueDetail.tsx`, `ui/src/components/IssueDocumentsSection.tsx`
|
||||
- `light/work-modes/standard-mode-run.png` — deps changed: `ui/src/pages/IssueDetail.tsx`, `ui/src/components/IssueChatThread.tsx`
|
||||
- `light/work-modes/work-mode-picker.png` — deps changed: `ui/src/components/NewIssueDialog.tsx`
|
||||
|
||||
### `light/work-timeline` (2)
|
||||
|
||||
- `light/work-timeline/work-timeline-overview.png` — deps changed: `ui/src/pages/Timeline.tsx`, `server/src/services/work-timeline.ts`
|
||||
- `light/work-timeline/work-timeline-handoff.png` — deps changed: `ui/src/pages/Timeline.tsx`, `server/src/services/work-timeline.ts`
|
||||
|
||||
### `light/workspaces` (5)
|
||||
|
||||
- `light/workspaces/configuration.png` — deps changed: `ui/src/pages/ExecutionWorkspaceDetail.tsx`
|
||||
- `light/workspaces/issues.png` — deps changed: `ui/src/pages/ExecutionWorkspaceDetail.tsx`
|
||||
- `light/workspaces/list.png` — deps changed: `ui/src/pages/Workspaces.tsx`
|
||||
- `light/workspaces/runtime-logs.png` — deps changed: `ui/src/pages/ExecutionWorkspaceDetail.tsx`
|
||||
- `light/workspaces/services.png` — deps changed: `ui/src/pages/ExecutionWorkspaceDetail.tsx`
|
||||
|
||||
> Regenerated each run (not appended). If more `depends_on` parent paths move
|
||||
> before capture happens, the stale set grows on the next run.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
paperclip_version: v2026.626.0
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Roles & Permissions
|
||||
@@ -23,8 +23,8 @@ There is also one layer that sits *above* the company: the **instance admin**, c
|
||||
|
||||
| Role | Who it's for | Implicit grants |
|
||||
|---|---|---|
|
||||
| **Owner** | The people who run the company | `agents:create`, `skills:create`, `environments:manage`, `users:invite`, `users:manage_permissions`, `tasks:assign`, `joins:approve` |
|
||||
| **Admin** | Trusted operators who onboard people and agents | `agents:create`, `skills:create`, `environments:manage`, `users:invite`, `tasks:assign`, `joins:approve` |
|
||||
| **Owner** | The people who run the company | `agents:create`, `agents:configure`, `skills:create`, `environments:manage`, `users:invite`, `users:manage_permissions`, `tasks:assign`, `joins:approve` |
|
||||
| **Admin** | Trusted operators who onboard people and agents | `agents:create`, `agents:configure`, `skills:create`, `environments:manage`, `users:invite`, `tasks:assign`, `joins:approve` |
|
||||
| **Operator** | Hands-on members who help run the work | `tasks:assign` |
|
||||
| **Viewer** | Read-only observers | *(none)* |
|
||||
|
||||
@@ -36,13 +36,23 @@ A fifth option, **Unset**, appears in the role drop-down. It leaves the member w
|
||||
|
||||
## The permission keys
|
||||
|
||||
There are ten permission keys. Seven of them show up as defaults on one or more roles; three are **explicit-grant-only** — no role includes them, so a member only ever gets them if you check the box in the member editor.
|
||||
There are twenty permission keys. Eight of them show up as defaults on one or more roles; twelve are **explicit-grant-only** — no role includes them, so a member only ever gets them from an explicit grant.
|
||||
|
||||
| Permission key | What it allows | In which role by default |
|
||||
|---|---|---|
|
||||
| `agents:create` | Create (hire) new agents in the company | Owner, Admin |
|
||||
| `agents:configure` | Change an existing agent's setup — its adapter config, instructions, role, and budget | Owner, Admin |
|
||||
| `agents:suggest-changes` | Propose changes to an agent's setup for review, without applying them directly | — (explicit only) |
|
||||
| `skills:create` | Create and manage company skills | Owner, Admin |
|
||||
| `skills:suggest-changes` | Propose changes to a company skill for review, without applying them directly | — (explicit only) |
|
||||
| `environments:manage` | Create, edit, and remove the execution environments agents run in | Owner, Admin |
|
||||
| `tools:admin` | Set up the tool plumbing a company shares — the stdio command templates behind tool apps, and the MCP gateways agents connect through (including minting and revoking gateway tokens) | — (explicit only) |
|
||||
| `tools:manage_connections` | Choose which agents and projects a tool connection is installed on | — (explicit only) |
|
||||
| `tools:manage_profiles` | Reserved for tool profile management. It is grantable today but nothing checks it yet — see the note below | — (explicit only) |
|
||||
| `tools:view_audit` | Read the gateway's audit trail of tool calls | — (explicit only) |
|
||||
| `tools:use` | Try a connection's tools from the board — the test-call surface | — (explicit only) |
|
||||
| `tools:manage_runtime` | Inspect the tool runtime slots that are running, and stop or restart them | — (explicit only) |
|
||||
| `inbox:manage` | Act on *another* person's inbox. An agent working its own responsible user's inbox does not need this key — see the note below | — (explicit only) |
|
||||
| `users:invite` | Create and revoke company invite links | Owner, Admin |
|
||||
| `users:manage_permissions` | View and change members' roles and grants | Owner |
|
||||
| `tasks:assign` | Assign any issue to any agent or member in the company | Owner, Admin, Operator |
|
||||
@@ -51,13 +61,27 @@ There are ten permission keys. Seven of them show up as defaults on one or more
|
||||
| `pipelines:write` | Create and modify pipeline automations | — (explicit only) |
|
||||
| `joins:approve` | Approve or reject human and agent join requests | Owner, Admin |
|
||||
|
||||
### About the direct-vs-suggest pairs
|
||||
|
||||
Two of the keys come in matched pairs — a *direct* key that applies a change immediately, and a *suggest* key that only proposes one for review:
|
||||
|
||||
- **`agents:configure`** lets a member change an existing agent directly. **`agents:suggest-changes`** is the softer counterpart: a member who holds it (but not `agents:configure`) can *propose* a change to an agent's instructions or config, which then goes through review before it takes effect. This is what powers coaching flows like the built-in [Reflection Coach](../reference/api/built-in-agents.md#the-reflection-coach) — it can suggest an improvement to an agent's `AGENTS.md` without hot-swapping it.
|
||||
- **`skills:create`** lets a member author and edit company skills directly. **`skills:suggest-changes`** lets a member propose an edit to a company skill for review instead of writing it straight in.
|
||||
|
||||
### About the explicit-only keys
|
||||
|
||||
Three keys never appear in a role's defaults, so a member only receives them through an explicit grant in the member editor:
|
||||
Twelve keys never appear in a role's defaults, so a member only receives them through an explicit grant — from the member editor, or `member role-and-grants` on the CLI:
|
||||
|
||||
- **`agents:suggest-changes`** and **`skills:suggest-changes`** — the review-gated proposal keys described just above. Grant them to a member (or agent) you want proposing improvements without direct write access.
|
||||
- **`tasks:assign_scope`** is how you let someone delegate *within their lane* without giving them company-wide assignment power. When a member has `tasks:assign_scope` but not `tasks:assign`, Paperclip evaluates the grant against the scope attached to it and allows the assignment only if the target falls inside that scope. Set the scope in the grant payload (via the member editor's grant, or `member role-and-grants` on the CLI).
|
||||
- **`tasks:manage_active_checkouts`** is an escape hatch. Normally an issue that an agent has checked out is off-limits to others until it's released; this grant lets the holder reassign or clear that active checkout — handy when an agent has stalled mid-task.
|
||||
- **`pipelines:write`** lets a member create and edit pipeline automations. Grant it to whoever runs your pipelines; it is kept off the standard roles so pipeline authorship is a deliberate choice.
|
||||
- **The six `tools:*` keys** cover the tools and MCP surface, and none of them ride along with a role — connecting an outside tool to your agents is always a deliberate choice. Split them by job: `tools:admin` for whoever wires up apps and gateways, `tools:manage_connections` for whoever decides which agents get a connection, `tools:use` for people who need to test-call a tool, `tools:manage_runtime` for whoever babysits running tool processes, and `tools:view_audit` for anyone who needs to read the call trail without touching the setup. Note that `tools:admin` is not a superset — holding it does not imply the others, so grant each key you actually need.
|
||||
- **`inbox:manage`** governs *cross-user* inbox access. It matters most for agents: an agent may act on the inbox of the user it is responsible for without holding this key at all, but the moment it needs to touch someone else's inbox, Paperclip looks for an `inbox:manage` grant — and then checks that the grant's scope actually covers the user being acted on. Grant it, scoped, to an agent you want triaging inboxes beyond its own responsible user.
|
||||
|
||||
> **`inbox:manage` and the low-trust preset.** Agents running under the low-trust review preset are denied `inbox:manage` by default, alongside the other company-wide and privileged actions. Raising an agent's trust preset is a separate decision from granting the key — a low-trust agent holding the grant is still refused.
|
||||
|
||||
> **`tools:manage_profiles` is not wired up yet.** The key exists and you can grant it, but no endpoint checks it in this build. Editing tool profiles today only requires an active membership with any role other than Viewer. Treat the key as reserved: granting it changes nothing, and withholding it blocks nothing.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Settings
|
||||
|
||||
Paperclip has two settings surfaces, and it matters which one you're looking at.
|
||||
@@ -248,6 +252,7 @@ Turning one on is not dangerous in the "this will break your data" sense — but
|
||||
### The available flags
|
||||
|
||||
- **Enable Isolated Workspaces** — shows the execution-workspace controls in project configuration and allows isolated workspace behavior for new and existing issue runs. If you're not using isolated workspaces, leave this off; turning it on exposes extra UI you don't need.
|
||||
- **Enable Built-in Agents** — makes the built-in-agent controls and API available. Turn it on before you provision `briefs`, `learning`, or `reflection-coach`; with it off, the built-in-agent routes return `404 Not Found`.
|
||||
- **Auto-Restart Dev Server When Idle** — only relevant under `pnpm dev:once`. When the backend detects a stale boot (code changes, new migrations), it waits for queued and running local agent runs to finish and then restarts the dev server automatically. Useful during active development on Paperclip itself, not intended for production.
|
||||
- **Streamlined Left Navigation Bar** — **on by default now**, so you don't need to flip anything to get it. It trims the sidebar: Projects move under the **Work** section as a single **Projects** link with its own page, and the agents list shows only your active agents (the five most recently active). Turning this flag **off** is the opt-out — it restores the classic sidebar, where each project gets its own collapsible entry and there's no top-level Projects link.
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Decisions
|
||||
|
||||
The **Decisions** page gives you one place to see work that needs a human call. Instead of checking approvals, blocked issues, failed runs, and budget warnings separately, you can start here and work through a ranked queue.
|
||||
|
||||
Open it from the sidebar, or go directly to `/decisions`.
|
||||
|
||||
---
|
||||
|
||||
## Read the queue
|
||||
|
||||
Each row explains why it needs you now and links to the work behind it. You may see approvals, questions from agents, join requests, recovery actions, blocked dependency chains, reviews, failed runs, budget alerts, or agent-error alerts.
|
||||
|
||||
Paperclip ranks the visible queue by recent activity and urgency. That makes it a good starting point for your operating day, but it is not a permanent event archive: a row leaves when its underlying work no longer needs attention.
|
||||
|
||||
Rows that Paperclip can complete in place — approvals, issue-thread interactions, and join requests — give you the relevant decision controls. Other rows take you to the original issue, alert, or record so you can inspect the context before you act.
|
||||
|
||||
---
|
||||
|
||||
## Focus on the next decision
|
||||
|
||||
Use the toolbar to make a busy queue manageable:
|
||||
|
||||
- **Type**, **Severity**, **Project**, and **Workspace** filters narrow the queue to the work you own right now.
|
||||
- **Group by** offers `none`, `date`, `type`, `project`, and `severity`.
|
||||
- **Sort** offers `newest` and `oldest`.
|
||||
|
||||
Your filter, grouping, and sort choices stay with your current browser, so you can set up the view that fits your review rhythm.
|
||||
|
||||
---
|
||||
|
||||
## Set something aside without losing it
|
||||
|
||||
If a row is real but not for this moment, you can dismiss it or snooze it. Dismissal removes it from your active queue until newer activity makes it relevant again. Snooze hides it until the time you choose.
|
||||
|
||||
The built-in choices are **1 hour**, **4 hours**, **tomorrow morning**, and **next week**. Dismissed and snoozed rows stay available in their own expandable sections, where you can restore them. Right after a dismissal, Paperclip also shows an **Undo** action for eight seconds.
|
||||
|
||||
These choices are personal to your board user. They tidy your queue without changing the underlying issue, approval, run, or budget policy.
|
||||
|
||||
---
|
||||
|
||||
## When the queue is empty
|
||||
|
||||
**You're all caught up** means Paperclip has no visible item that needs a decision from you. If you have filters turned on, **No decisions match your filters** means the work may still exist — broaden the filters before assuming nothing needs attention.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [Approvals](./approvals.md) — review and resolve formal approval requests.
|
||||
- [Blocked Inbox](./blocked-inbox.md) — focus specifically on stopped issues.
|
||||
- [Issues](./issues.md) — inspect and guide the work behind a decision.
|
||||
- [Attention API](../../reference/api/attention.md) — build a board-facing integration around the same queue.
|
||||
+122
-3
@@ -1,5 +1,5 @@
|
||||
---
|
||||
paperclip_version: v2026.609.0
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Skills
|
||||
@@ -55,7 +55,7 @@ Skills keep the base context small. An agent with 10 skills doesn't load all 10
|
||||
|
||||
Skills are managed from the company-level **Skills** page. The page has a left-hand list of every installed skill and a right-hand pane that shows the selected skill's files, description, and usage. Two controls live above the skill list: a filter box to search by name, key, slug, or source, and an input where you can paste a source string to import a skill from elsewhere.
|
||||
|
||||
You can add a skill in one of four ways, depending on where it is coming from.
|
||||
You can add a skill in one of several ways, depending on where it is coming from.
|
||||
|
||||
### 1. Import from a GitHub repository
|
||||
|
||||
@@ -75,6 +75,22 @@ You can also paste a local path (Linux, WSL, or Windows) into the source field.
|
||||
|
||||
[skills.sh](https://skills.sh) is a community catalog of ready-made skills. Each listing provides a copy-ready install command; paste it into the source field and Paperclip imports the skill along with its source metadata (you will see a small skills.sh badge on the skill row).
|
||||
|
||||
### 5. Import skills straight from a project
|
||||
|
||||
If your skills already live in a repository that one of your projects points at, you don't need to hunt for the path yourself. The **Import skills from project** dialog does the looking for you: *"Pick a project, scan its workspaces for skills, and import them as references."*
|
||||
|
||||
It walks you through three steps.
|
||||
|
||||
**Pick a project.** You get the company's project list, with a **Filter projects** box at the top and a line under each project name telling you how many workspaces it has. Projects with nothing scannable are greyed out.
|
||||
|
||||
**Choose what to import.** Paperclip scans that project's workspaces for `SKILL.md` files and shows everything it found, grouped by workspace and then by the folder it was found in. Tick the ones you want. A **Search discovered skills…** box narrows a long list. If a skill's slug collides with something already in your library, the row is flagged as a conflict and an **Import as** field appears so you can give the incoming copy a different, lowercase URL-safe slug instead of overwriting anything.
|
||||
|
||||
If the scan comes up empty you'll see **No skills found**, along with a note about which well-known agent-harness folders were searched. Skills kept somewhere non-standard can still come in through **Import from path or URL** — the same source field described above.
|
||||
|
||||
**Review the result.** The last step summarises how many skills were imported, updated, and skipped, and each imported skill gets an **Open** link straight into Skill Studio.
|
||||
|
||||
One thing worth understanding before you use this: *"No files were copied. These skills reference the files in the project workspace — editing them in Skill Studio saves directly back to those files."* A project-imported skill is a live link to your checkout, not a snapshot. That is exactly what you want when the repository is the source of truth, and something to be deliberate about otherwise — see [Editing a skill that isn't yours to edit](#editing-a-skill-that-isnt-yours-to-edit) if you'd rather work on a copy.
|
||||
|
||||
### Creating a skill from scratch
|
||||
|
||||
If none of those sources apply, click the **+** button next to the scan icon. A small form appears inline with three fields:
|
||||
@@ -108,6 +124,107 @@ Because these skills come from Paperclip, the app knows exactly which version yo
|
||||
|
||||
---
|
||||
|
||||
## Organising skills into folders
|
||||
|
||||
A flat list is fine for five skills and painful for fifty. The Skills page has a **Folders** rail down the left side that lets you file skills into a nested tree, so you can group them the way your company actually thinks about them.
|
||||
|
||||
The rail is laid out in a fixed order:
|
||||
|
||||
- **All skills** — everything, ignoring folders. This is the view you land on.
|
||||
- **My Skills** — your own personal space. Every board user gets one, and only you see what's in yours. It's the right home for a skill you're still drafting and don't want the whole company tripping over.
|
||||
- **Company** — the shared folders. These are yours to create, nest, rename, and rearrange. If you haven't made any yet you'll see *"No company folders yet."*
|
||||
- **Projects** — one folder per project, filled in automatically. It's a mirror of your project structure, so a skill that came in from a project workspace has an obvious home.
|
||||
- **Bundled** — the skills that ship with Paperclip, grouped by category. Read-only.
|
||||
- **System → Unfiled** — everything that hasn't been filed anywhere yet.
|
||||
|
||||
**My Skills**, **Projects**, and **Bundled** are managed by Paperclip itself, so you can't rename or delete them. **Company** folders are entirely yours. Use the **+** next to the **Folders** heading to add one at the top level, or open a folder's **⋯** menu for **Rename**, **Edit color**, and — handily — **Move to Company** / **Move to My Skills**, which promotes a personal folder to the shared tree or pulls a shared one back into your own space. To reparent a folder somewhere deeper, use **New folder inside…** on the parent, or the move picker with its **Search folders** box and **Move here** button.
|
||||
|
||||
Folders nest up to four levels deep, which is usually more than enough. Below the tree you'll find a **Tags** section — folders tell you *where* a skill lives, tags describe *what it is*, and picking a tag filters within whatever part of the tree you're looking at.
|
||||
|
||||
If you want to script any of this — creating folders, moving skills between them, reordering — the whole tree is available over REST. See the [Folders API](../../reference/api/folders.md) for the field shapes, nesting limits, and the rules around system-managed folders.
|
||||
|
||||
---
|
||||
|
||||
## Skill Studio
|
||||
|
||||
Writing a skill in one window and testing it in another gets old quickly. **Skill Studio** is a dedicated workspace where you author a skill, hand it a piece of input, run it against a real agent, and read the result — without leaving the page.
|
||||
|
||||
Open it from a skill in your library, or go straight to `/skills/studio`. The breadcrumb reads **Skills → Studio → <skill name>** so you always know where you are.
|
||||
|
||||
### The landing page
|
||||
|
||||
Landing on `/skills/studio` without a skill selected gives you two lists to get back to work fast:
|
||||
|
||||
- **Recently visited** — the skills you opened in Studio on this browser.
|
||||
- **Recently updated** — the skills the company changed most recently, each row showing when it was updated and a small avatar of whoever last edited it.
|
||||
|
||||
There's a skill picker in the header (searchable by name, slug, or key) and a **New skill** button. If the company has no skills at all yet, you get an empty state with **Create a new skill** instead.
|
||||
|
||||
### Three panes
|
||||
|
||||
Once a skill is open, Studio splits into three panes. Drag the dividers to resize them; your layout is remembered for next time. On a narrow screen the three panes become tabs instead.
|
||||
|
||||
**Left — the skill itself.** A file tree of everything in the skill folder, with an editor underneath. Markdown files open in the rich editor, and if the file has frontmatter you get a dedicated panel for it above the body so the routing description isn't buried in raw YAML. Everything else opens as plain text.
|
||||
|
||||
**Middle — the input.** This is the text you're going to hand the agent. You can paste something one-off, or pick a saved input to reuse. Saved inputs are organised with `/` in their name, exactly like folders — name one `onboarding/happy-path` and it nests. The **+** button starts a fresh **New input (not saved)**; save it when it turns out to be worth keeping. Collapse the whole pane with the chevron when you want more room, and it folds down to a single **Input folded** strip.
|
||||
|
||||
**Right — test runs.** Pick an agent, press **Run**, and watch the run appear in the history below. Runs refresh themselves while they're in flight and settle on one of **succeeded**, **failed**, or **cancelled**. Click any run to open its detail view with the output, any documents or attachments it produced, and — for a failed run — an error card explaining what went wrong. If the agent asked a question mid-run, you can answer it inline from the run detail rather than going hunting for the task.
|
||||
|
||||
### Editing and saving
|
||||
|
||||
Studio is explicit about unsaved work, because a half-edited skill is not what you want to test against. As soon as you change a file you get an **Unsaved** badge, an **Unsaved edits** chip in the header, and a note reminding you that *"Unsaved edits live only in this Studio session. Save to create the next version before running tests or switching files."* Try to switch files with unsaved edits and Studio asks before discarding them.
|
||||
|
||||
**Save** writes the file and creates the next version of the skill immediately — the header shows the current revision as `v1`, `v2`, and so on, and **Version history** opens the full list so you can look back or restore an earlier one.
|
||||
|
||||
The toolbar above the file tree covers the rest of the housekeeping: **Add file**, **Add folder**, **Delete file**, and **Delete folder**. Two things to know. `SKILL.md` cannot be deleted — the button stays disabled with a tooltip saying so, which makes sense given it's the file agents read first. And adding a folder seeds it with a `README.md`, because a folder with nothing in it has nowhere to show up in the tree.
|
||||
|
||||
The **⋯** menu in the header has **Share link**, which copies the current Studio URL — including which input and which run you're looking at — so a teammate opens exactly the view you're describing.
|
||||
|
||||
### Running a test
|
||||
|
||||
The **Run** button tells you why it's unavailable rather than leaving you guessing. Hover it and you'll get exactly one reason:
|
||||
|
||||
- *"This skill has no files to test"*
|
||||
- *"Pick an agent to run"*
|
||||
- *"Add or paste input text to run"*
|
||||
- *"Save skill edits before running"*
|
||||
|
||||
That last one is the important one — you always test what's actually saved, never a draft that only exists in your browser.
|
||||
|
||||
Under **Advanced** you can choose the **run template**, which is the wrapper Paperclip puts around your input. **Default test template** is the sensible starting point; **No template** *("Run only the input text.")* hands the agent your raw input and nothing else. You can also save your own templates alongside the built-in ones, and edit, duplicate, or delete them. Your choice is remembered per company.
|
||||
|
||||
Test runs happen on a hidden harness task, which keeps them out of your real issue list. Runs are kept for a while and then expire — an expired run still shows its stored snapshot, but the link out to the underlying task is disabled with **Test task expired**.
|
||||
|
||||
### Creating a skill in Studio
|
||||
|
||||
**New skill** opens a create page that walks you through the basics: **Name** and a route-safe **Slug** (derived from the name unless you set your own), a **Tagline**, a **Color** and **Categories** for how the skill appears in the store and the switcher, and a **Sharing** choice of **Company** ("Visible inside this company.") or **Private** ("Only visible in your library."). A public option is shown but marked *Coming later*. Expand **Starter content** if you want to write the opening `SKILL.md` right there. Press **Create skill** and Studio opens the new skill for editing.
|
||||
|
||||
If you started from a folder — say, from inside **My Skills** — the new skill is filed there rather than landing in **Unfiled**.
|
||||
|
||||
---
|
||||
|
||||
## Editing a skill that isn't yours to edit
|
||||
|
||||
Plenty of good skills come from somewhere you don't control: a GitHub repository, skills.sh, the built-in catalog. Those are read-only on purpose, so nobody quietly rewrites a pinned skill under your agents' feet. Open one in Studio and you'll see a **Read-only** badge, an explanation of why, and a **Edit a copy** button.
|
||||
|
||||
**Edit a copy** forks the skill into an editable, Paperclip-managed copy in your library. Before it does anything it runs a precheck and tells you plainly how many agents currently use the original — and offers a switch, on by default, to move those agents over to your copy. That's the decision that actually matters: fork without moving the agents and you've made a copy nobody runs; fork with the switch on and your edits take effect on the next run. The original is left untouched either way.
|
||||
|
||||
Once the copy exists, its Studio header carries a lineage chip reading **Forked from `owner/repo` @ `<short sha>`**, linking back to the skill it came from — so months later it's still obvious where this thing started. And if you already made a copy of this skill and haven't diverged from it, Studio offers to open that existing copy instead of minting another one.
|
||||
|
||||
**Skills imported from a project are a special case.** They're editable, but they aren't a copy — they point at files in your working tree. Studio says so with a persistent notice naming the location and warning that *"Saves write to the project working tree and are not committed."* There's an **Edit a copy instead** link right there if you'd rather not touch the checkout.
|
||||
|
||||
---
|
||||
|
||||
## Who is allowed to change skills
|
||||
|
||||
By default, nothing is locked down: every agent can create, import, install, edit, test, and remove skills, and you won't see any permission chrome in the UI at all. That's deliberate — most companies never need to think about this.
|
||||
|
||||
When you do want to tighten it, the **company skill policy** is where you say which agents may take which skill actions, on which skills, and from which sources. Once a policy is in place and something is actually denied, Paperclip stops being silent about it: the blocked action surfaces a banner naming what was denied and what to do about it, rather than a generic error.
|
||||
|
||||
For the policy document shape, the eight skill actions, and how a decision is reached, see the [Company Skill Policy API](../../reference/api/company-skill-policy.md).
|
||||
|
||||
---
|
||||
|
||||
## File inventory
|
||||
|
||||
Every skill is a folder, not a single file. Expanding a skill in the sidebar reveals its **file inventory** — the full tree of files underneath the skill's root. The inventory pinpoints each file with a few pieces of metadata that matter in practice:
|
||||
@@ -197,7 +314,7 @@ Skills live at the company level, not inside one specific agent. This means:
|
||||
- Your CEO can use a `delegation-checklist` skill that your CMO also uses for their own task breakdown
|
||||
- Updating a skill improves all agents that use it at once
|
||||
|
||||
You can also scan and import skills from project workspaces when you already have skill files on disk. That's useful for teams migrating existing `SKILL.md` folders into the Paperclip library.
|
||||
You can also scan and import skills from project workspaces when you already have skill files on disk. That's useful for teams migrating existing `SKILL.md` folders into the Paperclip library — see [Import skills straight from a project](#5-import-skills-straight-from-a-project).
|
||||
|
||||
---
|
||||
|
||||
@@ -213,6 +330,8 @@ The skills must already be installed at the company level before you can attach
|
||||
|
||||
- [Skills reference](../../reference/skills.md) — everything about how skills work on disk and over the wire.
|
||||
- [Agents API → Skills](../../reference/api/agents.md#skills) — request/response shapes for the agent-level routes.
|
||||
- [Folders API](../../reference/api/folders.md) — creating, nesting, and reordering the skill folder tree.
|
||||
- [Company Skill Policy API](../../reference/api/company-skill-policy.md) — restricting who may create, import, edit, test, or remove skills.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Debug a stuck heartbeat
|
||||
|
||||
Five symptoms that cover almost every "this agent isn't working right" report. Open the agent's detail page and scroll to **Run history** before you start — every diagnosis below begins there.
|
||||
@@ -58,6 +62,14 @@ Five symptoms that cover almost every "this agent isn't working right" report. O
|
||||
|
||||
---
|
||||
|
||||
## 6. Recovery wakes slow down after repeated no-progress runs
|
||||
|
||||
**Symptom.** A recovery-style wake is recorded as skipped with reason `issue_rewake_throttled`, instead of starting another agent session right away.
|
||||
|
||||
**Cause.** Paperclip protects you from an expensive no-op loop. After two successful runs for the same issue make no issue-visible progress, another recovery-style wake waits for an escalating cooldown. The delay starts at two minutes and never exceeds 30 minutes.
|
||||
|
||||
**Fix.** Read the latest run first, then give the agent new information or explicitly resume the work when you are ready to intervene. A new comment, fresh issue activity, and an explicit resume bypass the cooldown. Server-side recovery retries also continue immediately, so this safeguard does not delay a real crash-recovery attempt.
|
||||
|
||||
## Where to look first
|
||||
|
||||
- **Run logs.** Agent detail → **Run history** → click any run for the full transcript and exit code.
|
||||
|
||||
@@ -1,107 +1,40 @@
|
||||
---
|
||||
paperclip_version: v2026.525.0
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# ACPX Local
|
||||
# ACPX Local (retired)
|
||||
|
||||
`acpx_local` runs an agent through the Agent Client Protocol via ACPX on the Paperclip host or a managed execution environment. Use it when you want a single built-in adapter that can target Claude, Codex, or a custom ACP server command.
|
||||
> **This adapter has been retired.** `acpx_local` is no longer a choice when you create an agent. ACP is now a capability of each harness adapter rather than a separate adapter, so pick the adapter that matches your harness and let ACP handle itself:
|
||||
>
|
||||
> [Claude Code](./claude-code.md) · [Codex](./codex.md) · [Gemini CLI](./gemini-cli.md)
|
||||
|
||||
> ⚠ ACPX runtime execution is still being enabled in stages. For today's stable Claude Code or Codex CLI wrapper behaviour, use [Claude Code](./claude-code.md) or [Codex](./codex.md).
|
||||
## What replaced it
|
||||
|
||||
---
|
||||
ACPX used to be the one adapter that could target Claude, Codex, or a custom ACP server. That turned ACP into a separate agent *choice*, when really it is an execution *capability* of the harness you already picked.
|
||||
|
||||
## When To Use
|
||||
Now `claude_local`, `codex_local`, and `gemini_local` each speak ACP natively. Leave `engine` on its default `auto` and you get ACP whenever the host can support it, with an automatic fallback to the CLI lane when it cannot. Set `engine: "acp"` if ACP is required and a missing prerequisite should stop the run outright.
|
||||
|
||||
- The agent should run through Agent Client Protocol via ACPX on the Paperclip host or a managed execution environment.
|
||||
- You want one built-in adapter that can target Claude, Codex, or a custom ACP server command.
|
||||
- You need Paperclip-managed session identity and live streamed ACP events in later ACPX runtime phases.
|
||||
Each of those pages documents the ACP fields — `agentCommand`, `mode`, `nonInteractivePermissions`, `stateDir`, `warmHandleIdleMs` — in its own configuration section.
|
||||
|
||||
## When Not To Use
|
||||
## If you had an ACPX agent
|
||||
|
||||
- You need today's stable Claude Code or Codex CLI wrapper behaviour. Use `claude_local` or `codex_local` until ACPX runtime execution is enabled.
|
||||
- The host cannot satisfy ACPX's `Node >=22.12.0` prerequisite.
|
||||
- The agent runtime is not an ACP server and cannot be launched through ACPX.
|
||||
**You do not need to do anything.** Existing `acpx_local` agents were migrated for you when your instance upgraded. The migration reads each agent's old `agent` field and moves it to the matching adapter:
|
||||
|
||||
---
|
||||
| Old `adapterConfig.agent` | New adapter | New config |
|
||||
|---|---|---|
|
||||
| `codex` | `codex_local` | `engine: "acp"` |
|
||||
| anything else (including unset) | `claude_local` | `engine: "acp"` |
|
||||
|
||||
## Common Fields
|
||||
For Codex agents, the old `effort`, `reasoningEffort`, and `thinkingEffort` fields are consolidated onto a single `modelReasoningEffort` value.
|
||||
|
||||
| Field | Required | Default | Notes |
|
||||
|---|---:|---|---|
|
||||
| `agent` | no | `claude` | `claude`, `codex`, or `custom`. |
|
||||
| `agentCommand` | no | — | Custom ACP command when `agent=custom`, or an override for a built-in ACP agent command. |
|
||||
| `mode` | no | `persistent` | `persistent` or `oneshot`. Paperclip keeps session state persistent and may close the live process between runs. |
|
||||
| `cwd` | no | — | Default absolute working directory fallback for the agent process. |
|
||||
| `permissionMode` | no | `approve-all` | ACPX permission policy. `approve-all` auto-approves ACPX permission requests. |
|
||||
| `nonInteractivePermissions` | no | `deny` | Policy when no operator is available to answer a permission prompt. |
|
||||
| `timeoutSec` | no | `0` | Run timeout in seconds. `0` means no timeout. |
|
||||
| `warmHandleIdleMs` | no | `0` | Idle window before Paperclip releases a warm ACPX handle. `0` keeps the warm handle disabled. |
|
||||
## If you still see `acpx_local` in an error
|
||||
|
||||
The built-in `agent` options are `claude`, `codex`, and `custom`.
|
||||
|
||||
---
|
||||
|
||||
## Execution Details
|
||||
|
||||
- Paperclip launches the configured ACP server via ACPX and keeps a session handle.
|
||||
- In `persistent` mode, Paperclip preserves session state across heartbeats and may keep a warm handle when `warmHandleIdleMs > 0`.
|
||||
- In `oneshot` mode, Paperclip starts a fresh ACPX process for each run.
|
||||
|
||||
### Bare Claude model IDs just work
|
||||
|
||||
When `agent=claude`, you can pass a bare model ID like `claude-sonnet-4-5` or `claude-opus-4-7` directly in `model` — no fully qualified provider prefix required.
|
||||
|
||||
Under the hood, Paperclip sets `ANTHROPIC_MODEL` on the ACP server's environment at startup instead of routing the value through the ACP `set_config_option` call. Some ACP server versions validate the option's value against an internal allowlist and reject bare IDs that don't match an entry exactly. Reading `ANTHROPIC_MODEL` during initialization sidesteps that validator, so your model selection is honoured before the first turn runs.
|
||||
|
||||
### Real diagnostics on failure
|
||||
|
||||
When an ACPX run fails, the adapter now surfaces structured detail on the error instead of an opaque `Internal error` log line. Each failure's `errorMeta` includes:
|
||||
|
||||
- `errorName` — the underlying error class.
|
||||
- `acpCode` — the ACP protocol code (for example `ACP_SESSION_INIT_FAILED`, `ACP_TURN_FAILED`, `ACP_BACKEND_MISSING`, `ACP_BACKEND_UNAVAILABLE`) when the runtime reports one.
|
||||
- `causeMessage` — the inner cause string when the error wraps one.
|
||||
- `retryable` — the runtime's own retryability hint when it sets one.
|
||||
- `stackPreview` — the first six lines of the stack so you can locate the failure without dumping a full trace.
|
||||
- `phase` — which phase of the run failed (`ensure_session`, `configure_session`, or `turn`).
|
||||
|
||||
The adapter also picks a precise `errorCode` from those signals — `acpx_session_init_failed`, `acpx_session_config_failed`, `acpx_turn_failed`, `acpx_backend_missing`, `acpx_backend_unavailable`, `acpx_protocol_error`, `acpx_auth_required`, or `acpx_runtime_error` — so you can route alerts and retries against meaningful classifications.
|
||||
|
||||
### Respects your Claude settings on first run
|
||||
|
||||
When `agent=claude`, the adapter writes a Paperclip-managed `.claude/settings.local.json` into the working directory before launching the ACP server. The Claude Code SDK that `claude-agent-acp` runs reads settings from `user`, `project`, and `local` sources, so this local file takes precedence over your `~/.claude/settings.json`.
|
||||
|
||||
That means a first-run Claude Code ACPX agent no longer strands on permission prompts when your user-level settings.json sets `defaultMode: "dontAsk"` — Paperclip's local file flips `defaultMode` back to `default` (recording `overrodeDontAsk` in the run's command notes), preserves any `defaultMode` you set to a non-`dontAsk` value, and merges in:
|
||||
|
||||
- `permissions.allow` rules for the Paperclip helper scripts inside `cwd/scripts/` plus `Bash(curl:*)` and `Bash(env:*)`.
|
||||
- `permissions.additionalDirectories` entries for the adapter's `stateDir`, the agent's home directory, and the company root under your Paperclip instance directory — so Read tools can reach the paths the agent needs to talk to its own control plane.
|
||||
|
||||
Existing entries you've added to `.claude/settings.local.json` are merged, not replaced.
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Today's ACPX runtime is staged — see the callout at the top of this page. The configuration below is the shape Paperclip will accept once the runtime is enabled.
|
||||
|
||||
```json
|
||||
{
|
||||
"adapterType": "acpx_local",
|
||||
"adapterConfig": {
|
||||
"agent": "claude",
|
||||
"mode": "persistent",
|
||||
"cwd": "/Users/me/projects/paperclip-workspace",
|
||||
"permissionMode": "approve-all",
|
||||
"nonInteractivePermissions": "deny",
|
||||
"timeoutSec": 0,
|
||||
"warmHandleIdleMs": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
Paperclip deliberately keeps the retired adapter registered so that a stale row fails with a clear message instead of silently falling back to the `process` adapter. If a run logs a retirement message naming `acpx_local`, that agent's row predates the migration — open the agent and switch it to `claude_local` or `codex_local` with `engine` set to `acp`.
|
||||
|
||||
## Related
|
||||
|
||||
- [Claude Code](./claude-code.md) — stable Claude Code CLI wrapper.
|
||||
- [Codex](./codex.md) — stable Codex CLI wrapper.
|
||||
- [Creating An Adapter](./creating-an-adapter.md) — author your own adapter when none of the built-ins fit.
|
||||
- [Claude Code](./claude-code.md) — Claude harness, ACP by default.
|
||||
- [Codex](./codex.md) — Codex harness, ACP by default.
|
||||
- [Gemini CLI](./gemini-cli.md) — Gemini harness, ACP by default.
|
||||
- [Adapters Overview](./overview.md) — the full list of adapters you can pick today.
|
||||
- [Creating An Adapter](./creating-an-adapter.md) — author your own when none of the built-ins fit.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
paperclip_version: v2026.529.0
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Claude Code
|
||||
@@ -28,6 +28,7 @@ paperclip_version: v2026.529.0
|
||||
| Field | Required | Notes |
|
||||
|---|---:|---|
|
||||
| `cwd` | no | Absolute working directory for the agent. Recommended in practice. If omitted, the adapter falls back to the current process working directory. Paperclip creates the path when permissions allow. |
|
||||
| `engine` | no | How Claude Code is run: `auto` (the default — ACP preferred), `acp` (always the Agent Client Protocol), or `cli` (always the classic Claude CLI). See [ACP Engine](#acp-engine). |
|
||||
| `model` | no | Claude model id. Common choices include `claude-opus-4-6`, `claude-sonnet-4-6`, and `claude-haiku-4-6`. |
|
||||
| `promptTemplate` | no | Prompt template used for the run. |
|
||||
| `env` | no | Environment variables passed to Claude Code. Secret refs are supported. |
|
||||
@@ -37,7 +38,7 @@ paperclip_version: v2026.529.0
|
||||
| `chrome` | no | Passes `--chrome` when enabled. |
|
||||
| `maxTurnsPerRun` | no | Caps the number of agentic turns in one heartbeat. Defaults to `300`. |
|
||||
| `dangerouslySkipPermissions` | no | Defaults to `true` because Paperclip runs Claude in headless `--print` mode. |
|
||||
| `timeoutSec` | no | Run timeout in seconds. `0` means no timeout. |
|
||||
| `timeoutSec` | no | Run timeout in seconds. On local and SSH targets, `0` means no adapter wall-clock timeout. On a sandbox target, `0` or an unset value uses the 14,400-second sandbox default; use a positive value to override it or a negative value to opt out of the adapter timeout. |
|
||||
| `graceSec` | no | Grace period before a forced stop. |
|
||||
| `workspaceStrategy` | no | Execution workspace strategy, such as `git_worktree`. |
|
||||
| `workspaceRuntime` | no | Reserved workspace runtime metadata. |
|
||||
@@ -46,6 +47,36 @@ paperclip_version: v2026.529.0
|
||||
|
||||
---
|
||||
|
||||
## ACP Engine
|
||||
|
||||
Claude Code can run through one of two engines — ACP or the classic Claude CLI — selected by the `engine` field:
|
||||
|
||||
- **`auto` (default) — ACP preferred.** Paperclip runs Claude through the Agent Client Protocol (ACP) when the host meets the prerequisites, and falls back to the Claude CLI — with diagnostics explaining why — when it can't.
|
||||
- **`acp` — always ACP.** Force the Agent Client Protocol path.
|
||||
- **`cli` — always the Claude CLI.** Force the classic CLI wrapper and skip ACP entirely.
|
||||
|
||||
ACP gives you a richer, structured live transcript: session identity, status with context-window usage, assistant and thinking token deltas, and tool-call updates that fold into a single card as they progress. That extra detail is most useful when you're watching a sandbox run stream in.
|
||||
|
||||
When the engine resolves to ACP (either `acp`, or `auto` on a capable host), these extra fields apply:
|
||||
|
||||
| Field | Default | Notes |
|
||||
|---|---|---|
|
||||
| `agentCommand` | package-local `claude-agent-acp` | Optional override for the Claude ACP server command. |
|
||||
| `mode` | `persistent` | `persistent` keeps ACP session state between runs; `oneshot` starts fresh each run. |
|
||||
| `nonInteractivePermissions` | `deny` | What to do if the ACP agent asks for input outside an interactive session — `deny` the request or `fail` the run. |
|
||||
| `stateDir` | Paperclip-managed | Optional ACP session-state directory. Defaults to Paperclip's company- and agent-scoped storage. |
|
||||
| `warmHandleIdleMs` | `0` | How long to keep the ACP process warm between runs, in milliseconds. `0` closes it after each run while still retaining persistent session state. |
|
||||
|
||||
> **Heads-up:** ACP is where the old standalone `acpx_local` adapter's capabilities now live. That adapter has been retired — pick `claude_local` (or `codex_local` / `gemini_local`) and leave `engine` on `auto` to get ACP by default.
|
||||
|
||||
### ACP in sandbox environments
|
||||
|
||||
You can keep `engine` on `auto` when this agent runs in a Paperclip sandbox environment. If that sandbox provides Paperclip's bidirectional process session, Paperclip keeps the ACP engine and its structured live transcript; you do not add a separate bridge setting to the adapter config.
|
||||
|
||||
An environment that only runs one-shot commands cannot host an ACP session, so `auto` falls back to the Claude CLI with a diagnostic. The same fallback applies to non-sandbox remote targets such as SSH. Choose `engine: "acp"` when ACP is required and a failed prerequisite should stop the run, or `engine: "cli"` when you always want the CLI lane.
|
||||
|
||||
---
|
||||
|
||||
## Model Discovery
|
||||
|
||||
When you pick a model in the agent config form, Claude Code fills the model dropdown from a live query to Anthropic's API instead of a hard-coded list — so a Claude model that shipped after your last Paperclip update still shows up without waiting for a new release.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
paperclip_version: v2026.618.0
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Codex
|
||||
@@ -28,6 +28,7 @@ paperclip_version: v2026.618.0
|
||||
| Field | Required | Notes |
|
||||
|---|---:|---|
|
||||
| `cwd` | no | Absolute working directory for the agent. Recommended in practice. If omitted, the adapter falls back to the current process working directory. Paperclip creates the path when permissions allow. |
|
||||
| `engine` | no | How Codex is run: `auto` (the default — ACP preferred), `acp` (always the Agent Client Protocol), or `cli` (always the classic Codex CLI). See [ACP Engine](#acp-engine). |
|
||||
| `model` | no | Codex model id. See [Models](#models). If you leave it unset, the adapter omits `--model` so the Codex CLI uses its own default. |
|
||||
| `promptTemplate` | no | Prompt template used for the run. |
|
||||
| `instructionsFilePath` | no | Markdown file prepended to the stdin prompt sent to `codex exec`. |
|
||||
@@ -37,7 +38,7 @@ paperclip_version: v2026.618.0
|
||||
| `command` | no | Defaults to `codex`. |
|
||||
| `extraArgs` | no | Extra CLI arguments appended to the Codex invocation. |
|
||||
| `env` | no | Environment variables passed to the runtime. Secret refs are supported. |
|
||||
| `timeoutSec` | no | Run timeout in seconds. `0` means no timeout. |
|
||||
| `timeoutSec` | no | Run timeout in seconds. On local and SSH targets, `0` means no adapter wall-clock timeout. On a sandbox target, `0` or an unset value uses the 14,400-second sandbox default; use a positive value to override it or a negative value to opt out of the adapter timeout. |
|
||||
| `graceSec` | no | Grace period before a forced stop. |
|
||||
| `outputInactivityTimeoutMs` | no | How long the adapter waits for Codex to produce output before treating the run as stuck. The timer resets every time Codex emits a parsed event, so a busy run never trips it. Defaults to 7 minutes (`420000`) when unset. Set it to `null` to switch the monitor off entirely — only do that for tasks you know go quiet for long stretches, since Paperclip's platform-level one-hour silent-run safety net still applies. When it fires, the adapter stops the Codex process and reports the run as failed with a message like `monitor: no codex output for 7m 0s`. |
|
||||
| `workspaceStrategy` | no | Execution workspace strategy, such as `git_worktree`. |
|
||||
@@ -47,13 +48,46 @@ paperclip_version: v2026.618.0
|
||||
|
||||
---
|
||||
|
||||
## ACP Engine
|
||||
|
||||
Codex can run through one of two engines — ACP or the classic Codex CLI — selected by the `engine` field:
|
||||
|
||||
- **`auto` (default) — ACP preferred.** Paperclip runs Codex through the Agent Client Protocol (ACP) when the host meets the prerequisites, and falls back to the Codex CLI — with diagnostics explaining why — when it can't.
|
||||
- **`acp` — always ACP.** Force the Agent Client Protocol path.
|
||||
- **`cli` — always the Codex CLI.** Force the classic CLI wrapper and skip ACP entirely.
|
||||
|
||||
ACP gives you a richer, structured live transcript: session identity, status with context-window usage, assistant and thinking token deltas, and tool-call updates that fold into a single card as they progress. That extra detail is most useful when you're watching a sandbox run stream in.
|
||||
|
||||
When the engine resolves to ACP (either `acp`, or `auto` on a capable host), these extra fields apply:
|
||||
|
||||
| Field | Default | Notes |
|
||||
|---|---|---|
|
||||
| `agentCommand` | package-local `codex-acp` | Optional override for the Codex ACP server command. |
|
||||
| `mode` | `persistent` | `persistent` keeps ACP session state between runs; `oneshot` starts fresh each run. |
|
||||
| `nonInteractivePermissions` | `deny` | What to do if the ACP agent asks for input outside an interactive session — `deny` the request or `fail` the run. |
|
||||
| `stateDir` | Paperclip-managed | Optional ACP session-state directory. Defaults to Paperclip's company- and agent-scoped storage. |
|
||||
| `warmHandleIdleMs` | `0` | How long to keep the ACP process warm between runs, in milliseconds. `0` closes it after each run while still retaining persistent session state. |
|
||||
|
||||
> **Heads-up:** ACP is where the old standalone `acpx_local` adapter's capabilities now live. That adapter has been retired — pick `codex_local` (or `claude_local` / `gemini_local`) and leave `engine` on `auto` to get ACP by default.
|
||||
|
||||
### ACP in sandbox environments
|
||||
|
||||
You can keep `engine` on `auto` when this agent runs in a Paperclip sandbox environment. If that sandbox provides Paperclip's bidirectional process session, Paperclip keeps the ACP engine and its structured live transcript; you do not add a separate bridge setting to the adapter config.
|
||||
|
||||
An environment that only runs one-shot commands cannot host an ACP session, so `auto` falls back to the Codex CLI with a diagnostic. The same fallback applies to non-sandbox remote targets such as SSH. Choose `engine: "acp"` when ACP is required and a failed prerequisite should stop the run, or `engine: "cli"` when you always want the CLI lane.
|
||||
|
||||
---
|
||||
|
||||
## Models
|
||||
|
||||
Pick any of the known Codex model ids in the `model` field. The current options are:
|
||||
|
||||
- `gpt-5.5`
|
||||
- `gpt-5.6` (the adapter default)
|
||||
- `gpt-5.6-sol`
|
||||
- `gpt-5.6-terra`
|
||||
- `gpt-5.6-luna`
|
||||
- `gpt-5.4`
|
||||
- `gpt-5.3-codex` (the adapter default)
|
||||
- `gpt-5.4-mini`
|
||||
- `gpt-5.3-codex-spark`
|
||||
- `gpt-5`
|
||||
- `o3`
|
||||
@@ -173,7 +207,7 @@ Paperclip writes these definitions into a managed region of the per-company mana
|
||||
"adapterType": "codex_local",
|
||||
"adapterConfig": {
|
||||
"cwd": "/Users/me/projects/paperclip-workspace",
|
||||
"model": "gpt-5.3-codex",
|
||||
"model": "gpt-5.6",
|
||||
"instructionsFilePath": "/Users/me/projects/paperclip-workspace/INSTRUCTIONS.md",
|
||||
"modelReasoningEffort": "medium",
|
||||
"search": false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
paperclip_version: v2026.618.0
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Gemini CLI
|
||||
@@ -30,6 +30,7 @@ paperclip_version: v2026.618.0
|
||||
| Field | Required | Notes |
|
||||
|---|---:|---|
|
||||
| `cwd` | no | Absolute working directory for the agent. Recommended in practice. If omitted, the adapter falls back to the current process working directory. Paperclip creates the path when permissions allow. |
|
||||
| `engine` | no | How Gemini is run: `auto` (the default — ACP preferred), `acp` (always the Agent Client Protocol), or `cli` (always the classic Gemini CLI). See [ACP Engine](#acp-engine). |
|
||||
| `model` | no | Gemini model id. Defaults to `auto`. Common choices include `gemini-2.5-pro` and `gemini-2.5-flash`. |
|
||||
| `promptTemplate` | no | Prompt template used for the run. |
|
||||
| `instructionsFilePath` | no | Markdown instructions file prepended to the prompt. |
|
||||
@@ -40,13 +41,43 @@ paperclip_version: v2026.618.0
|
||||
| `extraArgs` | no | Extra CLI arguments appended to the Gemini invocation. |
|
||||
| `env` | no | Environment variables passed to the runtime. Secret refs are supported. |
|
||||
| `helloProbeTimeoutSec` | no | Timeout for the readiness probe. |
|
||||
| `timeoutSec` | no | Run timeout in seconds. `0` means no timeout. |
|
||||
| `timeoutSec` | no | Run timeout in seconds. On local and SSH targets, `0` means no adapter wall-clock timeout. On a sandbox target, `0` or an unset value uses the 14,400-second sandbox default; use a positive value to override it or a negative value to opt out of the adapter timeout. |
|
||||
| `graceSec` | no | Grace period before a forced stop. |
|
||||
|
||||
> **Note:** Gemini CLI uses `--output-format stream-json` for readiness checks and resumes sessions with `--resume` when the stored session cwd still matches the current cwd. It passes your prompt with `--prompt` for non-interactive runs (not through stdin), and it sets a headless-safe terminal and browser environment for the Gemini CLI child process so unattended runs never stall waiting on browser auth or a colour-terminal prompt.
|
||||
|
||||
---
|
||||
|
||||
## ACP Engine
|
||||
|
||||
Gemini can run through one of two engines — ACP or the classic Gemini CLI — selected by the `engine` field:
|
||||
|
||||
- **`auto` (default) — ACP preferred.** Paperclip runs Gemini through the Agent Client Protocol (ACP) when the host meets the prerequisites, and falls back to the Gemini CLI — with diagnostics explaining why — when it can't.
|
||||
- **`acp` — always ACP.** Force the Agent Client Protocol path.
|
||||
- **`cli` — always the Gemini CLI.** Force the classic CLI wrapper and skip ACP entirely.
|
||||
|
||||
ACP gives you a richer, structured live transcript: session identity, status with context-window usage, assistant and thinking token deltas, and tool-call updates that fold into a single card as they progress. That extra detail is most useful when you're watching a sandbox run stream in.
|
||||
|
||||
When the engine resolves to ACP (either `acp`, or `auto` on a capable host), these extra fields apply:
|
||||
|
||||
| Field | Default | Notes |
|
||||
|---|---|---|
|
||||
| `agentCommand` | `gemini --acp` | Optional override for the Gemini ACP server command. |
|
||||
| `mode` | `persistent` | `persistent` keeps ACP session state between runs; `oneshot` starts fresh each run. |
|
||||
| `nonInteractivePermissions` | `deny` | What to do if the ACP agent asks for input outside an interactive session — `deny` the request or `fail` the run. |
|
||||
| `stateDir` | Paperclip-managed | Optional ACP session-state directory. Defaults to Paperclip's company- and agent-scoped storage. |
|
||||
| `warmHandleIdleMs` | `0` | How long to keep the ACP process warm between runs, in milliseconds. `0` closes it after each run while still retaining persistent session state. |
|
||||
|
||||
> **Heads-up:** ACP is where the old standalone `acpx_local` adapter's capabilities now live. That adapter has been retired — pick `gemini_local` (or `claude_local` / `codex_local`) and leave `engine` on `auto` to get ACP by default.
|
||||
|
||||
### ACP in sandbox environments
|
||||
|
||||
You can keep `engine` on `auto` when this agent runs in a Paperclip sandbox environment. If that sandbox provides Paperclip's bidirectional process session, Paperclip keeps the ACP engine and its structured live transcript; you do not add a separate bridge setting to the adapter config.
|
||||
|
||||
An environment that only runs one-shot commands cannot host an ACP session, so `auto` falls back to the Gemini CLI with a diagnostic. The same fallback applies to non-sandbox remote targets such as SSH. Choose `engine: "acp"` when ACP is required and a failed prerequisite should stop the run, or `engine: "cli"` when you always want the CLI lane.
|
||||
|
||||
---
|
||||
|
||||
## Session Persistence
|
||||
|
||||
The adapter stores Gemini session ids between heartbeats and resumes them on the next wake.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Adapters Overview
|
||||
|
||||
Adapters connect Paperclip's control plane to the runtime that actually does the work. Use this section when you need to choose an adapter, understand what Paperclip expects from one, or build a new adapter package.
|
||||
@@ -24,7 +28,6 @@ Every adapter is responsible for the same core jobs:
|
||||
|---|---|
|
||||
| Claude Code on your machine | [Claude Code](./claude-code.md) |
|
||||
| OpenAI Codex CLI on your machine | [Codex](./codex.md) |
|
||||
| Claude, Codex, or a custom ACP agent through ACPX | [ACPX Local](./acpx-local.md) |
|
||||
| Gemini CLI on your machine | [Gemini CLI](./gemini-cli.md) |
|
||||
| Cursor Agent CLI on your machine | [Cursor Local](./cursor-local.md) |
|
||||
| OpenCode CLI with provider/model routing | [OpenCode](./opencode.md) |
|
||||
@@ -55,7 +58,6 @@ These adapters ship with Paperclip and are always available in the host:
|
||||
|---|---|---|---|
|
||||
| [Claude Code](./claude-code.md) | `claude_local` | Selectable (recommended) | Claude Code runs with session persistence, skills sync, and structured transcript parsing. |
|
||||
| [Codex](./codex.md) | `codex_local` | Selectable (recommended) | Codex CLI runs with session persistence and managed `CODEX_HOME`. |
|
||||
| [ACPX Local](./acpx-local.md) | `acpx_local` | Selectable (staged rollout) | Claude, Codex, or a custom ACP agent through ACPX with live structured event streaming. |
|
||||
| [Gemini CLI](./gemini-cli.md) | `gemini_local` | Selectable | Gemini CLI runs with resume support and local skills sync. |
|
||||
| [Cursor Local](./cursor-local.md) | `cursor` | Selectable | Cursor Agent CLI runs with `--resume` session continuity and structured stream output. |
|
||||
| [OpenCode](./opencode.md) | `opencode_local` | Selectable | OpenCode CLI runs with provider/model routing and `--session` resume. |
|
||||
@@ -107,11 +109,11 @@ Every adapter streams its stdout to the run log, and Paperclip renders it live a
|
||||
|
||||
Here are the rough tiers, richest first:
|
||||
|
||||
- **`acpx_local` — full structured event stream.** ACPX emits a JSONL event for each meaningful moment: session identity, status (progress text plus context-window usage), assistant and thinking token deltas, tool-call title and status updates as calls progress, a result stop-reason summary, and errors. The transcript renders these as live-updating message, thinking, tool, and status blocks — and repeated tool-call status updates fold into a single tool card instead of stacking.
|
||||
- **CLI wrappers (`claude_local`, `codex_local`, `cursor`, `opencode_local`, …) — the CLI's own stream.** These parse each CLI's streaming JSON output: assistant text, tool calls and results, and a final usage/cost summary. You get as much detail as the CLI itself prints.
|
||||
- **ACP engine — full structured event stream.** When `claude_local`, `codex_local`, or `gemini_local` runs through the Agent Client Protocol — the default `engine: auto`, or a forced `engine: acp` — it emits a JSONL event for each meaningful moment: session identity, status (progress text plus context-window usage), assistant and thinking token deltas, tool-call title and status updates as calls progress, a result stop-reason summary, and errors. The transcript renders these as live-updating message, thinking, tool, and status blocks — and repeated tool-call status updates fold into a single tool card instead of stacking.
|
||||
- **CLI wrappers (`claude_local` / `codex_local` / `gemini_local` on `engine: cli`, plus `cursor`, `opencode_local`, …) — the CLI's own stream.** These parse each CLI's streaming JSON output: assistant text, tool calls and results, and a final usage/cost summary. You get as much detail as the CLI itself prints.
|
||||
- **Generic adapters (`process`, `http`) — plain output.** You see stdout and stderr lines with no structured transcript.
|
||||
|
||||
If you're running sandbox workers, prefer `acpx_local`. Sandbox run logs stream live, so the richer the event stream, the more useful the live transcript and status line are while a remote run is still in flight.
|
||||
If you're running sandbox workers, leave `engine` on `auto` so the adapter uses ACP when the sandbox provides Paperclip's bidirectional process session. A sandbox that only runs one-shot commands, and non-sandbox remote targets, fall back to the CLI lane. Sandbox run logs stream live either way, but the richer event stream makes the transcript and status line more useful while a remote run is in flight.
|
||||
|
||||
---
|
||||
|
||||
@@ -119,7 +121,6 @@ If you're running sandbox workers, prefer `acpx_local`. Sandbox run logs stream
|
||||
|
||||
- [Claude Code](./claude-code.md)
|
||||
- [Codex](./codex.md)
|
||||
- [ACPX Local](./acpx-local.md)
|
||||
- [Gemini CLI](./gemini-cli.md)
|
||||
- [Cursor Local](./cursor-local.md)
|
||||
- [OpenCode](./opencode.md)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
paperclip_version: v2026.618.0
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Sandbox Providers
|
||||
@@ -204,7 +204,7 @@ Common optional fields:
|
||||
| Field | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `backend` | `"sandbox-cr"` | `sandbox-cr` (alpha, requires the agent-sandbox controller) or `job` (stable, one-shot entrypoint). |
|
||||
| `adapterType` | `"claude_local"` | One of the supported adapter types (`claude_local`, `codex_local`, `gemini_local`, `cursor_local`, `opencode_local`, `acpx_local`, `pi_local`). Determines the runtime image, env keys, and egress allow-list. |
|
||||
| `adapterType` | `"claude_local"` | One of the supported adapter types (`claude_local`, `codex_local`, `gemini_local`, `cursor_local`, `opencode_local`, `pi_local`). Determines the runtime image, env keys, and egress allow-list. |
|
||||
| `namespacePrefix` | `"paperclip-"` | Prefix for the per-company tenant namespace. |
|
||||
| `companySlug` | derived from companyId | Override the auto-derived company slug. |
|
||||
| `imageRegistry` | (none) | Override the default registry for agent runtime images. |
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Attention
|
||||
|
||||
Read the board's current decision queue. This is the feed behind the **Decisions** screen: a single, ranked view of work that needs a person to act, from approvals and questions to blocked work, failed runs, budget alerts, and agent errors.
|
||||
|
||||
Use this API when you are building an operator view or need to give a board user a concise, current list of what needs their attention. It is a read-only feed; the action a person takes still belongs to the underlying approval, interaction, issue, or alert.
|
||||
|
||||
If you have not read it yet, start with the [API Overview](./overview.md) for the base URL, authentication, and company-scoping rules.
|
||||
|
||||
---
|
||||
|
||||
## Read the queue
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/attention
|
||||
```
|
||||
|
||||
The route requires company access and board access. A board actor also needs a user context; otherwise it returns `403` with `"Board user context required"`.
|
||||
|
||||
By default, the feed leaves out items that the current user has dismissed or snoozed. Add `includeDismissed=true` when you are rendering those hidden rows as well.
|
||||
|
||||
```bash
|
||||
curl -s \
|
||||
"http://localhost:3100/api/companies/{companyId}/attention?includeDismissed=true" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
The response is an `AttentionFeed` with these top-level fields:
|
||||
|
||||
| Field | What you get |
|
||||
|---|---|
|
||||
| `companyId` | The company that owns the feed. |
|
||||
| `generatedAt` | When Paperclip generated this view. |
|
||||
| `totalCount` | The number of items included in this response. |
|
||||
| `countsBySourceKind` | Counts grouped by each attention source. |
|
||||
| `items` | The ranked `AttentionItem` records. |
|
||||
|
||||
Each item tells you what needs attention (`whyNow`), the subject to open (`subject`), suggested actions (`decisionVerbs`), its `severity`, and its `activityAt` time. It also carries an `entryRule` and `exitRule` so an operator can understand why it appeared and what clears it. When available, `relatedIssue`, `project`, `workspace`, and `detail` add context without another lookup.
|
||||
|
||||
### Sources and severity
|
||||
|
||||
`sourceKind` is one of:
|
||||
|
||||
- `approval`
|
||||
- `issue_thread_interaction`
|
||||
- `join_request`
|
||||
- `recovery_action`
|
||||
- `productivity_review`
|
||||
- `blocker_attention`
|
||||
- `review`
|
||||
- `failed_run`
|
||||
- `budget_alert`
|
||||
- `agent_error_alert`
|
||||
|
||||
`severity` is one of `critical`, `high`, `medium`, or `low`.
|
||||
|
||||
Paperclip deduplicates the underlying signals, then ranks the feed by recent activity, severity, source priority, and a stable deduplication key. Treat it as a decision queue, not as a complete history of every event in your company.
|
||||
|
||||
---
|
||||
|
||||
## Dismiss or snooze an item
|
||||
|
||||
The attention endpoint does not mutate the queue. Dismissal state belongs to the current board user and uses the inbox-dismissal routes instead.
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/inbox-dismissals
|
||||
POST /api/companies/{companyId}/inbox-dismissals
|
||||
DELETE /api/companies/{companyId}/inbox-dismissals/{itemKey}
|
||||
```
|
||||
|
||||
For an attention item, pass its `dismissalKey`, which begins with `attention:`. A `POST` body has `itemKey`, an optional `kind` of `dismiss` or `snooze`, and `snoozedUntil` when you snooze. `snoozedUntil` must be a future ISO timestamp; it must be absent for a dismissal. A successful create returns `201`, and deleting the same item key restores it with `204`.
|
||||
|
||||
```json
|
||||
{
|
||||
"itemKey": "attention:<attention-dismissal-key>",
|
||||
"kind": "snooze",
|
||||
"snoozedUntil": "2026-07-13T09:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
These routes require board authentication and a board user context. Invalid keys return `"Unsupported inbox item key"`; an invalid snooze time returns `"snoozedUntil must be an ISO timestamp"` or `"snoozedUntil must be in the future"`.
|
||||
|
||||
---
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [Decisions](../../guides/day-to-day/decisions.md) — use the built-in operator workflow.
|
||||
- [Approvals](./approvals.md) — act on approval records in the queue.
|
||||
- [Issues](./issues.md) — work with issue-thread interactions and the issues behind many attention items.
|
||||
@@ -0,0 +1,241 @@
|
||||
---
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Built-in Agents
|
||||
|
||||
First-party agents that Paperclip ships with, ready to provision into any company.
|
||||
|
||||
A built-in agent is a normal agent that Paperclip knows about by name. Instead of hiring it from scratch and wiring up its role, purpose, and instructions yourself, you provision it from a small registry that the server maintains. Each built-in has a stable `key` — like `briefs` or `learning` — and Paperclip resolves it through that key rather than a hardcoded database ID, so backend features can rely on "the Briefs agent for this company" without anyone pinning an ID.
|
||||
|
||||
Use this API when you want to turn on one of Paperclip's shipped agents for a company, check whether it's set up, point it at an adapter, or (for bundled built-ins like the Reflection Coach) control its schedule.
|
||||
|
||||
If you have not read it yet, start with the [API Overview](./overview.md) for the base URL, authentication, and company-scoping rules. Everything here builds on those conventions.
|
||||
|
||||
---
|
||||
|
||||
## What Makes a Built-in Different
|
||||
|
||||
Under the hood a built-in agent is an ordinary row in the same `agents` table as any agent you hire. What sets it apart is a small, immutable marker stored on the agent's `metadata` as `metadata.paperclipBuiltInAgent`. The marker records the registry `key` and the feature keys the agent backs. That marker is how Paperclip finds the agent later, and it is also protected: you cannot forge, move, or strip it through the normal agent create and update routes. Only the built-in service is allowed to write it.
|
||||
|
||||
Because they are registry-owned, built-ins come with sensible defaults baked in — a display name, a short purpose, a default role, and the set of adapter types they are allowed to run on. You bring the last mile: which adapter to use and how to configure it.
|
||||
|
||||
---
|
||||
|
||||
## The Shipped Built-ins
|
||||
|
||||
Three built-in agents ship with Paperclip today. Each row below comes straight from the registry.
|
||||
|
||||
| Key | Display name | What it does | Default role | Allowed adapter types |
|
||||
|---|---|---|---|---|
|
||||
| `briefs` | Briefs Agent | Prepares concise operational briefs for the board and agent company. | `general` | `codex_local`, `claude_local`, `gemini_local`, `opencode_local`, `process` |
|
||||
| `learning` | Learning Agent | Maintains reusable company learning from completed work and recurring patterns. | `general` | `codex_local`, `claude_local`, `gemini_local`, `opencode_local`, `process` |
|
||||
| `reflection-coach` | Reflection Coach | Runs evidence-backed reflection loops on recent agent work, proposes small instruction and skill improvements, and requests approval before changes are applied. | `general` | `claude_local`, `codex_local`, `gemini_local`, `opencode_local`, `process` |
|
||||
|
||||
The first adapter type in each list is the default the server picks if you provision without naming one. The Reflection Coach is a little different from the other two — it ships as a **bundle** and starts life paused. More on that [below](#the-reflection-coach).
|
||||
|
||||
---
|
||||
|
||||
## Lifecycle States
|
||||
|
||||
Every built-in has a state, computed per company and per key from the marked agent row. When you list built-ins or read one's status, you get one of these back:
|
||||
|
||||
| State | What it means for you |
|
||||
|---|---|
|
||||
| `not_provisioned` | No built-in agent exists for this company and key yet. Provision it to get started. |
|
||||
| `pending_approval` | You asked for the built-in, but the company requires board approval for new agents, so it is waiting on an approval decision before it goes live. |
|
||||
| `needs_setup` | The agent exists, but its adapter config is incomplete — for example, no model or command yet. It will not run until you finish the setup. |
|
||||
| `ready` | Adapter config is complete and the agent is not paused. It is available to do work. |
|
||||
| `paused` | The agent is paused. Scheduled or background work should skip it rather than queue work against it. |
|
||||
|
||||
Backend features that need a built-in call an internal helper before they schedule work. If the agent is missing or still `needs_setup`, that helper returns **HTTP 412** with `code: "built_in_agent_not_configured"` — treat this as an operator setup task, not a server bug. If the agent is `paused`, the feature instead gets a `built_in_agent_paused` warning alongside the agent, so it can log the skip and move on without treating the agent as ready.
|
||||
|
||||
---
|
||||
|
||||
## Provisioning and Permissions
|
||||
|
||||
Provisioning a built-in does **not** go through the full agent hire flow. There is no separate hire-approval permission to obtain — the endpoints are gated on the ordinary `agents:create` permission, the same one you need to create any agent. Built-ins are registry-owned system capacity, so Paperclip treats standing one up as configuration rather than a bespoke hire.
|
||||
|
||||
There is one wrinkle worth knowing: if your company has "require board approval for new agents" turned on, provisioning still routes the new built-in through a normal pending approval. In that case the provision endpoint returns `202 Accepted` and the built-in sits in `pending_approval` until the board decides. When board approval is not required, the built-in is provisioned immediately and comes back `200 OK`.
|
||||
|
||||
> **Note:** Built-in agents are behind an instance feature flag. If they are not enabled for your instance, every route on this page returns `404 Not Found` with the message "Built-in agents are not enabled."
|
||||
|
||||
The routine controls (enable, disable, run) have a stricter gate: they require a **board operator** with the `tasks:assign` permission, since they schedule and trigger real work.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
Every route is company-scoped and lives under `/api`. Throughout, `{key}` is a registry key such as `briefs`, `learning`, or `reflection-coach`.
|
||||
|
||||
### List Built-ins
|
||||
|
||||
`GET /api/companies/{companyId}/built-in-agents`
|
||||
|
||||
Returns every registry definition together with its current state for this company. This is the endpoint to render an operator's "built-in agents" panel. Adapter config and runtime config are redacted from the list view.
|
||||
|
||||
<!-- tabs: cURL, JavaScript, Python -->
|
||||
|
||||
<!-- tab: cURL -->
|
||||
```bash
|
||||
curl -s \
|
||||
"http://localhost:3100/api/companies/{companyId}/built-in-agents" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
<!-- tab: JavaScript -->
|
||||
```js
|
||||
const res = await fetch(
|
||||
`http://localhost:3100/api/companies/${companyId}/built-in-agents`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
},
|
||||
);
|
||||
|
||||
const builtIns = await res.json();
|
||||
```
|
||||
<!-- tab: Python -->
|
||||
```python
|
||||
import requests
|
||||
|
||||
res = requests.get(
|
||||
f"http://localhost:3100/api/companies/{companyId}/built-in-agents",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
built_ins = res.json()
|
||||
```
|
||||
<!-- /tabs -->
|
||||
|
||||
### Get Status
|
||||
|
||||
`GET /api/companies/{companyId}/built-in-agents/{key}/status`
|
||||
|
||||
Returns the current state for a single built-in. Use it after a provision or reset to confirm where the agent landed.
|
||||
|
||||
### Provision
|
||||
|
||||
`POST /api/companies/{companyId}/built-in-agents/{key}/provision`
|
||||
|
||||
Creates the built-in for the company, or updates an existing one, and points it at an adapter. Requires `agents:create`.
|
||||
|
||||
The body is optional — provision with an empty body to accept the registry defaults, or supply any of these fields:
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `adapterType` | string | Must be one of the built-in's allowed adapter types. Defaults to the first allowed type. |
|
||||
| `adapterConfig` | object | Adapter-specific config, such as a `model`, `command`, or `url` depending on the adapter type. |
|
||||
| `budgetMonthlyCents` | integer | Monthly budget in cents. Defaults to the registry default (currently `0` for all shipped built-ins). |
|
||||
|
||||
Returns `200 OK` when the built-in is provisioned directly, or `202 Accepted` when the company requires board approval and the request created a pending approval instead.
|
||||
|
||||
<!-- tabs: cURL, JavaScript, Python -->
|
||||
|
||||
<!-- tab: cURL -->
|
||||
```bash
|
||||
curl -s -X POST \
|
||||
"http://localhost:3100/api/companies/{companyId}/built-in-agents/briefs/provision" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"adapterType": "claude_local",
|
||||
"adapterConfig": {
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"cwd": "/Users/me/projects/company"
|
||||
}
|
||||
}'
|
||||
```
|
||||
<!-- tab: JavaScript -->
|
||||
```js
|
||||
const res = await fetch(
|
||||
`http://localhost:3100/api/companies/${companyId}/built-in-agents/briefs/provision`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
adapterType: "claude_local",
|
||||
adapterConfig: {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
cwd: "/Users/me/projects/company",
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const state = await res.json();
|
||||
```
|
||||
<!-- tab: Python -->
|
||||
```python
|
||||
import requests
|
||||
|
||||
res = requests.post(
|
||||
f"http://localhost:3100/api/companies/{companyId}/built-in-agents/briefs/provision",
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"adapterType": "claude_local",
|
||||
"adapterConfig": {
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"cwd": "/Users/me/projects/company",
|
||||
},
|
||||
},
|
||||
)
|
||||
state = res.json()
|
||||
```
|
||||
<!-- /tabs -->
|
||||
|
||||
### Reconcile
|
||||
|
||||
`POST /api/companies/{companyId}/built-in-agents/{key}/reconcile`
|
||||
|
||||
Ensures the built-in exists and its registry-managed pieces are materialized, without changing the adapter you configured. Send an empty body. This is the quiet, idempotent way to make sure a built-in (and, for bundled built-ins, its instructions, skill, and routine) is in place. Requires `agents:create`.
|
||||
|
||||
### Reset
|
||||
|
||||
`POST /api/companies/{companyId}/built-in-agents/{key}/reset`
|
||||
|
||||
Restores the registry-owned defaults on the built-in — its display name, purpose, role, and any bundled instructions, skill, or routine — while **preserving your adapter setup**, so you do not lose the local model or command you configured. Requires `agents:create`.
|
||||
|
||||
The body is optional. To reset everything, send an empty body. To reset only part of a bundle, pass a `resources` array:
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `resources` | array | Any of `"agent"`, `"instructions"`, `"skill"`, `"routine"`. Omit to reset all of them. |
|
||||
|
||||
### Routine Controls (bundled built-ins)
|
||||
|
||||
Bundled built-ins ship a scheduled routine that starts disabled. These routes turn it on or off and let you kick off a one-off run. They require a board operator holding `tasks:assign`. In each path, `{routineKey}` is the routine's key — for the Reflection Coach that is `recent-agent-reflection`.
|
||||
|
||||
`POST /api/companies/{companyId}/built-in-agents/{key}/routines/{routineKey}/enable`
|
||||
`POST /api/companies/{companyId}/built-in-agents/{key}/routines/{routineKey}/disable`
|
||||
`POST /api/companies/{companyId}/built-in-agents/{key}/routines/{routineKey}/run`
|
||||
|
||||
- **enable** activates the routine and its schedule (resuming the agent if it was paused).
|
||||
- **disable** pauses the schedule and the routine.
|
||||
- **run** triggers a single run right now, outside the schedule. It returns `202 Accepted`.
|
||||
|
||||
---
|
||||
|
||||
## The Reflection Coach
|
||||
|
||||
The Reflection Coach is worth a closer look because it is both a built-in agent **and** a bundled skill. When you provision it, Paperclip materializes three things for the company: the agent's instructions, the `reflection-coach` skill, and a weekly `recent-agent-reflection` routine (disabled until you enable it). It also starts **paused** by default, so it never runs until you deliberately configure and enable it.
|
||||
|
||||
Its job is coaching. On each run it reviews another agent's recent work and proposes the single smallest durable improvement to that agent's instructions or skills. The key design point: it never hot-swaps anything mid-run. Every change is a **reviewed, gated proposal** — the coach must show a diff, wait for an accepted task interaction, and apply the change only in a separate follow-up run. There is no same-run edit.
|
||||
|
||||
That gating is enforced through permissions. The Reflection Coach is granted two review-only keys rather than direct-write ones:
|
||||
|
||||
- `agents:suggest-changes` — propose a change to an agent's setup for review, without applying it directly.
|
||||
- `skills:suggest-changes` — propose a change to a company skill for review, without applying it directly.
|
||||
|
||||
These are the softer counterparts to the direct `agents:configure` and `skills:create` permissions. Because the coach holds only the "suggest" keys, its proposals always flow through review before anything takes effect. The full model — including which roles hold which keys — lives on the [Roles and Permissions](../../administration/roles-and-permissions.md) page.
|
||||
|
||||
---
|
||||
|
||||
## Where to Go Next
|
||||
|
||||
- [Agents API](./agents.md) — the full agent lifecycle, adapters, and configuration that built-ins are built on.
|
||||
- [Roles and Permissions](../../administration/roles-and-permissions.md) — the `agents:create`, `tasks:assign`, and `*:suggest-changes` keys referenced here.
|
||||
- [Skills reference](../skills.md) — how bundled skills like the Reflection Coach's are packaged, imported, and versioned.
|
||||
@@ -0,0 +1,387 @@
|
||||
---
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Cases
|
||||
|
||||
A **case** is a first-class work object for the messy, document-heavy work that doesn't fit neatly into a single issue — a research write-up, an investigation, a proposal, a piece of legal or compliance review. Where an issue tracks a unit of work through a status pipeline, a case is a container: it has a type you define, a title and summary, arbitrary structured `fields`, attached documents (with full revision history, locking, and inline annotation threads), file attachments, labels, an event timeline, and typed links back to the issues that spawned or serviced it. Cases can also nest, so a parent case can gather a family of related child cases.
|
||||
|
||||
Cases are company-scoped, and — like the rest of the control-plane API — company access is enforced on every request. If you are new to the API, read the [API Overview](./overview.md) first for base URL, authentication, and error-code conventions; this page builds on those and won't repeat them.
|
||||
|
||||
> **Experimental — off by default.** Cases are gated behind an instance experimental setting. Until an operator turns on `enableCases`, **every** endpoint on this page responds `403 Forbidden` with `{ "error": "Cases are disabled" }`. Because the feature is still experimental, treat the request and response shapes below as subject to change between releases.
|
||||
|
||||
---
|
||||
|
||||
## Enabling Cases
|
||||
|
||||
Every case route begins by asserting that the feature is enabled. The server reads the instance's experimental settings, and if `enableCases` is not turned on it stops immediately with:
|
||||
|
||||
```json
|
||||
{ "error": "Cases are disabled" }
|
||||
```
|
||||
|
||||
returned as `403 Forbidden`. Enable Cases through the instance experimental settings before calling anything here — it is an instance-wide switch, not a per-company one.
|
||||
|
||||
---
|
||||
|
||||
## How cases are identified
|
||||
|
||||
Every case belongs to exactly one company and carries two forms of identity assigned by the server:
|
||||
|
||||
- `caseNumber` — a per-company sequential integer, unique within the company.
|
||||
- `identifier` — a human-readable handle built from the company's issue prefix, such as `PAP-C1`, `PAP-C2`, and so on. (The prefix is shared with issues; cases add a `C` before the number.)
|
||||
|
||||
On the single-case routes, `{id}` accepts either the case UUID or that `identifier` — the server resolves the identifier (case-insensitively) before handling the request, the same way issue routes do.
|
||||
|
||||
A case also has these core fields:
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `caseType` | A free-form string you choose, 1–120 characters — for example `investigation`, `proposal`, or `legal_review`. Cases are grouped and filtered by type. |
|
||||
| `key` | Optional stable key within a `caseType`. The pair `(caseType, key)` is unique per company and drives the upsert behavior on create (see below). |
|
||||
| `title` | Required, 1–500 characters. |
|
||||
| `summary` | Optional free text, up to 8,000 characters. |
|
||||
| `status` | One of `draft`, `in_progress`, `in_review`, `approved`, `done`, `cancelled`. Defaults to `draft`. |
|
||||
| `fields` | An arbitrary JSON object for your own structured data. Defaults to `{}`. |
|
||||
| `projectId` | Optional project the case belongs to. Must belong to the same company. |
|
||||
| `parentCaseId` | Optional parent case, so cases can nest. Must belong to the same company, and a case cannot be its own parent. |
|
||||
| `completedAt` | Set by the server when the status becomes `done` or `cancelled`, and cleared otherwise. |
|
||||
|
||||
### Status values
|
||||
|
||||
| Status | Typical meaning | Terminal? |
|
||||
|---|---|---|
|
||||
| `draft` | Just created, still being shaped. | No |
|
||||
| `in_progress` | Actively being worked. | No |
|
||||
| `in_review` | Paused for review or sign-off. | No |
|
||||
| `approved` | Reviewed and accepted, not yet closed out. | No |
|
||||
| `done` | Complete. Sets `completedAt`. | Yes |
|
||||
| `cancelled` | Abandoned. Sets `completedAt`. | Yes |
|
||||
|
||||
---
|
||||
|
||||
## List Cases
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/cases
|
||||
```
|
||||
|
||||
Return the company's cases, newest activity first.
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Param | Description |
|
||||
|---|---|
|
||||
| `type` / `types` | Filter by one case type, or several. `types` accepts a comma-separated list or repeated params. |
|
||||
| `status` / `statuses` | Filter by one status or a list. The special value `status=active` returns everything that is **not** `done` or `cancelled`. |
|
||||
| `project` / `projectId` / `projectIds` | Filter by one or more projects. |
|
||||
| `includeNoProject` | When `true` (or `1`), also include cases with no project. Combine with `projectIds` to union both sets. |
|
||||
| `label` / `labelId` | Filter to cases carrying a given label. |
|
||||
| `parent` | Filter to the direct children of a given parent case (UUID). |
|
||||
| `q` | Case-insensitive search across `identifier`, `title`, `summary`, and `key`. |
|
||||
| `includeAncestors` | When `true`, also return the parent chain of every matched case. Each returned row carries `matchesListFilters` — `true` for rows that matched your filters, `false` for ancestors pulled in only for context. |
|
||||
| `limit` | Positive integer, 1–200. Defaults to `100`. |
|
||||
|
||||
---
|
||||
|
||||
## Get Case
|
||||
|
||||
```
|
||||
GET /api/cases/{id}
|
||||
```
|
||||
|
||||
Return the full case detail. On top of the core fields, the response folds in the related objects you need to render a case page:
|
||||
|
||||
- `parent` — a compact summary of the parent case, if any.
|
||||
- `labels` — the attached labels.
|
||||
- `issueLinks` — the linked issues, each with its `role` and a compact issue summary (`id`, `identifier`, `title`, `status`).
|
||||
- `documents` — the case's keyed documents, each as `{ key, document }`.
|
||||
- `attachments` — uploaded files, each with its stored asset.
|
||||
|
||||
---
|
||||
|
||||
## Create or Update a Case
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/cases
|
||||
```
|
||||
|
||||
This route is an **upsert** keyed on `(caseType, key)` within the company. If no case exists yet for that pair the server creates one and responds `201 Created`; if one already exists it updates it in place and responds `200 OK`. (A `null` key is a valid key — there can be one keyless case per `caseType`.)
|
||||
|
||||
Request body:
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `caseType` | yes | 1–120 characters. |
|
||||
| `title` | yes | 1–500 characters. |
|
||||
| `key` | no | Stable key within the type; `null` is allowed. |
|
||||
| `summary` | no | Up to 8,000 characters, or `null`. |
|
||||
| `status` | no | Defaults to `draft` on create. |
|
||||
| `fields` | no | Arbitrary JSON object. |
|
||||
| `projectId` | no | Must belong to the company, or `null`. |
|
||||
| `parentCaseId` | no | Must belong to the company, or `null`. |
|
||||
|
||||
Unknown fields are rejected. A `projectId` that doesn't belong to the company, or a `parentCaseId` that doesn't (or that points at the case itself), comes back as `422 Unprocessable Entity` — for example `{ "error": "Project does not belong to company" }` or `{ "error": "A case cannot be its own parent" }`.
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://paperclip.example.com/api/companies/{companyId}/cases" \
|
||||
-d '{
|
||||
"caseType": "investigation",
|
||||
"key": "incident-4821",
|
||||
"title": "Latency spike on checkout",
|
||||
"summary": "Investigate the p99 regression seen on 2026-07-10.",
|
||||
"status": "in_progress",
|
||||
"fields": { "severity": "high" }
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Update a Case
|
||||
|
||||
```
|
||||
PATCH /api/cases/{id}
|
||||
```
|
||||
|
||||
Apply a partial update to an existing case. Accepts any of `projectId`, `title`, `summary`, `status`, `fields`, and `parentCaseId`, plus one of:
|
||||
|
||||
- `labels` / `labelIds` — an array of label UUIDs (up to 100) that **replaces** the case's current label set. Send `[]` to clear all labels.
|
||||
|
||||
Notes:
|
||||
|
||||
- Unknown fields are rejected.
|
||||
- Setting `status` to `done` or `cancelled` stamps `completedAt`; any other status clears it.
|
||||
- `projectId`, `parentCaseId`, and every label id are validated against the company; a mismatch returns `422 Unprocessable Entity` (e.g. `{ "error": "One or more labels do not belong to company" }`).
|
||||
- The update records a matching event on the case timeline — `status_changed`, `fields_changed`, `child_linked`, `label_added` / `label_removed`, or a generic `updated`.
|
||||
|
||||
---
|
||||
|
||||
## Linking issues to a case
|
||||
|
||||
Cases and issues are joined by typed links. A link carries a **role** that says why the issue is attached:
|
||||
|
||||
| Role | Meaning |
|
||||
|---|---|
|
||||
| `origin` | The issue this case originated from. |
|
||||
| `work` | An issue doing the work for this case. |
|
||||
| `reference` | A related issue, linked for context. |
|
||||
|
||||
### Link an Issue
|
||||
|
||||
```
|
||||
POST /api/cases/{id}/links
|
||||
```
|
||||
|
||||
Request body:
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `issueId` | yes | UUID of an issue in the same company. |
|
||||
| `role` | yes | One of `origin`, `work`, `reference`. |
|
||||
|
||||
An issue can be linked to a case only once — re-posting the same `(case, issue)` pair returns the existing link rather than creating a duplicate. An issue that belongs to another company is rejected with `422 Unprocessable Entity` (`{ "error": "Issue does not belong to case company" }`). A successful new link responds `201 Created` and records an `issue_linked` event.
|
||||
|
||||
### List the cases linked to an issue
|
||||
|
||||
```
|
||||
GET /api/issues/{issueId}/cases
|
||||
```
|
||||
|
||||
The inverse lookup: given an issue (by UUID or identifier such as `PAP-39`), return every case linked to it. Each item carries the link `id`, `role`, `createdAt`, and a compact `case` summary (`id`, `identifier`, `title`, `caseType`, `status`).
|
||||
|
||||
### Automatic run linking
|
||||
|
||||
When an agent run acts on a case, the server links the run's issue to the case automatically so the work stays traceable. Creating a case auto-links the originating issue with role `origin`; updating a case, revising one of its documents, restoring a revision, or uploading an attachment auto-links the run's issue with role `work`. Auto-links are recorded as `issue_linked` events with `autoLinked: true` in the payload, and are skipped when the run has no associated issue.
|
||||
|
||||
---
|
||||
|
||||
## Case Documents
|
||||
|
||||
Case documents are revisioned artifacts attached to a case under a stable **key** — for example `summary`, `findings`, or `report`. Each write appends a new revision, so history is never overwritten. Documents also support locking and inline annotation threads.
|
||||
|
||||
Document keys are 1–120 characters and may contain letters, numbers, and the characters `_ . : -` (matching `^[A-Za-z0-9_.:-]+$`). The default `format` is `markdown`.
|
||||
|
||||
### Get a Document
|
||||
|
||||
```
|
||||
GET /api/cases/{id}/documents/{key}
|
||||
```
|
||||
|
||||
Return a single case document by key, including its latest `body`. Responds `404` (`{ "error": "Case document not found" }`) if the key doesn't exist on the case.
|
||||
|
||||
### Create or Update a Document
|
||||
|
||||
```
|
||||
PUT /api/cases/{id}/documents/{key}
|
||||
```
|
||||
|
||||
Create a new document at `key`, or append a new revision to an existing one.
|
||||
|
||||
Request body:
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `body` | yes | Document content, up to 200,000 characters. |
|
||||
| `title` | no | Optional document title. |
|
||||
| `format` | no | Defaults to `markdown`. |
|
||||
| `changeSummary` | no | Optional note attached to the new revision, up to 1,000 characters. |
|
||||
| `baseRevisionId` | no on create, required on update | The revision you based your edit on. |
|
||||
|
||||
Concurrency rules:
|
||||
|
||||
- Omit `baseRevisionId` when creating a brand-new document. Supplying one for a key that doesn't exist yet returns `409 Conflict` (`{ "error": "Case document does not exist yet" }`).
|
||||
- When updating an existing document you **must** send `baseRevisionId`. Omitting it returns `409 Conflict` (`{ "error": "Case document update requires baseRevisionId" }`).
|
||||
- A stale `baseRevisionId` — one that isn't the current latest revision — returns `409 Conflict` (`{ "error": "Case document was updated by someone else" }`) along with the current latest revision so you can re-anchor and retry.
|
||||
- If the document is locked, the write is rejected with `409 Conflict` (`{ "error": "Document is locked" }`).
|
||||
|
||||
### Revision History
|
||||
|
||||
```
|
||||
GET /api/cases/{id}/documents/{key}/revisions
|
||||
```
|
||||
|
||||
Return the document's revisions, newest first, each enriched with the display name of the agent that wrote it and the issue attributed to the run behind it (when there is one).
|
||||
|
||||
### Restore a Revision
|
||||
|
||||
```
|
||||
POST /api/cases/{id}/documents/{key}/revisions/{revisionId}/restore
|
||||
```
|
||||
|
||||
Restore a prior revision by writing its contents forward as a new latest revision — history is preserved, not rewound. Restoring the revision that is already latest returns `409 Conflict`, and a locked document is rejected the same way as a write.
|
||||
|
||||
### Lock and Unlock a Document
|
||||
|
||||
```
|
||||
POST /api/cases/{id}/documents/{key}/lock
|
||||
POST /api/cases/{id}/documents/{key}/unlock
|
||||
```
|
||||
|
||||
Lock a document to freeze it against further writes; unlock to allow them again. Both routes are idempotent — locking an already-locked document (or unlocking an already-unlocked one) simply returns the current state. While a document is locked, writes, restores, and deletes all return `409 Conflict` with `{ "error": "Document is locked" }`.
|
||||
|
||||
### Delete a Document
|
||||
|
||||
```
|
||||
DELETE /api/cases/{id}/documents/{key}
|
||||
```
|
||||
|
||||
Delete the document and all of its revisions. A locked document cannot be deleted (`409 Conflict`). Deleting a key that doesn't exist is a no-op and still responds `{ "ok": true }`.
|
||||
|
||||
---
|
||||
|
||||
## Document Annotations
|
||||
|
||||
Annotations attach comment threads to a specific passage of a case document — the same margin-note model used for issue documents. A thread is anchored to a selected range of text, carries one or more comments, and can be resolved once the conversation is settled. Both users and agents can open threads and reply. When a document is later revised, the server re-anchors each open thread against the new revision, so feedback follows the text it was written about.
|
||||
|
||||
### List Threads
|
||||
|
||||
```
|
||||
GET /api/cases/{id}/documents/{key}/annotations
|
||||
```
|
||||
|
||||
Return the annotation threads on a document. Query parameters:
|
||||
|
||||
- `status` — `open`, `resolved`, or `all`. Defaults to `open`.
|
||||
- `includeComments` — when `true`, each thread embeds its full comment list.
|
||||
|
||||
### Get a Thread
|
||||
|
||||
```
|
||||
GET /api/cases/{id}/documents/{key}/annotations/{threadId}
|
||||
```
|
||||
|
||||
Return a single thread with its comments. Responds `404` (`{ "error": "Annotation thread not found" }`) if the thread doesn't belong to that case document.
|
||||
|
||||
### Create a Thread
|
||||
|
||||
```
|
||||
POST /api/cases/{id}/documents/{key}/annotations
|
||||
```
|
||||
|
||||
Open a new thread anchored to a passage, with its first comment in the same request. The thread is pinned to the document's current revision, so — as with issue annotations — a stale base revision is rejected so the anchor can't land in the wrong place, and an unmatchable selector returns `422 Unprocessable Entity`. A successful create responds `201 Created`.
|
||||
|
||||
### Reply to a Thread
|
||||
|
||||
```
|
||||
POST /api/cases/{id}/documents/{key}/annotations/{threadId}/comments
|
||||
```
|
||||
|
||||
Add a comment to an existing thread. Responds `201 Created`.
|
||||
|
||||
### Resolve or Reopen a Thread
|
||||
|
||||
```
|
||||
PATCH /api/cases/{id}/documents/{key}/annotations/{threadId}
|
||||
```
|
||||
|
||||
Change a thread's status — `resolved` to close the conversation, or `open` to reopen it.
|
||||
|
||||
---
|
||||
|
||||
## Attachments
|
||||
|
||||
```
|
||||
POST /api/cases/{id}/attachments
|
||||
```
|
||||
|
||||
Upload a single file to a case with `multipart/form-data`. The file goes in a field named `file`.
|
||||
|
||||
Upload rules:
|
||||
|
||||
- Exactly one file is accepted; the field name must be `file` (`{ "error": "Missing file field 'file'" }` otherwise).
|
||||
- Empty files are rejected with `422 Unprocessable Entity` (`{ "error": "Attachment is empty" }`).
|
||||
- Files larger than the company's attachment size limit are rejected with `422 Unprocessable Entity` (`{ "error": "Attachment exceeds {n} bytes" }`). The default limit is 10 MiB.
|
||||
|
||||
A successful upload responds `201 Created`, records an `attachment_added` event, and (for agent runs) auto-links the run's issue with role `work`.
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-F "file=@./findings.pdf" \
|
||||
"https://paperclip.example.com/api/cases/{id}/attachments"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Event Timeline
|
||||
|
||||
```
|
||||
GET /api/cases/{id}/events
|
||||
```
|
||||
|
||||
Return the case's event history, newest first. Query parameters:
|
||||
|
||||
- `limit` — positive integer, 1–500. Defaults to `100`.
|
||||
|
||||
Each event is enriched with the display name of the agent that caused it (`actorAgentName`) and, when the event is tied to a run or an issue link, a compact summary of the related `issue`.
|
||||
|
||||
Events are typed by `kind`:
|
||||
|
||||
| Kind | Emitted when |
|
||||
|---|---|
|
||||
| `created` | A case is created. |
|
||||
| `updated` | A case is updated with no more specific change. |
|
||||
| `status_changed` | The case status changes. |
|
||||
| `fields_changed` | The `fields` object is updated. |
|
||||
| `child_linked` | The case is re-parented under another case. |
|
||||
| `issue_linked` | An issue is linked (manually or auto-linked). |
|
||||
| `issue_unlinked` | An issue link is removed. |
|
||||
| `document_revised` | A case document gains a new revision (including restores). |
|
||||
| `attachment_added` | A file is uploaded. |
|
||||
| `label_added` | A label is attached. |
|
||||
| `label_removed` | A label is removed. |
|
||||
|
||||
---
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [Issues](./issues.md) — the work objects cases link to, and the source of the issue identifiers used by `POST /api/cases/{id}/links` and `GET /api/issues/{issueId}/cases`.
|
||||
- [Goals and Projects](./goals-and-projects.md) — the projects a case can belong to via `projectId`.
|
||||
- [API Overview](./overview.md) — base URL, authentication, company scoping, and the shared error-code table.
|
||||
@@ -0,0 +1,248 @@
|
||||
---
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Company Skill Policy
|
||||
|
||||
Skills are how your agents pick up new abilities — and left unguarded, an agent can create, import, install, edit, or remove them at will. The **company skill policy** is the guardrail. It is a single per-company document that says which agents may perform which skill actions, on which skills, and from which sources.
|
||||
|
||||
The policy is one document per company, with a revision number for safe concurrent edits. You read it, replace it wholesale, reset it back to open, and — usefully — ask the server to explain a decision before you make a change.
|
||||
|
||||
Every route below is company-scoped, and company access is checked on every request. If you are new to the API, read the [API Overview](./overview.md) first for base URL, authentication, and error-code conventions; this page builds on those and won't repeat them.
|
||||
|
||||
---
|
||||
|
||||
## The open default
|
||||
|
||||
A brand-new company has no stored policy at all. Reading the policy in that state gives you an **open default**:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"revision": 0,
|
||||
"defaultEffect": "allow",
|
||||
"rules": [],
|
||||
"materialized": false
|
||||
}
|
||||
```
|
||||
|
||||
The `materialized` flag is the one to watch. When it is `false`, no policy row exists — nothing has been written yet, and every skill action is allowed. Once you save a policy it becomes `true` and `revision` starts counting up from `1`.
|
||||
|
||||
That distinction also shows up in evaluation. An unmaterialized policy short-circuits to "allowed" with the reason `no_policy_default`, without walking any rules.
|
||||
|
||||
---
|
||||
|
||||
## What a policy document looks like
|
||||
|
||||
A policy has three parts: a schema version, a default effect, and an ordered set of rules.
|
||||
|
||||
| Field | Notes |
|
||||
|---|---|
|
||||
| `schemaVersion` | Must be `1`. |
|
||||
| `defaultEffect` | `allow` or `deny`. Applied when no rule matches. |
|
||||
| `rules` | Array of rule objects, up to 1,000. Rule `id` values must be unique. |
|
||||
|
||||
Documents are strict — unknown fields are rejected.
|
||||
|
||||
### Rules
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `id` | yes | 1–128 characters, matching `^[a-zA-Z0-9][a-zA-Z0-9._:-]*$`. |
|
||||
| `priority` | yes | Integer between `-1000000` and `1000000`. Lower numbers are considered first. |
|
||||
| `effect` | yes | `allow` or `deny`. |
|
||||
| `subject` | yes | Who the rule applies to — see below. |
|
||||
| `actions` | yes | At least one skill action, no duplicates. |
|
||||
| `resources` | no | Narrows the rule to particular skills or sources. Omit it to match every resource. |
|
||||
|
||||
### Subjects
|
||||
|
||||
The `subject` object is a tagged union on `type`:
|
||||
|
||||
| `type` | Shape | Matches |
|
||||
|---|---|---|
|
||||
| `all_agents` | `{ "type": "all_agents" }` | Any agent principal. |
|
||||
| `agents` | `{ "type": "agents", "agentIds": [...] }` | The listed agents. 1–500 unique UUIDs. |
|
||||
| `roles` | `{ "type": "roles", "roles": [...] }` | Any principal whose role matches, compared case-insensitively after trimming. 1–500 unique strings. |
|
||||
|
||||
`all_agents` and `agents` only ever match agent principals. `roles` matches on the principal's role string, so a board principal — which evaluates with the role `board` — can be targeted with a `roles` rule.
|
||||
|
||||
### Actions
|
||||
|
||||
| Action | Covers |
|
||||
|---|---|
|
||||
| `skills.create` | Creating a new skill. |
|
||||
| `skills.import` | Importing a skill. |
|
||||
| `skills.install` | Installing a skill. |
|
||||
| `skills.edit` | Editing an existing skill. |
|
||||
| `skills.update` | Updating a skill from its source. |
|
||||
| `skills.test` | Running a skill test. |
|
||||
| `skills.reset` | Resetting a skill. |
|
||||
| `skills.remove` | Removing a skill. |
|
||||
|
||||
### Resource selectors
|
||||
|
||||
`resources` narrows a rule. At least one of these keys must be present, and unknown keys are rejected:
|
||||
|
||||
| Key | Notes |
|
||||
|---|---|
|
||||
| `skillIds` | 1–500 unique skill UUIDs. |
|
||||
| `skillKeys` | 1–500 unique strings, each 1–512 characters. |
|
||||
| `sourceTypes` | One or more of `workspace`, `catalog`, `git`, `external_package`, `generated`, `unknown`. Must be unique. |
|
||||
| `sourceLocators` | 1–500 unique locator strings, each up to 2,048 characters. |
|
||||
|
||||
Every listed selector has to match for the rule to apply. A rule that names `skillKeys` will never match an evaluation that doesn't carry a `skillKey`.
|
||||
|
||||
**Source locators are normalized.** Both the values you store in a rule and the values you pass at evaluation time are canonicalized the same way — repo-style `https` URLs get a lowercased host (`www.github.com` becomes `github.com`), a lowercased owner and repo, and a stripped `.git` suffix. That matters: without matching normalization on both sides, a deny rule could silently never fire.
|
||||
|
||||
Locators are also screened for embedded credentials. A locator with a username or password in the URL, or with a query or fragment parameter that looks like `token`, `secret`, `password`, `api_key`, or `authorization`, is rejected with the message *"Source locators must not contain credentials or secret query or fragment parameters"*.
|
||||
|
||||
---
|
||||
|
||||
## Who can read and who can change
|
||||
|
||||
Reading the policy — and evaluating against it for yourself — needs nothing more than access to the company.
|
||||
|
||||
Changing it (`PUT`, `DELETE`, or evaluating on behalf of another agent) requires **skill policy administration authority**, which means one of:
|
||||
|
||||
- a board actor from the implicit local session, or an instance admin;
|
||||
- a board user holding the `users:manage_permissions` permission in that company;
|
||||
- an agent holding the `users:manage_permissions` permission in that company.
|
||||
|
||||
Anything else comes back as `403 Forbidden` with code `skill_policy_admin_required` and the remediation *"Ask a company administrator to manage the skill policy."*
|
||||
|
||||
Two more boundaries apply to every route here:
|
||||
|
||||
- An unauthenticated caller gets `401` with code `skill_authentication_required`.
|
||||
- An agent key scoped to a different company gets `403` with code `skill_company_boundary_denied`.
|
||||
|
||||
---
|
||||
|
||||
## Get the Policy
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/skill-policy
|
||||
```
|
||||
|
||||
Return the company's effective policy — either the stored document (with `materialized: true` and its current `revision`) or the open default described above.
|
||||
|
||||
---
|
||||
|
||||
## Replace the Policy
|
||||
|
||||
```
|
||||
PUT /api/companies/{companyId}/skill-policy
|
||||
```
|
||||
|
||||
Policies are replaced wholesale, never patched. Send the complete document plus the revision you believe is current:
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `schemaVersion` | yes | `1`. |
|
||||
| `defaultEffect` | yes | `allow` or `deny`. |
|
||||
| `rules` | yes | The full rule set. Send `[]` for none. |
|
||||
| `expectedRevision` | yes | Non-negative integer. Use `0` when no policy exists yet. |
|
||||
|
||||
On success the server bumps the revision by one and returns the saved document with `materialized: true`.
|
||||
|
||||
If `expectedRevision` doesn't match what is stored, the write is rejected with `409 Conflict`, code `skill_policy_revision_conflict`, and the details carry `expectedRevision` and `currentRevision` so you can re-read, re-apply your change, and retry.
|
||||
|
||||
A document that fails validation returns `422 Unprocessable Entity` with `{ "error": "Invalid skill policy document" }`, code `skill_policy_validation_failed`, and an `issues` array describing what went wrong.
|
||||
|
||||
A successful replace records a `company.skill_policy_replaced` activity entry against entity type `company_skill_policy`, with the previous and new revision, the default effect, and the rule count.
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl -sS -X PUT \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://paperclip.example.com/api/companies/{companyId}/skill-policy" \
|
||||
-d '{
|
||||
"schemaVersion": 1,
|
||||
"defaultEffect": "allow",
|
||||
"expectedRevision": 0,
|
||||
"rules": [
|
||||
{
|
||||
"id": "no-external-installs",
|
||||
"priority": 10,
|
||||
"effect": "deny",
|
||||
"subject": { "type": "all_agents" },
|
||||
"actions": ["skills.install", "skills.import"],
|
||||
"resources": { "sourceTypes": ["external_package", "git"] }
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reset the Policy
|
||||
|
||||
```
|
||||
DELETE /api/companies/{companyId}/skill-policy
|
||||
```
|
||||
|
||||
Delete the stored policy and return to the open default. The response is the open default document (`defaultEffect: "allow"`, no rules, `materialized: false`).
|
||||
|
||||
Resetting a company that had a policy records a `company.skill_policy_reset` activity entry with the previous revision and a new revision of `0`. Resetting a company that never had one is a no-op and still returns the default.
|
||||
|
||||
---
|
||||
|
||||
## Evaluate a Decision
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/skill-policy/evaluate
|
||||
```
|
||||
|
||||
Ask the policy engine what it would decide, without performing the action. This is the endpoint to reach for when you want to explain a denial, or to test a rule set before relying on it.
|
||||
|
||||
Request body:
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `action` | yes | One of the eight skill actions. |
|
||||
| `resource` | no | `skillId`, `skillKey`, `sourceType`, `sourceLocator` — any subset. Defaults to `{}`. |
|
||||
| `principal` | no | `{ "agentId": "<uuid>" }`. Evaluate as that agent instead of yourself. |
|
||||
|
||||
Without `principal`, the evaluation runs as **you**: an agent caller evaluates as its own agent (with that agent's role), and a board caller evaluates as a board principal with the role `board`.
|
||||
|
||||
With `principal`, you are asking about someone else — so the route requires skill policy administration authority, exactly like a write. The named agent must exist (`404 Agent not found`) and must belong to this company (`403`, code `skill_company_boundary_denied`).
|
||||
|
||||
### How a decision is reached
|
||||
|
||||
1. If the policy isn't materialized, the answer is **allow**, reason `no_policy_default`.
|
||||
2. Otherwise rules are sorted by `priority` ascending, ties broken by `id` in lexicographic order, and the first rule whose actions, subject, and resource selectors all match wins. The decision follows that rule's `effect`, reason `explicit_rule`, with the rule's id in `matchedRuleId`.
|
||||
3. If nothing matched and `defaultEffect` is `deny`, one compatibility check runs: a principal holding a `skills:create` or `skills:suggest-changes` permission grant in the company is allowed with reason `legacy_compatibility`. Those grants historically authorized the whole skill mutation surface, and this preserves that scope only as a default-deny fallback — explicit rules still take precedence.
|
||||
4. Otherwise the answer follows `defaultEffect`, reason `policy_default`.
|
||||
|
||||
### The decision object
|
||||
|
||||
```json
|
||||
{
|
||||
"allowed": false,
|
||||
"action": "skills.install",
|
||||
"reason": "explicit_rule",
|
||||
"policyRevision": 4,
|
||||
"matchedRuleId": "no-external-installs",
|
||||
"remediation": "Contact a company administrator to change the skill policy."
|
||||
}
|
||||
```
|
||||
|
||||
`remediation` is `null` when the action is allowed.
|
||||
|
||||
| `reason` | Meaning |
|
||||
|---|---|
|
||||
| `no_policy_default` | No policy is stored, so the action is allowed. |
|
||||
| `explicit_rule` | A rule matched; `matchedRuleId` names it. |
|
||||
| `policy_default` | No rule matched; `defaultEffect` decided it. |
|
||||
| `legacy_compatibility` | Default-deny, but a legacy `skills:create` / `skills:suggest-changes` grant allowed it. |
|
||||
| `platform_invariant` | A platform-level rule the skill routes enforce ahead of the policy. |
|
||||
|
||||
---
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [Agents](./agents.md) — the agents you name in `subject.agentIds` and in `principal.agentId`.
|
||||
- [Activity](./activity.md) — where `company.skill_policy_replaced` and `company.skill_policy_reset` land.
|
||||
- [API Overview](./overview.md) — base URL, authentication, company scoping, and the shared error-code table.
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Costs
|
||||
|
||||
Use this API when you want to answer four questions:
|
||||
@@ -36,6 +40,7 @@ Common optional fields:
|
||||
- `billingCode` for your own accounting label
|
||||
- `biller` if the charge came from a billing entity different from `provider`
|
||||
- `billingType` if you want to distinguish `metered_api`, `subscription_included`, `subscription_overage`, `credits`, `fixed`, or `unknown`
|
||||
- `costStatus` to mark whether the usage is priced — `reported` for normal spend, or `unpriced` when you recorded token usage but no dollar cost was available
|
||||
- `inputTokens`, `cachedInputTokens`, `outputTokens` for token-level reporting
|
||||
|
||||
Rules from the implementation:
|
||||
@@ -45,8 +50,11 @@ Rules from the implementation:
|
||||
- Agent-authenticated calls can only report that agent’s own costs.
|
||||
- `biller` defaults to `provider` when omitted.
|
||||
- `billingType` defaults to `unknown` when omitted.
|
||||
- `costStatus` defaults to `reported` when omitted.
|
||||
- `occurredAt` must be an ISO datetime string.
|
||||
|
||||
> **Priced vs. unpriced usage.** Most cost events carry a real `costCents` amount, so they stay `reported`. When an agent run uses tokens but Paperclip can't determine a dollar cost — for example a local CLI whose per-token pricing isn't known — the usage is recorded with `costStatus` set to `unpriced` instead of being logged as if it cost nothing. That keeps the token counts on the record while flagging that the money figure is an unknown rather than a real zero.
|
||||
|
||||
When the event is accepted, Paperclip:
|
||||
|
||||
- stores the event
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
---
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Decision Training
|
||||
|
||||
Every day, the people in your company make judgement calls that agents can learn from — someone answers an agent's question in a thread, someone approves or rejects a risky action, someone decides how an execution stage should proceed. **Decision training** lets you keep those moments. You point at a decision that already happened, the server freezes everything the decider could see at that moment into an immutable snapshot, you add a note explaining *why* the call went the way it did, and the result becomes a labelled training example you can export.
|
||||
|
||||
Think of it as bookmarking good judgement. The snapshot is the input, your note plus the recorded outcome is the label.
|
||||
|
||||
Decision training examples are company-scoped, and company access is enforced on every request. If you are new to the API, read the [API Overview](./overview.md) first for base URL, authentication, and error-code conventions; this page builds on those and won't repeat them.
|
||||
|
||||
> **Writes are human-only.** Creating, editing, and deleting examples require a signed-in human user on the board. An agent token calling those routes gets `403 Forbidden` with `{ "error": "Decision training writes require a human user" }`. This is deliberate — the whole point is to capture human judgement.
|
||||
|
||||
---
|
||||
|
||||
## The three kinds of decision
|
||||
|
||||
When you capture an example you tell the server which kind of decision you are pointing at. There are exactly three:
|
||||
|
||||
| `sourceKind` | What it points at |
|
||||
|---|---|
|
||||
| `interaction` | A question or request an agent raised on an issue thread, and how it was resolved. |
|
||||
| `approval` | An approval request on an issue — approved, rejected, or still pending. |
|
||||
| `execution_decision` | A decision recorded against an execution stage on an issue. |
|
||||
|
||||
Whichever kind you choose, you also supply the `sourceId` of that specific decision and the `issueId` of the issue it lives on. All three must line up — the server looks the decision up scoped to both the company and the issue, so a mismatched pair comes back `404 Not Found` with `{ "error": "Decision interaction not found" }`, `{ "error": "Decision approval not found" }`, or `{ "error": "Execution decision not found" }` depending on the kind. An `issueId` that doesn't belong to the company returns `{ "error": "Issue not found" }`.
|
||||
|
||||
---
|
||||
|
||||
## What gets frozen: the snapshot
|
||||
|
||||
The most important idea here is the **cutoff**. The snapshot is not "the issue as it looks today" — it is "the issue as it looked at the instant the decision was made". Anything created after that moment is left out, so a training example can never accidentally contain the future.
|
||||
|
||||
For a resolved `interaction` or `approval` the cutoff is the moment it was resolved or decided. For an `execution_decision` it is the moment the decision was created. If the interaction or approval is still pending, there is no decision moment yet, so the cutoff is the capture time and `decisionOutcome` comes back `null`.
|
||||
|
||||
Everything at or before that cutoff is copied into a versioned `snapshot` object:
|
||||
|
||||
| Snapshot field | Contents |
|
||||
|---|---|
|
||||
| `version` | Snapshot format version — currently `1`. |
|
||||
| `retention` | The retention rules baked into this snapshot: `policy`, plus `commentDeletion: "redact"` and `issueDeletion: "cascade"`. |
|
||||
| `capturedAt` | ISO timestamp of when the capture ran. |
|
||||
| `cutoff` | `at` (ISO timestamp), `lastCommentId`, and `commentCount`. |
|
||||
| `issue` | The full issue record. |
|
||||
| `comments` | Every issue comment created at or before the cutoff, oldest first. |
|
||||
| `runs` | The agent runs tied to this issue that had started and last updated at or before the cutoff. |
|
||||
| `decision` | `kind`, `payload`, `actor`, and `outcome` for the decision itself. |
|
||||
| `code` | Best-effort code context — see below. |
|
||||
|
||||
### Code context
|
||||
|
||||
Paperclip tries to record which code the decision was made against, so a training example can be replayed meaningfully. The `code` object carries `repoUrl`, `ref`, `commitSha`, and a `resolution` field telling you how confident that commit is:
|
||||
|
||||
| `resolution` | Meaning |
|
||||
|---|---|
|
||||
| `exact` | The commit came from the exact run that produced the decision. |
|
||||
| `nearest_run` | No commit on the exact run, so the latest run before the cutoff that had one was used. |
|
||||
| `workspace` | Fell back to the execution or project workspace metadata. |
|
||||
| `none` | No commit could be determined. |
|
||||
|
||||
---
|
||||
|
||||
## Retention: what happens when the source changes
|
||||
|
||||
A snapshot is immutable, but it is not exempt from deletion requests. Two rules apply, and both are recorded inside every snapshot's `retention` block under the policy `scrub_deleted_comments_v1`:
|
||||
|
||||
- **Comment deletion redacts.** If an issue comment that made it into a snapshot is later deleted, the server rewrites that comment inside the snapshot to an empty body, stamps `deletedAt`, and adds `retentionRedaction` with the reason `source_comment_deleted`. The rest of the snapshot is untouched.
|
||||
- **Issue deletion cascades.** If the issue is deleted, its decision training examples go with it.
|
||||
|
||||
---
|
||||
|
||||
## Create an Example
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/decision-training
|
||||
```
|
||||
|
||||
Capture a decision as a training example. Responds `201 Created` with the stored example.
|
||||
|
||||
Request body:
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `sourceKind` | yes | One of `interaction`, `approval`, `execution_decision`. |
|
||||
| `sourceId` | yes | UUID of the decision. |
|
||||
| `issueId` | yes | UUID of the issue the decision belongs to. |
|
||||
| `notes` | no | Your explanation of the decision. Up to 100,000 characters. Defaults to `""`. |
|
||||
|
||||
Unknown fields are rejected.
|
||||
|
||||
**One example per decision, per person.** The server enforces uniqueness on `(sourceKind, sourceId, createdByUserId)`. If you have already trained this decision, a second attempt returns `409 Conflict` with `{ "error": "This decision is already trained by this user" }`. Your colleague can still train the same decision independently — the constraint is per author, so several people can label the same moment differently.
|
||||
|
||||
A successful create records a `decision_training.created` activity entry.
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://paperclip.example.com/api/companies/{companyId}/decision-training" \
|
||||
-d '{
|
||||
"sourceKind": "approval",
|
||||
"sourceId": "8f1c2b7e-5f3a-4d61-9a5e-2c0b7d4e1a93",
|
||||
"issueId": "3a9d0c14-77b2-4f8e-b0e6-9d5c1e2f7a44",
|
||||
"notes": "Rejected because the migration had no rollback path. Always ask for one on schema changes."
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Preview an Example
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/decision-training/preview
|
||||
```
|
||||
|
||||
Want to see exactly what would be frozen before you commit? Preview runs the same capture and returns the result **without saving anything**.
|
||||
|
||||
Request body takes `sourceKind`, `sourceId`, and `issueId` — the same three fields as create, minus `notes`. Unknown fields are rejected.
|
||||
|
||||
The response is:
|
||||
|
||||
```json
|
||||
{
|
||||
"cutoffAt": "2026-07-14T09:21:03.118Z",
|
||||
"decisionOutcome": "rejected",
|
||||
"snapshot": { "version": 1, "...": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
Preview is read-only, but it carries the same human-only authorization as a write, because it exposes the same captured decision state.
|
||||
|
||||
---
|
||||
|
||||
## List Examples
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/decision-training
|
||||
```
|
||||
|
||||
Return the company's examples, newest first. Each row is `{ example, issueTitle, issueIdentifier }`, so you can render a list without a second lookup per issue.
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Param | Description |
|
||||
|---|---|
|
||||
| `project` | Filter to examples whose issue belongs to this project (UUID). |
|
||||
| `kind` | Filter by source kind — `interaction`, `approval`, or `execution_decision`. |
|
||||
| `author` | Filter to examples created by a given user id. |
|
||||
| `q` | Case-insensitive search across the example's `notes` and the issue's `title` and `identifier`. Up to 500 characters. |
|
||||
|
||||
An invalid query returns `400 Bad Request` with `{ "error": "Invalid decision training query", "details": { ... } }`.
|
||||
|
||||
---
|
||||
|
||||
## Get an Example
|
||||
|
||||
```
|
||||
GET /api/decision-training/{id}
|
||||
```
|
||||
|
||||
Return one example by UUID, including its full `snapshot`, `notes`, and `notesHistory`. If the id is not a UUID, or the example belongs to a company you cannot access, the response is `404 Not Found` with `{ "error": "Decision training example not found" }` — the same answer either way, so the route never confirms an example you aren't allowed to see.
|
||||
|
||||
Reads are logged as `decision_training.read` activity.
|
||||
|
||||
### Example fields
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `id` | The example's UUID. |
|
||||
| `companyId` | Owning company. |
|
||||
| `sourceKind` / `sourceId` | The decision this example was captured from. |
|
||||
| `issueId` | The issue the decision lives on. |
|
||||
| `cutoffAt` | The moment the snapshot was frozen at. |
|
||||
| `notes` | The current note text. |
|
||||
| `notesHistory` | Prior note versions, each `{ author, at, body }`. |
|
||||
| `decisionOutcome` | The recorded outcome, or `null` if the decision was still pending at capture. |
|
||||
| `retentionPolicy` | The retention policy applied — `scrub_deleted_comments_v1`. |
|
||||
| `snapshot` | The frozen state described above. |
|
||||
| `createdByUserId` | The human who captured it. |
|
||||
| `createdAt` / `updatedAt` | Timestamps. |
|
||||
|
||||
---
|
||||
|
||||
## Update the Notes
|
||||
|
||||
```
|
||||
PATCH /api/decision-training/{id}
|
||||
```
|
||||
|
||||
The snapshot never changes, but your explanation can. This route updates `notes` and nothing else.
|
||||
|
||||
Request body:
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `notes` | yes | Up to 100,000 characters. |
|
||||
|
||||
Unknown fields are rejected. **Only the author can edit.** Someone else attempting to change your example gets `403 Forbidden` with `{ "error": "Only the example author can change decision training examples" }`.
|
||||
|
||||
Old note text is never lost — each real change pushes the previous body onto `notesHistory` as `{ author, at, body }` and logs a `decision_training.notes_updated` activity entry. Submitting the identical text is a no-op: the example is returned unchanged, with no history entry and no activity.
|
||||
|
||||
---
|
||||
|
||||
## Delete an Example
|
||||
|
||||
```
|
||||
DELETE /api/decision-training/{id}
|
||||
```
|
||||
|
||||
Remove an example entirely. Responds `204 No Content` with an empty body.
|
||||
|
||||
As with editing, only the author can delete, and only a human user can. A deletion records a `decision_training.deleted` activity entry.
|
||||
|
||||
---
|
||||
|
||||
## Export as JSONL
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/decision-training/export.jsonl
|
||||
```
|
||||
|
||||
This is the endpoint you point a training pipeline at. It returns the company's examples — the same set and order as the list route, newest first — as newline-delimited JSON, served with content type `application/x-ndjson`.
|
||||
|
||||
Each line is one complete JSON object with exactly three keys:
|
||||
|
||||
| Key | Contents |
|
||||
|---|---|
|
||||
| `retentionPolicy` | The example's retention policy, e.g. `scrub_deleted_comments_v1`. |
|
||||
| `state` | The full frozen `snapshot` — the input side of the example. |
|
||||
| `label` | `{ "outcome": ..., "notes": ... }` — the recorded `decisionOutcome` and the author's notes. |
|
||||
|
||||
So a single line looks like this (formatted here for readability; in the file it is one line):
|
||||
|
||||
```json
|
||||
{
|
||||
"retentionPolicy": "scrub_deleted_comments_v1",
|
||||
"state": { "version": 1, "capturedAt": "...", "issue": {}, "comments": [], "runs": [], "decision": {}, "code": {} },
|
||||
"label": { "outcome": "rejected", "notes": "Rejected because the migration had no rollback path." }
|
||||
}
|
||||
```
|
||||
|
||||
The body ends with a trailing newline when there is at least one example, and is completely empty when there are none. Note that the export applies **no filters** — it always returns every example in the company. Use the list route when you want a filtered subset.
|
||||
|
||||
Every export is recorded as a `decision_training.exported` activity entry, including how many examples went out and their ids, so there is always an audit trail of who took the data.
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl -sS \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
"https://paperclip.example.com/api/companies/{companyId}/decision-training/export.jsonl" \
|
||||
-o decision-training.jsonl
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [Issues](./issues.md) — the issues decision training examples hang off, and the source of the thread interactions you can capture.
|
||||
- [Approvals](./approvals.md) — the approval requests you can capture with `sourceKind: "approval"`.
|
||||
- [Activity](./activity.md) — where the `decision_training.*` entries described on this page show up.
|
||||
- [API Overview](./overview.md) — base URL, authentication, company scoping, and the shared error-code table.
|
||||
@@ -0,0 +1,280 @@
|
||||
---
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Folders
|
||||
|
||||
Once a company has more than a handful of routines or skills, a flat list stops being useful. **Folders** give you somewhere to put them — a nested tree, scoped to one company, that you can rename, recolour, reorder, and drag items into.
|
||||
|
||||
Folders come in two flavours, and the two trees are completely separate: a `routine` folder holds routines, a `skill` folder holds skills. Every request names which tree you mean, and the server refuses to mix them.
|
||||
|
||||
Some folders are created and maintained by Paperclip itself — the `Bundled` category folders that ship with the product, the personal `My Skills` folder each board user gets, and a folder per project. Those are marked as system-managed and are read-only to you. Everything else is yours to organise however you like.
|
||||
|
||||
Folders are company-scoped, and — like the rest of the control-plane API — company access is enforced on every request. If you are new to the API, read the [API Overview](./overview.md) first for base URL, authentication, and error-code conventions; this page builds on those and won't repeat them.
|
||||
|
||||
---
|
||||
|
||||
## What a folder looks like
|
||||
|
||||
Every folder carries these fields:
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `id` | UUID of the folder. |
|
||||
| `companyId` | The company the folder belongs to. |
|
||||
| `kind` | `routine` or `skill`. A folder never changes kind. |
|
||||
| `parentId` | The parent folder's UUID, or `null` for a root folder. |
|
||||
| `name` | Display name, 1–120 characters. |
|
||||
| `slug` | URL-safe identifier, unique among its siblings. |
|
||||
| `systemKey` | Set on system-managed folders (for example `bundled`, `my`, `my:{userId}`, `project:{projectId}`, `bundled:{slug}`). `null` on folders you created. |
|
||||
| `path` | The slug chain from the root down to this folder, joined with `/` — for example `my/ada-lovelace`. Computed by the server, not stored. |
|
||||
| `depth` | `1` for a root folder, `2` for its children, and so on. |
|
||||
| `color` | Optional colour label, 1–80 characters, or `null`. |
|
||||
| `position` | Integer sort order among its siblings. |
|
||||
| `createdAt` / `updatedAt` | Timestamps. |
|
||||
|
||||
### Slugs
|
||||
|
||||
A slug must match `^[a-z0-9]+(?:-[a-z0-9]+)*$` — lowercase letters, numbers, and single hyphens between them, 1–120 characters. If you don't send one, the server derives it from the folder's name (accents stripped, everything lowercased, runs of other characters collapsed to a single hyphen).
|
||||
|
||||
Slugs only need to be unique **among siblings**, so two different parents can each hold a `research` folder.
|
||||
|
||||
### Depth and nesting
|
||||
|
||||
Folders nest up to **4 levels deep**. Creating or moving a folder that would push anything past that limit returns `422 Unprocessable Entity` with `{ "error": "Folder depth cannot exceed 4" }` — and on a move the whole subtree is measured, not just the folder you're dragging.
|
||||
|
||||
### System-managed folders
|
||||
|
||||
Anything with a `systemKey`, plus everything inside the `bundled` tree, is managed by Paperclip. Trying to rename, move, or delete one returns `403 Forbidden`:
|
||||
|
||||
```json
|
||||
{ "error": "System-managed folders cannot be changed" }
|
||||
```
|
||||
|
||||
and anything that would write into the bundled tree returns:
|
||||
|
||||
```json
|
||||
{ "error": "Bundled folders are read-only" }
|
||||
```
|
||||
|
||||
Three root slugs are reserved in the `skill` tree — `bundled`, `my`, and `projects`. You cannot create, rename, or move a root skill folder onto one of them; the server answers `403 Forbidden` with `{ "error": "Reserved skill folders are system-managed" }`. The `my` and `projects` containers are also closed to hand-made children, since Paperclip fills them in itself.
|
||||
|
||||
---
|
||||
|
||||
## List Folders
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/folders?kind={kind}
|
||||
```
|
||||
|
||||
Return one whole folder tree for a company, flattened into an array and sorted by `position`, then `name`, then `id`.
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Param | Required | Description |
|
||||
|---|---|---|
|
||||
| `kind` | yes | `routine` or `skill`. Anything else (including omitting it) returns `400 Bad Request` with `{ "error": "Folder kind query parameter is required" }`. |
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "skill",
|
||||
"folders": [ ... ],
|
||||
"allCount": 42,
|
||||
"unfiledCount": 7
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `kind` | The kind you asked for. |
|
||||
| `folders` | Every folder of that kind, each with all the fields above plus `itemCount` — how many items sit directly in it. |
|
||||
| `allCount` | Total items of that kind in the company, filed or not. |
|
||||
| `unfiledCount` | Items of that kind with no folder — what a "Unfiled" bucket in your UI would show. |
|
||||
|
||||
Counts are direct, not cumulative: a parent folder's `itemCount` does not include items filed in its children.
|
||||
|
||||
---
|
||||
|
||||
## Create a Folder
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/folders
|
||||
```
|
||||
|
||||
Create a folder in either tree. Responds `201 Created` with the new folder.
|
||||
|
||||
Request body:
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `kind` | yes | `routine` or `skill`. |
|
||||
| `name` | yes | 1–120 characters, trimmed. |
|
||||
| `parentId` | no | UUID of the parent folder, or `null` for a root folder. |
|
||||
| `slug` | no | Overrides the slug derived from `name`. Must match the slug pattern. |
|
||||
| `color` | no | 1–80 characters, or `null`. |
|
||||
| `position` | no | Integer, `0` or greater. Defaults to one past the highest position among its siblings, so new folders land at the end. |
|
||||
|
||||
Things that can go wrong:
|
||||
|
||||
- A `parentId` that doesn't exist, or that belongs to the other tree, returns `404 Not Found` with `{ "error": "Parent folder not found" }`.
|
||||
- A slug already taken by a sibling returns `409 Conflict` with `{ "error": "Folder slug already exists under this parent" }`.
|
||||
- Exceeding the depth limit returns `422 Unprocessable Entity`.
|
||||
- A reserved root slug or a bundled parent returns `403 Forbidden` (see above).
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://paperclip.example.com/api/companies/{companyId}/folders" \
|
||||
-d '{
|
||||
"kind": "skill",
|
||||
"name": "Research",
|
||||
"color": "#4f46e5"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ensure Your Personal Skill Folder
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/folders/ensure-my
|
||||
```
|
||||
|
||||
Every signed-in board user can have a personal folder inside `My Skills` for skills that are theirs rather than the company's. This route is idempotent: call it and you get your folder back, whether it already existed or was created on the spot. It responds `200 OK`.
|
||||
|
||||
Request body (optional — an empty body is fine):
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `slug` | no | Preferred slug for your folder. Must match the slug pattern; `null` is allowed. If it collides with a sibling, the server appends a stable suffix rather than failing. Defaults to a slug derived from your display name. |
|
||||
|
||||
The folder is keyed by `systemKey: "my:{userId}"`, so it stays yours across renames. The `My Skills` container it lives in is created automatically if it isn't there yet.
|
||||
|
||||
This is the one route on this page that needs a real person behind it. Agent tokens and other non-board actors get `403 Forbidden`:
|
||||
|
||||
```json
|
||||
{ "error": "A signed-in board user is required to create a personal skill folder" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Update a Folder
|
||||
|
||||
```
|
||||
PATCH /api/companies/{companyId}/folders/{folderId}
|
||||
```
|
||||
|
||||
Rename, recolour, re-slug, or reorder a folder in place. Responds with the updated folder, or `404 Not Found` with `{ "error": "Folder not found" }`.
|
||||
|
||||
Request body — send at least one field:
|
||||
|
||||
| Field | Notes |
|
||||
|---|---|
|
||||
| `name` | 1–120 characters, trimmed. If you change the name without sending a `slug`, the slug is re-derived from the new name. |
|
||||
| `slug` | Must match the slug pattern. |
|
||||
| `color` | 1–80 characters, or `null` to clear it. |
|
||||
| `position` | Integer, `0` or greater. |
|
||||
|
||||
An empty body is rejected with `{ "message": "At least one folder field is required" }`. This route does not change `parentId` — use the move route below for that.
|
||||
|
||||
---
|
||||
|
||||
## Move a Folder
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/folders/{folderId}/move
|
||||
```
|
||||
|
||||
Re-parent a folder and place it among its new siblings. The folder's children come with it. Responds with the updated folder, or `404 Not Found`.
|
||||
|
||||
Request body:
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `position` | yes | Integer, `0` or greater — where the folder sits among its new siblings. |
|
||||
| `parentId` | no | UUID of the new parent, or `null` to move it to the root. Omit it entirely to keep the current parent and only change position. |
|
||||
|
||||
Guard rails:
|
||||
|
||||
- `parentId` pointing at the folder itself returns `422 Unprocessable Entity` with `{ "error": "A folder cannot be its own parent" }`.
|
||||
- `parentId` pointing at one of the folder's own descendants returns `422` with `{ "error": "A folder cannot be moved into its own subtree" }`.
|
||||
- The deepest branch of the subtree is measured against the 4-level limit, so a tall subtree may not fit under a deep parent.
|
||||
- A slug clash at the destination returns `409 Conflict`.
|
||||
- System-managed and bundled folders can't be moved.
|
||||
|
||||
---
|
||||
|
||||
## Move an Item into a Folder
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/folders/items/move
|
||||
```
|
||||
|
||||
File a single routine or skill into a folder, or pull it back out to unfiled. This is the endpoint behind drag-and-drop in the board.
|
||||
|
||||
Request body:
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `kind` | yes | `routine` or `skill` — which kind of item you're moving. |
|
||||
| `itemId` | yes | UUID of the routine or skill. |
|
||||
| `folderId` | no | UUID of the destination folder. Send `null` (or omit it) to unfile the item. |
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{ "kind": "skill", "itemId": "...", "folderId": "..." }
|
||||
```
|
||||
|
||||
Things that can go wrong:
|
||||
|
||||
- A `folderId` that doesn't exist returns `404 Not Found` with `{ "error": "Folder not found" }`.
|
||||
- A folder whose `kind` doesn't match the item's returns `422 Unprocessable Entity` with `{ "error": "Folder kind must match item kind" }`.
|
||||
- A missing item returns `404` with `{ "error": "Routine not found" }` or `{ "error": "Skill not found" }`.
|
||||
- A bundled destination returns `403 Forbidden` with `{ "error": "Bundled folders are read-only" }`, and a skill that currently lives in the bundled tree can't be moved out: `{ "error": "Bundled skills cannot be moved" }`.
|
||||
|
||||
---
|
||||
|
||||
## Delete a Folder
|
||||
|
||||
```
|
||||
DELETE /api/companies/{companyId}/folders/{folderId}
|
||||
```
|
||||
|
||||
Delete an empty-of-subfolders folder. Responds `{ "deleted": { ...folder } }` with the folder as it was, or `404 Not Found` if it isn't there.
|
||||
|
||||
Deleting a folder that still has child folders returns `409 Conflict`:
|
||||
|
||||
```json
|
||||
{ "error": "Move or delete nested folders first" }
|
||||
```
|
||||
|
||||
Items are not deleted with the folder — routines and skills that were filed there simply become unfiled. System-managed and bundled folders cannot be deleted.
|
||||
|
||||
---
|
||||
|
||||
## Activity log
|
||||
|
||||
Every write on this page records an entry in the company activity log, so folder reshuffles stay auditable:
|
||||
|
||||
| Action | Emitted when |
|
||||
|---|---|
|
||||
| `folder.created` | A folder is created. |
|
||||
| `folder.personal_ensured` | A personal skill folder is ensured via `ensure-my`. |
|
||||
| `folder.updated` | A folder is renamed, recoloured, re-slugged, or repositioned. |
|
||||
| `folder.moved` | A folder is re-parented or repositioned via the move route. |
|
||||
| `folder.item_moved` | A routine or skill is filed into (or out of) a folder. |
|
||||
| `folder.deleted` | A folder is deleted. |
|
||||
|
||||
---
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [Routines](./routines.md) — the scheduled work that `kind: "routine"` folders organise.
|
||||
- [Activity](./activity.md) — where the `folder.*` entries above show up.
|
||||
- [API Overview](./overview.md) — base URL, authentication, company scoping, and the shared error-code table.
|
||||
@@ -0,0 +1,143 @@
|
||||
---
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Inbox Agent Policy
|
||||
|
||||
Agents can act on your inbox — archiving items, clearing things out, keeping your attention queue tidy. That is helpful right up until it isn't. The **inbox agent policy** is your personal switch: per company, you decide whether agents may manage your inbox at all, and if so, which ones.
|
||||
|
||||
Each policy belongs to one user in one company. You manage your own through the `me` routes, and a company administrator can read or set another member's through the by-user routes.
|
||||
|
||||
Every route below is company-scoped, and company access is checked on every request. If you are new to the API, read the [API Overview](./overview.md) first for base URL, authentication, and error-code conventions; this page builds on those and won't repeat them.
|
||||
|
||||
---
|
||||
|
||||
## The three modes
|
||||
|
||||
| Mode | What it means |
|
||||
|---|---|
|
||||
| `open` | Any agent may manage your inbox. This is the default. |
|
||||
| `allowlist` | Only the agents you list in `allowedAgentIds` may manage your inbox. |
|
||||
| `disabled` | No agent may manage your inbox. |
|
||||
|
||||
`open` is not just the starting value — it is what applies when no policy row exists at all.
|
||||
|
||||
---
|
||||
|
||||
## The policy object
|
||||
|
||||
```json
|
||||
{
|
||||
"companyId": "…",
|
||||
"userId": "…",
|
||||
"mode": "open",
|
||||
"allowedAgentIds": [],
|
||||
"materialized": false,
|
||||
"createdAt": null,
|
||||
"updatedAt": null
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Notes |
|
||||
|---|---|
|
||||
| `mode` | `open`, `allowlist`, or `disabled`. |
|
||||
| `allowedAgentIds` | Agent UUIDs. Always `[]` unless `mode` is `allowlist`. |
|
||||
| `materialized` | `false` when nothing has been saved yet and you are seeing the default; `true` once a policy has been written. |
|
||||
| `createdAt` / `updatedAt` | Timestamps, or `null` while unmaterialized. |
|
||||
|
||||
---
|
||||
|
||||
## Your own policy
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/users/me/inbox-agent-policy
|
||||
PUT /api/companies/{companyId}/users/me/inbox-agent-policy
|
||||
```
|
||||
|
||||
These routes need company access and nothing more — no permission key is involved. You are always allowed to manage your own inbox policy.
|
||||
|
||||
They do require a **board user context**: the acting identity has to be a board actor carrying a user id. A caller without one — an agent API key, for instance — gets `401` with `{ "error": "Board user context required" }`, because there is no "me" to resolve.
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl -sS -X PUT \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://paperclip.example.com/api/companies/{companyId}/users/me/inbox-agent-policy" \
|
||||
-d '{
|
||||
"mode": "allowlist",
|
||||
"allowedAgentIds": ["6d2f1c8e-1f4b-4a51-9f0b-2c2a4c9c8f11"]
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Someone else's policy
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/users/{userId}/inbox-agent-policy
|
||||
PUT /api/companies/{companyId}/users/{userId}/inbox-agent-policy
|
||||
```
|
||||
|
||||
Administrators use these to inspect or set a teammate's policy — during onboarding, or to switch an inbox off while someone is away.
|
||||
|
||||
Both routes require **inbox agent policy administration authority**, which means one of:
|
||||
|
||||
- a board actor from the implicit local session, or an instance admin;
|
||||
- a board user holding the `users:manage_permissions` permission in that company;
|
||||
- an agent holding the `users:manage_permissions` permission in that company.
|
||||
|
||||
Anything else returns `403 Forbidden` with `{ "error": "Inbox agent policy administration authority required" }` and code `inbox_agent_policy_admin_required`.
|
||||
|
||||
> **Note.** The permission these routes check is `users:manage_permissions`, not `inbox:manage`. The `inbox:manage` permission key does exist in the permission catalog, but it governs whether an agent may *act on* an inbox — not whether it may edit the policy. See [Who the policy actually governs](#who-the-policy-actually-governs) below.
|
||||
|
||||
After the authority check, the target user must be an **active member** of the company. If there is no membership, or the membership isn't `active`, the request returns `404` with `{ "error": "Active company user membership not found" }`.
|
||||
|
||||
---
|
||||
|
||||
## Updating a policy
|
||||
|
||||
Both `PUT` routes take the same body:
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `mode` | yes | `open`, `allowlist`, or `disabled`. |
|
||||
| `allowedAgentIds` | no | Array of agent UUIDs, up to 100. Defaults to `[]`. |
|
||||
|
||||
Rules:
|
||||
|
||||
- Unknown fields are rejected.
|
||||
- `allowedAgentIds` must be empty unless `mode` is `allowlist`. Sending agents alongside `open` or `disabled` fails validation with *"allowedAgentIds must be empty when mode is not \"allowlist\""*.
|
||||
- Validation failures come back as `400 Bad Request` with `{ "error": "Validation error", "details": [...] }`.
|
||||
- Duplicate agent ids are de-duplicated before saving.
|
||||
- Every allowlisted agent must belong to this company. Any that don't return `422 Unprocessable Entity` with `{ "error": "Inbox agent policy contains agents outside the company" }`, code `inbox_agent_policy_invalid_agents`, and an `invalidAgentIds` array naming the offenders.
|
||||
|
||||
Switching away from `allowlist` clears the list. If you set `mode` to `open` or `disabled`, `allowedAgentIds` is stored as `[]` — so switching back to `allowlist` later means naming your agents again.
|
||||
|
||||
Every successful write — through either the `me` or the by-user route — records an `inbox.agent_policy_updated` activity entry against entity type `user_inbox_agent_policy`, with the affected `userId`, the `previousMode`, the new `mode`, and the resulting `allowedAgentIds`.
|
||||
|
||||
---
|
||||
|
||||
## Who the policy actually governs
|
||||
|
||||
The policy is consulted when an agent attempts the `inbox:manage` action, and how it applies depends on *whose* inbox is being touched.
|
||||
|
||||
**The agent's own responsible user.** When an agent acts on the inbox of the user it is running on behalf of, that user's policy is the deciding factor:
|
||||
|
||||
- `disabled` → denied, reason `inbox_management_disabled`.
|
||||
- `allowlist` and the agent isn't on the list → denied, reason `inbox_agent_not_allowed`.
|
||||
- Otherwise allowed, carrying the effective `inboxPolicyMode` (`open` when nothing is stored).
|
||||
|
||||
**Any other user.** Reaching into a different member's inbox is a board-admin override, not something the target's policy opens up. The agent must hold an explicit `inbox:manage` grant in the company, and that grant's scope must cover the target user. Without the grant the denial reason is `deny_missing_grant`; with a grant whose scope doesn't reach that user it is `deny_scope`. A successful cross-user action is allowed by explicit grant and reports `inboxPolicyMode: "grant_override"` — the target's own policy is not applied in this path.
|
||||
|
||||
In both cases the acting agent must be active in the company, and the target user must be an active member.
|
||||
|
||||
---
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [Attention](./attention.md) — the inbox surface these policies protect.
|
||||
- [Agents](./agents.md) — the agents you name in `allowedAgentIds`.
|
||||
- [Resource Memberships](./resource-memberships.md) — company memberships and the permission grants behind `users:manage_permissions` and `inbox:manage`.
|
||||
- [API Overview](./overview.md) — base URL, authentication, company scoping, and the shared error-code table.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
paperclip_version: v2026.529.0
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Instance Admin API
|
||||
@@ -25,6 +25,8 @@ These surfaces are stable enough to call from your own tooling, but they are int
|
||||
|
||||
The experimental "issue-graph-liveness auto-recovery" routes are paired: preview produces a dry-run summary; run applies the same changes. Both share the `issueGraphLivenessAutoRecoveryRequestSchema` body shape.
|
||||
|
||||
The experimental-settings response and patch body include `enableBuiltInAgents` as a boolean. Set it to `true` before using the [Built-in Agents API](./built-in-agents.md); while it is off, those routes return `404 Not Found`.
|
||||
|
||||
---
|
||||
|
||||
## Database backups
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
paperclip_version: v2026.609.0
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Issues
|
||||
@@ -1073,10 +1073,10 @@ POST /api/issues/{issueId}/interactions
|
||||
|
||||
Request body fields:
|
||||
|
||||
- `kind` — one of `suggest_tasks`, `ask_user_questions`, `request_confirmation`.
|
||||
- `kind` — one of `suggest_tasks`, `ask_user_questions`, `request_confirmation`, `request_checkbox_confirmation`, or `request_item_verdicts`.
|
||||
- `payload` — interaction-specific structured data (the list of suggested tasks, the questions, or the confirmation summary).
|
||||
- `idempotencyKey` — optional. Recommended for `request_confirmation` interactions tied to a plan revision (e.g. `confirmation:{issueId}:plan:{revisionId}`) so re-sends do not double-create.
|
||||
- `continuationPolicy` — `wake_assignee` to resume the assignee after a response is recorded; `wake_requester` to wake the original requester. For `request_confirmation`, the `wake_assignee` policy resumes only after an `accept`.
|
||||
- `continuationPolicy` — one of `none`, `wake_assignee`, or `wake_assignee_on_accept`. It defaults to `wake_assignee` for every kind except `request_confirmation`, which defaults to `none`.
|
||||
|
||||
Permissions:
|
||||
|
||||
@@ -1102,6 +1102,8 @@ After a terminal action, the interaction is sealed — further responses are rej
|
||||
| `suggest_tasks` | The agent has identified work it could do next and wants the board (or user) to choose which to spin up as subtasks. |
|
||||
| `ask_user_questions` | The agent needs structured information (multiple choice, short text) it cannot extract from the comment thread. |
|
||||
| `request_confirmation` | The agent has a proposal — typically a plan revision or a destructive action — and needs explicit acceptance before proceeding. |
|
||||
| `request_checkbox_confirmation` | The agent needs a decision over one or more explicitly listed checkbox options. |
|
||||
| `request_item_verdicts` | The agent needs a verdict for each item in a supplied list. |
|
||||
|
||||
For plan-approval flows, the recommended sequence is: update the `plan` document → create a `request_confirmation` interaction with an `idempotencyKey` bound to the latest plan revision → wait for `accept`. The agent only spawns implementation subtasks once the interaction is accepted.
|
||||
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
---
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Summary Slots
|
||||
|
||||
A **summary slot** is a place in the UI that always holds a short, current summary of what is going on — at the top of a project, a project workspace, or the workspaces overview. You do not write these summaries yourself. You ask for one, Paperclip's built-in **Summarizer** agent goes and writes it, and the slot holds the result along with its revision history.
|
||||
|
||||
Think of a slot as a labelled shelf rather than a document. The shelf is identified by *what it describes* (a project, say) and *where on the page it sits* (the header). Whatever the Summarizer last wrote sits on that shelf, and each new generation appends a revision instead of overwriting the old one.
|
||||
|
||||
Summary slots are company-scoped, and company access is enforced on every request. If you are new to the API, read the [API Overview](./overview.md) first for base URL, authentication, and error-code conventions — this page builds on those and won't repeat them.
|
||||
|
||||
> **Experimental — off by default.** Summary slots are gated behind an instance experimental setting. Until an operator turns on `enableSummaries`, **every** endpoint on this page responds `404 Not Found` with `{ "error": "Summaries are not enabled" }`. Because the feature is still experimental, treat the request and response shapes below as subject to change between releases.
|
||||
|
||||
---
|
||||
|
||||
## Enabling summaries
|
||||
|
||||
Every route here starts by asking the instance whether summaries are on. The server reads the instance's experimental settings, and if `enableSummaries` is not `true` it stops immediately with:
|
||||
|
||||
```json
|
||||
{ "error": "Summaries are not enabled" }
|
||||
```
|
||||
|
||||
returned as `404 Not Found`. Turn on summaries in the instance experimental settings before calling anything on this page — it is an instance-wide switch, not a per-company one.
|
||||
|
||||
You also need the Summarizer built-in agent provisioned and ready for the company. Generation refuses to start otherwise; see [Asking for a summary](#asking-for-a-summary) below and the [Built-in Agents](./built-in-agents.md) page for how to provision one.
|
||||
|
||||
---
|
||||
|
||||
## How a slot is identified
|
||||
|
||||
A slot is addressed by three things, which together make up the path and query of every request:
|
||||
|
||||
| Part | Where it goes | Meaning |
|
||||
|---|---|---|
|
||||
| `scopeKind` | Path | What kind of thing the summary is about. |
|
||||
| `scopeId` | Query string (or request body on writes) | Which specific thing, as a UUID. |
|
||||
| `slotKey` | Path | Which slot on that thing. |
|
||||
|
||||
### Scope kinds
|
||||
|
||||
| `scopeKind` | Describes | Needs `scopeId`? |
|
||||
|---|---|---|
|
||||
| `project` | A single project. | Yes — the project's UUID. |
|
||||
| `project_workspace` | A single project workspace. | Yes — the workspace's UUID. |
|
||||
| `workspaces_overview` | The company-wide workspaces overview. | No. |
|
||||
|
||||
The rule is strict in both directions. A `project` or `project_workspace` slot without a `scopeId` is rejected, and a `workspaces_overview` slot that *does* include one is rejected too — the message is `workspaces_overview summary slots must not include scopeId`. Either way you get `422 Unprocessable Entity` with `{ "error": "Invalid summary slot selector" }` and the validation details.
|
||||
|
||||
The scope target also has to live inside your company. Pointing a `project` or `project_workspace` slot at something belonging to another company comes back as `404 Not Found` with `{ "error": "Summary target not found" }`.
|
||||
|
||||
### Slot keys
|
||||
|
||||
Today there is exactly one slot key: `header` — the summary that appears at the top of the page. The field exists so more slots can be added later without changing the shape of these routes.
|
||||
|
||||
### Slot fields
|
||||
|
||||
Reading a slot gives you the slot record itself plus the document it points at. The slot carries:
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `id` | The slot's UUID. |
|
||||
| `companyId`, `scopeKind`, `scopeId`, `slotKey` | The identity described above. |
|
||||
| `documentId` | The document holding the summary text, or `null` before the first write. |
|
||||
| `status` | `idle`, `generating`, or `failed`. |
|
||||
| `failureReason` | Plain-language explanation when `status` is `failed`, otherwise `null`. |
|
||||
| `generatingIssueId` | The generation task currently in flight, or `null`. |
|
||||
| `lastGeneratedAt` | When the last summary was written. |
|
||||
| `lastGeneratedByAgentId` | The agent that wrote it. |
|
||||
| `lastModel` | The model the Summarizer reported using for that write. |
|
||||
| `createdAt`, `updatedAt` | Timestamps. |
|
||||
|
||||
### Status values
|
||||
|
||||
| Status | What it means |
|
||||
|---|---|
|
||||
| `idle` | Nothing is running. Either there is a summary to read, or none has been generated yet. |
|
||||
| `generating` | A generation task is in flight. |
|
||||
| `failed` | The generation task ended without producing a summary. `failureReason` explains why. |
|
||||
|
||||
A slot lands in `failed` when its generation task reaches `done` or `cancelled` without a summary ever being written. The server sets `failureReason` for you — either "Summary generation task {label} was cancelled before writing a summary." or "Summary generation task {label} finished without writing a summary." — so a stuck slot never sits on `generating` forever.
|
||||
|
||||
---
|
||||
|
||||
## Read a Summary Slot
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/summary-slots/{scopeKind}/{slotKey}
|
||||
```
|
||||
|
||||
Return the current state of a slot. Pass `?scopeId={uuid}` for `project` and `project_workspace` scopes; omit it for `workspaces_overview`.
|
||||
|
||||
The response has three parts:
|
||||
|
||||
- `slot` — the slot record described above, or `null` if nobody has ever touched this slot.
|
||||
- `document` — the summary document, including its `body` (the Markdown you render), `title`, `latestRevisionId`, and `latestRevisionNumber`. `null` when no summary has been written yet.
|
||||
- `generatingIssue` — a compact view of the in-flight generation task (`id`, `identifier`, `title`, `status`, `assigneeAgentId`), or `null` when nothing is running.
|
||||
|
||||
A brand-new slot is not an error. You get `{ "slot": null, "document": null, "generatingIssue": null }` and can render an empty state with a "generate" button.
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl -sS \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
"https://paperclip.example.com/api/companies/{companyId}/summary-slots/project/header?scopeId={projectId}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Revision History
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/summary-slots/{scopeKind}/{slotKey}/revisions
|
||||
```
|
||||
|
||||
Return the slot's past summaries, newest first, up to 20 revisions. Each revision carries `revisionNumber`, `title`, `body`, `changeSummary`, `createdAt`, and the `createdByAgentId` / `createdByRunId` behind it, so you can show how the summary has moved over time.
|
||||
|
||||
If the slot exists but has no document yet, you get the slot back with an empty `revisions` array. If the slot has never existed, `slot` is `null` and `revisions` is empty.
|
||||
|
||||
---
|
||||
|
||||
## Asking for a summary
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/summary-slots/{scopeKind}/{slotKey}/generate
|
||||
```
|
||||
|
||||
This is the "refresh this summary" button. It does not write anything itself — it creates a task, assigns it to the Summarizer, and wakes the agent up. The summary appears a little later, when the Summarizer writes it back.
|
||||
|
||||
**Who can call it.** Generating is deliberately a human action: only board operators may do it, and agents cannot trigger it at all. An agent caller gets `403 Forbidden` with `{ "error": "Only board operators can generate summaries." }`. A board operator needs the `tasks:assign` permission (implicit local board users and instance admins skip the check); without it the response is `403 Forbidden` with `{ "error": "Missing permission: tasks:assign" }`.
|
||||
|
||||
**The Summarizer must be ready.** If the `summarizer` built-in agent is not provisioned and ready for the company, the request fails with `422 Unprocessable Entity`, `{ "error": "Summarizer built-in agent is not configured" }`, and a `details` object carrying `code: "summarizer_not_configured"` plus the built-in's current `status`.
|
||||
|
||||
**Request body.** Optional, and the only accepted field is `scopeId` — a convenient alternative to the query string. Unknown fields are rejected.
|
||||
|
||||
**Response.**
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `slot` | The slot, now with `status: "generating"` and its `generatingIssueId` set. |
|
||||
| `generatingIssue` | The task that was created or found (`id`, `identifier`, `title`, `status`, `assigneeAgentId`). |
|
||||
| `alreadyGenerating` | `true` when a generation was already in flight and this request joined it rather than starting a new one. |
|
||||
|
||||
**Status codes.** A newly started generation responds `202 Accepted`. When the request was deduplicated onto an existing run, it responds `200 OK` — so pressing "refresh" twice does not queue two summaries.
|
||||
|
||||
Behind the scenes the server prebuilds a snapshot of the scope's blocked, in review, in progress, and recently done issues and hands it to the Summarizer in the task description, so the agent works from a bounded, company-scoped view rather than crawling the issue list.
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://paperclip.example.com/api/companies/{companyId}/summary-slots/project/header/generate" \
|
||||
-d '{ "scopeId": "{projectId}" }'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Write a Summary
|
||||
|
||||
```
|
||||
PUT /api/companies/{companyId}/summary-slots/{scopeKind}/{slotKey}
|
||||
```
|
||||
|
||||
This is the endpoint the Summarizer calls at the end of its run. It is documented here so you can understand what happens to the slot — not because you will normally call it yourself. **Only the Summarizer built-in agent may write summaries**, and only from the run of the generation task that was created for this exact slot.
|
||||
|
||||
Request body:
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `markdown` | yes | The summary body, 1–200,000 characters. Stored with format `markdown`. |
|
||||
| `title` | no | Up to 200 characters, or `null`. |
|
||||
| `changeSummary` | no | Note attached to the new revision, up to 1,000 characters, or `null`. |
|
||||
| `baseRevisionId` | no | The revision this write is based on, for conflict detection. |
|
||||
| `generationIssueId` | no in the schema, required in practice | The generation task this write belongs to. |
|
||||
| `model` | no | The model the agent actually used, up to 200 characters. Stored on the slot as `lastModel`. |
|
||||
| `scopeId` | no | Alternative to the query-string `scopeId`. |
|
||||
|
||||
Unknown fields are rejected.
|
||||
|
||||
### The rules the write has to satisfy
|
||||
|
||||
Every one of these failures returns `403 Forbidden`:
|
||||
|
||||
- The caller must be an agent carrying the `summarizer` built-in marker for this company — otherwise `{ "error": "Only the Summarizer built-in agent may write summaries" }`.
|
||||
- The write must name its task: a missing `generationIssueId` gives `{ "error": "Summary writes must identify the active generation task" }`.
|
||||
- That task must be the one the slot is currently waiting on — `{ "error": "Summary write does not match the active generation task" }` if not, and `{ "error": "Linked generation task not found" }` if the task cannot be loaded.
|
||||
- The task must target this exact slot — `{ "error": "Generation task does not target this summary slot" }`.
|
||||
- The task must be assigned to the calling agent — `{ "error": "Generation task is not assigned to this agent" }`.
|
||||
- The write must come from that task's run — `{ "error": "Summary write must run from the linked generation task" }`.
|
||||
|
||||
Two conflicts round it out, both `409 Conflict`:
|
||||
|
||||
- If a newer generation has taken over the slot in the meantime: `{ "error": "Summary generation was superseded by a newer task" }`.
|
||||
- If `baseRevisionId` is supplied but is no longer the latest revision: `{ "error": "Summary was updated by someone else" }`, along with the `currentRevisionId` so the writer can re-anchor.
|
||||
|
||||
### What a successful write does
|
||||
|
||||
The write appends a new revision rather than replacing the old text, then settles the slot: `status` returns to `idle`, `failureReason` and `generatingIssueId` are cleared, and `lastGeneratedAt`, `lastGeneratedByAgentId`, and `lastModel` are stamped. The response contains the updated `slot`, the `document`, and the new `revision`.
|
||||
|
||||
---
|
||||
|
||||
## Activity logging
|
||||
|
||||
Both mutating routes record an entry on the company activity log against `entityType: "summary_slot"`:
|
||||
|
||||
| Action | Recorded when |
|
||||
|---|---|
|
||||
| `summary_slot.generate_requested` | Someone asks for a summary. Details include the scope, slot key, `generatingIssueId`, and `alreadyGenerating`. |
|
||||
| `summary_slot.write` | The Summarizer writes a revision. Details include the scope, slot key, `documentId`, `revisionId`, and `revisionNumber`. |
|
||||
|
||||
See the [Activity](./activity.md) page for how to read the log.
|
||||
|
||||
---
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [Built-in Agents](./built-in-agents.md) — how to provision the Summarizer, and what its lifecycle states mean.
|
||||
- [Issues](./issues.md) — the generation tasks created by `POST .../generate` are ordinary issues.
|
||||
- [Goals and Projects](./goals-and-projects.md) — the projects and workspaces a summary slot can be scoped to.
|
||||
- [API Overview](./overview.md) — base URL, authentication, company scoping, and the shared error-code table.
|
||||
@@ -0,0 +1,529 @@
|
||||
---
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Tool Gateway
|
||||
|
||||
Your agents are good at deciding *what* to do. The **tool gateway** is what decides whether they are allowed to actually do it. It sits between an agent and every external tool you have connected — a Model Context Protocol (MCP) server, a spreadsheet, an internal HTTP service — and brokers each call: it looks up who is asking, checks the tool-access policy, and then allows the call, blocks it, asks a human first, or defers it to a runtime that isn't ready yet. Everything it decides lands in an audit trail you can read back.
|
||||
|
||||
Around that broker sit the pieces you configure. **Applications** and **connections** are the things you connect to. A **catalog** is the list of tools a connection actually offers. A **profile** is a named bundle of "these tools are in scope", which you **bind** to a company, an agent, a project, a routine, an issue, or a gateway. **Policies** are the rules layered on top — block this, rate-limit that, ask a person before this one. And a **named MCP gateway** is a front door you can hand to an outside MCP client, so that client speaks plain MCP while Paperclip quietly applies all of the above.
|
||||
|
||||
Every route below is company-scoped, and company access is checked on every request. If you are new to the API, read the [API Overview](./overview.md) first for base URL, authentication, and error-code conventions; this page builds on those and won't repeat them.
|
||||
|
||||
> **Experimental — off by default.** The Tools, Profiles, and Apps surfaces are gated behind an instance experimental setting: an operator has to turn on `enableApps` before any of this appears in your instance. The Smoke Lab described near the end of this page is gated separately behind `enableSmokeLab`, and while that flag is off every Smoke Lab route responds `404 Not Found` with `{ "error": "Smoke lab is disabled" }`. Because the whole surface is experimental, treat the request and response shapes below as subject to change between releases.
|
||||
|
||||
---
|
||||
|
||||
## Permissions
|
||||
|
||||
Board callers need a permission key for the more sensitive routes. A local implicit board actor or an instance admin skips these checks; everyone else needs the key granted, or the request comes back `403 Forbidden` with `{ "error": "Missing permission: <key>" }`.
|
||||
|
||||
| Permission key | Covers |
|
||||
|---|---|
|
||||
| `tools:admin` | Creating, listing, and updating named MCP gateways; minting and revoking gateway tokens; managing approved stdio command templates. |
|
||||
| `tools:manage_runtime` | Listing runtime slots, and stopping or restarting them. |
|
||||
| `tools:view_audit` | Reading the gateway audit feed. |
|
||||
| `tools:manage_connections` | Syncing a connection's installs. |
|
||||
| `tools:use` | Running test calls against a connection (either this or `tools:manage_connections` is enough). |
|
||||
|
||||
Several routes that create or edit configuration don't use a permission key at all — instead they require an **active, non-viewer** company membership. A viewer gets `403 Forbidden` with `{ "error": "Viewer access is read-only" }`, and someone with no active membership gets `{ "error": "User does not have active company access" }`.
|
||||
|
||||
---
|
||||
|
||||
## The moving parts
|
||||
|
||||
### Applications and connections
|
||||
|
||||
An **application** is the thing you are connecting to, and a **connection** is one configured way of reaching it. An application has a `type` and a `status`; a connection has a `transport` — `remote_http` or `local_stdio` — plus its own `status` (`draft`, `active`, `disabled`, `archived`) and an `enabled` flag. A connection that is disabled, archived, or simply not `enabled` short-circuits every call through it with a denial (`deny_disabled_connection`), and the same is true one level up for an application that is `disabled` or `archived`.
|
||||
|
||||
### Catalog entries
|
||||
|
||||
Refreshing a connection pulls in its **catalog** — one entry per tool the connection offers, with the tool's `name`, `description`, `inputSchema`, and a `riskLevel`. Risk levels come from a fixed set: `low`, `medium`, `high`, `critical`, `read`, `write`, `destructive`. Each entry also carries a `versionHash` and a `schemaHash`, which is how Paperclip notices that a tool you previously trusted has quietly changed shape.
|
||||
|
||||
### Profiles, entries, and bindings
|
||||
|
||||
A **profile** is a named set of tools, and it has a `defaultAction` of either `deny` or `allow`. Its **entries** narrow that down. Every entry has a `selectorType` — `application`, `connection`, `catalog_entry`, `tool_name`, or `risk_level` — and an `effect` of `include` or `exclude`.
|
||||
|
||||
A profile does nothing until you **bind** it. A binding has a `targetType` of `company`, `agent`, `project`, `routine`, `issue`, or `gateway`, and when several bindings match a single call, the narrowest scope wins.
|
||||
|
||||
### Policies
|
||||
|
||||
**Policies** are the rules evaluated on every call, in `priority` order (lower first). A policy has a `policyType`, a set of `selectors` that decide which calls it applies to, optional `conditions` that must also hold, and — for rate limits — a `config`.
|
||||
|
||||
The useful types are `allow`, `block`, `require_approval`, and `rate_limit`. Two others are special: `trust_rule` policies are managed only through the trust-rule promotion and revoke routes (trying to create one directly returns `422` with `Trust rules are managed through the trust-rule promotion and revoke endpoints`), and `redact` and `validate` are rejected outright with `Tool policy type '<type>' is not supported at runtime`.
|
||||
|
||||
A `rate_limit` policy needs a positive `limit` and `windowSeconds` in its `config`, or you get `422 Unprocessable Entity` with `Rate-limit policy config requires positive numeric limit and windowSeconds`. Its optional `keyBy` decides what the limit counts against; the default buckets by company, agent, connection, and tool.
|
||||
|
||||
Conditions come in named groups — `arguments`, `actor`, `context`, `risk`, `credentialScope`, `trustBoundary`, and `timeWindow` — and all of them must hold for the policy to apply. The `arguments` group supports `fieldEquals`, `fieldNotEquals`, `fieldIn`, `fieldMatches`, `fieldExists`, and `fieldAbsent`. A `fieldMatches` pattern that looks dangerous to evaluate is rejected up front with `Tool policy fieldMatches includes an unsafe regular expression`.
|
||||
|
||||
### What a decision looks like
|
||||
|
||||
Every brokered call ends in one of five decisions: `allow`, `deny`, `require_approval`, `rate_limited`, or `defer_runtime`. Alongside the decision you get a `reasonCode` (`deny_disabled_connection`, `allow_trust_rule`, `deny_policy_block`, and friends), a human-readable `explanation`, the `effectiveProfileIds` that were in play, and the `matchedPolicyIds` that fired.
|
||||
|
||||
---
|
||||
|
||||
## Named MCP Gateways
|
||||
|
||||
A named gateway is how you hand an outside MCP client — an editor, a desktop agent, another service — a single governed endpoint. The client authenticates with a bearer token you mint, asks for a tool list, and calls tools; Paperclip resolves the token to the gateway, applies the gateway's profile and your policies, and forwards only what is allowed.
|
||||
|
||||
### List gateways
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/tools/gateways
|
||||
```
|
||||
|
||||
Requires `tools:admin`. Returns `{ "gateways": [ ... ] }`.
|
||||
|
||||
### Create a gateway
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/tools/gateways
|
||||
```
|
||||
|
||||
Requires `tools:admin`. Responds `201 Created`.
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `name` | yes | 1–160 characters, unique within the company. |
|
||||
| `profileId` | yes | The tool profile this gateway serves. |
|
||||
| `slug` / `displaySlug` | no | 1–120 characters each. |
|
||||
| `description` | no | Up to 4,000 characters, or `null`. |
|
||||
| `defaultProfileMode` | no | `gateway_only` (the default), `inherit_context_then_gateway`, or `gateway_then_context`. |
|
||||
| `contextScopeType` | no | Defaults to `none`. |
|
||||
| `contextScopeId` | no | 1–200 characters, or `null`. |
|
||||
| `agentId`, `projectId`, `issueId`, `approvalIssueId` | no | UUIDs, or `null`. |
|
||||
| `authConfig`, `headerPolicy`, `metadataPolicy`, `onDemandToolsConfig` | no | See below. |
|
||||
| `metadata` | no | Arbitrary JSON object, or `null`. |
|
||||
|
||||
An invalid payload comes back `422 Unprocessable Entity` as `{ "error": "Invalid gateway payload", "issues": [ ... ] }`.
|
||||
|
||||
The four config blocks are where a gateway's manners live, and each has a sensible default:
|
||||
|
||||
- **`headerPolicy`** — whether caller headers pass through (`callerPassthrough`, off by default with an empty `allowedHeaders`), any `staticHeaders` you always attach, `generatedMetadata` headers, and which `responseHeaders` come back. By default it forwards MCP-required headers and safe cache headers.
|
||||
- **`metadataPolicy`** — which identifiers Paperclip is allowed to tell the upstream tool about: `forwardCompanyId`, `forwardGatewayId`, `forwardProjectId`, `forwardIssueId`, `forwardAgentId`, `forwardRunId`, and `forwardCorrelationId`. Only `forwardCorrelationId` is on by default, so nothing about your company leaks unless you say so.
|
||||
- **`authConfig`** — bearer-token behaviour. By default bearer auth is enabled with a `pcgw` token prefix, a 90-day default TTL, `requireFiniteExpiry` on, and `longLivedTokenRequiresOverride` on. OAuth is present but disabled.
|
||||
- **`onDemandToolsConfig`** — off by default. When enabled it exposes two meta-tools, `search_tools` and `run_tool`, instead of a long static list, which keeps a large catalog from flooding the client's context.
|
||||
|
||||
### Update a gateway
|
||||
|
||||
```
|
||||
PATCH /api/tool-gateway/gateways/{gatewayId}
|
||||
```
|
||||
|
||||
Requires `tools:admin`. Send `companyId` in the body — without it you get `400 Bad Request` and `{ "error": "companyId is required" }`. Every create field is optional here, plus `status` (`draft`, `active`, `disabled`, `archived`). At least one field must be present.
|
||||
|
||||
### Mint a gateway token
|
||||
|
||||
```
|
||||
POST /api/tool-gateway/gateways/{gatewayId}/tokens
|
||||
```
|
||||
|
||||
Requires `tools:admin`, and `companyId` in the body. Responds `201 Created`.
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `name` | yes | 1–160 characters. |
|
||||
| `clientLabel` | yes | 1–160 characters — which client this token is for. |
|
||||
| `ownerNote` | yes | 1–1,000 characters — who owns it and why it exists. |
|
||||
| `subjectType` | no | Only `gateway_client` is accepted today. |
|
||||
| `subjectId` | no | 1–240 characters, or `null`. |
|
||||
| `allowedActions` | no | Any of `tools/list` and `tools/call`. Defaults to both. |
|
||||
| `expiresAt` | no | A date, or `null` for a non-expiring token. |
|
||||
| `expiryOverrideReason` | no | Required when `expiresAt` is `null`. |
|
||||
|
||||
Two rules are worth knowing before you script this. Asking for anything other than `gateway_client` as `subjectType` is rejected — `heartbeat_run` is runtime-managed, and `board_user` and `agent` are reserved for later OAuth and user-bound flows. And a token with no expiry is deliberately awkward: set `expiresAt` to `null` and you must also supply `expiryOverrideReason`, or you get `Non-expiring gateway tokens require an override reason`.
|
||||
|
||||
### Revoke a gateway token
|
||||
|
||||
```
|
||||
POST /api/tool-gateway/gateway-tokens/{tokenId}/revoke
|
||||
```
|
||||
|
||||
Requires `tools:admin`, and `companyId` in the body.
|
||||
|
||||
### The MCP endpoint itself
|
||||
|
||||
There are two ways to reach the same gateway — by its public id, or by its internal UUID:
|
||||
|
||||
```
|
||||
GET /mcp/gateways/{gatewayPublicId}
|
||||
POST /mcp/gateways/{gatewayPublicId}
|
||||
GET /api/tool-gateway/gateways/{gatewayId}/mcp
|
||||
POST /api/tool-gateway/gateways/{gatewayId}/mcp
|
||||
```
|
||||
|
||||
The `GET` is a small discovery document telling a client how to connect:
|
||||
|
||||
```json
|
||||
{
|
||||
"transport": "streamable_http",
|
||||
"endpoint": "/mcp/gateways/{gatewayPublicId}",
|
||||
"authentication": "bearer"
|
||||
}
|
||||
```
|
||||
|
||||
The `POST` speaks JSON-RPC. Authenticate with `Authorization: Bearer <gateway-token>`; without it you get `401 Unauthorized` and `{ "error": "Bearer token is required" }`.
|
||||
|
||||
| Method | What happens |
|
||||
|---|---|
|
||||
| `initialize` | Returns `protocolVersion` `2025-03-26`, a `capabilities` block advertising `tools`, and `serverInfo` naming `Paperclip MCP Gateway`. |
|
||||
| `notifications/initialized` | Acknowledged with `202 Accepted` and no body. |
|
||||
| `tools/list` | Returns the tools this gateway's profile allows, each with `name`, `title`, `description`, and `inputSchema`. |
|
||||
| `tools/call` | Runs a tool. Takes `params.name` and `params.arguments`; a missing name is a JSON-RPC error `-32602` with `params.name is required`. |
|
||||
|
||||
Anything else returns `404` with JSON-RPC error `-32601`, `Method not found`. When a call is refused, the JSON-RPC `error` object carries a `data.reasonCode` so you can tell a policy block apart from a broken upstream.
|
||||
|
||||
---
|
||||
|
||||
## Agent Sessions
|
||||
|
||||
Named gateways are for outside clients. An agent working inside Paperclip uses a **session** instead: a short-lived token scoped to one agent and one run.
|
||||
|
||||
### Create a session
|
||||
|
||||
```
|
||||
POST /api/tool-gateway/sessions
|
||||
```
|
||||
|
||||
Board or agent callers. An agent's own `companyId`, `agentId`, and `runId` are taken from its token; a board caller supplies them in the body along with optional `issueId`, `projectId`, and `ttlMs`. Missing any of the three required ids gives `400 Bad Request` with `{ "error": "companyId, agentId, and runId are required" }`.
|
||||
|
||||
The `201 Created` response hands back everything the agent needs:
|
||||
|
||||
```json
|
||||
{
|
||||
"sessionId": "...",
|
||||
"token": "...",
|
||||
"expiresAt": "2026-07-20T12:00:00.000Z",
|
||||
"toolsUrl": "/api/tool-gateway/tools",
|
||||
"callUrl": "/api/tool-gateway/tools/call"
|
||||
}
|
||||
```
|
||||
|
||||
### Revoke a session
|
||||
|
||||
```
|
||||
POST /api/tool-gateway/sessions/{sessionId}/revoke
|
||||
```
|
||||
|
||||
Returns `{ "sessionId": ..., "revokedAt": ... }`. An agent can only revoke its own sessions.
|
||||
|
||||
### List and call tools
|
||||
|
||||
```
|
||||
GET /api/tool-gateway/tools
|
||||
POST /api/tool-gateway/tools/call
|
||||
```
|
||||
|
||||
Both authenticate with the session token in the `x-paperclip-tool-gateway-token` header — not `Authorization`. Without it you get `401 Unauthorized` and `{ "error": "Tool gateway session token is required" }`.
|
||||
|
||||
The call body takes:
|
||||
|
||||
| Field | Required | Notes |
|
||||
|---|---|---|
|
||||
| `tool` | yes | The tool name. A missing or non-string value returns `400` with `"tool" is required and must be a string`. |
|
||||
| `parameters` | no | The tool's arguments. Defaults to `{}`. |
|
||||
| `timeoutMs` | no | How long to wait on the upstream tool. |
|
||||
| `approvedActionRequestId` | no | The approval you are cashing in, when a previous attempt returned `require_approval`. |
|
||||
| `idempotencyKey` | no | Makes a retry safe — the same key won't run the tool twice. |
|
||||
|
||||
---
|
||||
|
||||
## Approvals
|
||||
|
||||
When a policy says `require_approval`, the call becomes an **action request** rather than an immediate result. A human then decides.
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/tools/action-requests
|
||||
POST /api/tool-gateway/action-requests/{id}/approve
|
||||
POST /api/tool-gateway/action-requests/{id}/decline
|
||||
```
|
||||
|
||||
The list route takes a `status` query parameter and defaults to `pending`. Approve and decline both need `companyId`, either in the body or as a query parameter, and both require an active non-viewer membership.
|
||||
|
||||
Each action request stores a `canonicalArgumentsHash` and a redacted `canonicalArgumentsSummary`, so what a person approves is exactly what later runs — approving one call doesn't quietly bless a different set of arguments. Many requests also carry `previewMarkdown`, a readable summary of what the tool is about to do.
|
||||
|
||||
Once you have approved the same thing a few times, you can promote it into a standing rule:
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/tools/action-requests/{actionRequestId}/trust-rule
|
||||
POST /api/companies/{companyId}/tools/trust-rules/{policyId}/revoke
|
||||
GET /api/companies/{companyId}/tools/trust-rules
|
||||
```
|
||||
|
||||
A trust rule remembers the catalog `versionHash` and `schemaHash` it was created against. If the upstream tool is later quarantined, removed, or changes shape, the rule stops applying on its own and the call goes back to asking — trust is pinned to the tool you actually approved, not to its name.
|
||||
|
||||
---
|
||||
|
||||
## Redaction
|
||||
|
||||
Paperclip never stores raw tool arguments. Before anything is written to the audit trail, values are summarized and scrubbed: keys that look like secrets (`api_key`, `authorization`, `password`, `token`, `client_secret`, and similar) are replaced with `[REDACTED]`, and so are values matching known credential shapes. Long strings are truncated, and long arrays are capped. What you get back is a `redactionPlan` with a `redactedFieldCount` and the list of `redactedFields`, plus a hash you can compare across calls without ever seeing the values.
|
||||
|
||||
---
|
||||
|
||||
## Runtime Slots
|
||||
|
||||
A `local_stdio` connection runs a real process, and a **runtime slot** is Paperclip's handle on it — its status, health, last error, and idle deadline.
|
||||
|
||||
```
|
||||
GET /api/tool-gateway/runtime-slots?companyId={companyId}
|
||||
POST /api/tool-gateway/runtime-slots/{slotId}/stop
|
||||
POST /api/tool-gateway/runtime-slots/{slotId}/restart
|
||||
```
|
||||
|
||||
All three require `tools:manage_runtime`. The same operations are also available under the company path:
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/tools/runtime-slots
|
||||
POST /api/companies/{companyId}/tools/runtime-slots/{id}/stop
|
||||
POST /api/companies/{companyId}/tools/runtime-slots/{id}/restart
|
||||
GET /api/companies/{companyId}/tools/runtime-health
|
||||
```
|
||||
|
||||
Because stdio connections launch commands, they are constrained by an approved list of **command templates**:
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/tools/stdio-templates
|
||||
POST /api/companies/{companyId}/tools/stdio-templates
|
||||
POST /api/companies/{companyId}/tools/stdio-templates/{templateId}/disable
|
||||
```
|
||||
|
||||
All three require `tools:admin`. A template pins a `command`, its `args`, the `envKeys` it may read, and the `tools` it provides — so a connection can only ever start something an admin has already vetted.
|
||||
|
||||
---
|
||||
|
||||
## The Audit Feed
|
||||
|
||||
```
|
||||
GET /api/tool-gateway/audit?companyId={companyId}
|
||||
```
|
||||
|
||||
Requires `tools:view_audit`. This is the record of everything the gateway did.
|
||||
|
||||
| Query parameter | Notes |
|
||||
|---|---|
|
||||
| `window` | `1h`, `24h`, `7d`, or `30d`. Defaults to `24h`. Anything else returns `400` with `window must be one of 1h, 24h, 7d, 30d`. |
|
||||
| `limit` | 1–100. Defaults to `100`. |
|
||||
| `app` | An `applicationId` or `connectionId` UUID. A non-UUID returns `400` with `app must be an applicationId or connectionId UUID`. |
|
||||
| `agent` | An `agentId` UUID; likewise validated. |
|
||||
| `outcome` | See the table below. |
|
||||
| `search` | Free text, matched server-side against agent, app, and connection names as well as action names, tool names, and reason codes. |
|
||||
| `cursor` | The `nextCursor` from a previous page. An unreadable cursor returns `400` with `Invalid audit cursor`. |
|
||||
|
||||
Each event carries a `normalizedOutcome` — a plain-language version of what happened, so you don't have to memorize the underlying action names:
|
||||
|
||||
| `normalizedOutcome` | Means |
|
||||
|---|---|
|
||||
| `allowed` | The call went through. |
|
||||
| `asked_first` | An approval was requested before running. |
|
||||
| `waiting` | The call was deferred, usually to a runtime that wasn't ready. |
|
||||
| `blocked` | Policy or a rate limit stopped it. |
|
||||
| `failed` | It ran and errored. |
|
||||
| `unknown` | Doesn't map to any of the above. |
|
||||
|
||||
Events are also enriched for display with `agentDisplayName`, `appDisplayName`, `applicationDisplayName`, `connectionDisplayName`, and `toolDisplayName`. The response is `{ "events": [ ... ], "nextCursor": ... }`; page by feeding `nextCursor` back in until it comes back `null`.
|
||||
|
||||
The underlying actions, if you need them, are `tool_gateway.session_created`, `session_revoked`, `session_rejected`, `discovery`, `call_allowed`, `call_denied`, `call_completed`, `call_failed`, `call_deferred`, `approval_requested`, and `runtime_mcp_delivery`.
|
||||
|
||||
---
|
||||
|
||||
## Wiring up an App
|
||||
|
||||
The Apps surface is the friendly path to all of the above — pick something from a gallery, connect it, and let Paperclip generate a sensible profile and a set of ask-first rules for you.
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/tools/gallery
|
||||
POST /api/companies/{companyId}/tools/apps/connect
|
||||
POST /api/companies/{companyId}/tools/apps/{connectionId}/finish
|
||||
GET /api/companies/{companyId}/tools/apps/attention
|
||||
```
|
||||
|
||||
`connect` creates the application, the connection, and the catalog in one step, and responds `201 Created`. If the app authenticates with OAuth, the response's `auth` block comes back with a `startUrl` you send the person to. `finish` then decides `access` and, from it, builds the profile, its entries, its bindings, and the ask-first policies — the response tells you how many of each it made.
|
||||
|
||||
The `attention` route is the one to poll on a dashboard: it lists apps that need a human — a broken health check, an expired credential, newly appeared tools nobody has reviewed.
|
||||
|
||||
OAuth has its own two routes:
|
||||
|
||||
```
|
||||
POST /api/tools/oauth/{connectionId}/start
|
||||
GET /api/tools/oauth/callback
|
||||
```
|
||||
|
||||
These need a public base URL configured; without one you get `422 Unprocessable Entity` with `OAuth connections require PAPERCLIP_PUBLIC_URL or an auth public base URL`.
|
||||
|
||||
You can also start from a config file you already have:
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/tools/mcp/import-json
|
||||
```
|
||||
|
||||
This previews an MCP JSON config as a set of connection drafts. It only previews — nothing is created until you say so.
|
||||
|
||||
Examples are prebuilt, working setups you can install and smoke-test in place:
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/tools/examples
|
||||
POST /api/companies/{companyId}/tools/examples/{id}/install
|
||||
POST /api/companies/{companyId}/tools/examples/{id}/smoke
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Managing Applications, Connections, and Profiles Directly
|
||||
|
||||
If you would rather build it yourself, every object has a plain CRUD surface.
|
||||
|
||||
Applications:
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/tools/applications
|
||||
POST /api/companies/{companyId}/tools/applications
|
||||
PATCH /api/tool-applications/{applicationId}
|
||||
DELETE /api/tool-applications/{applicationId}
|
||||
```
|
||||
|
||||
Connections:
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/tools/connections
|
||||
POST /api/companies/{companyId}/tools/connections
|
||||
GET /api/tool-connections/{connectionId}
|
||||
PATCH /api/tool-connections/{connectionId}
|
||||
DELETE /api/tool-connections/{connectionId}
|
||||
POST /api/tool-connections/{connectionId}/health-check
|
||||
POST /api/tool-connections/{connectionId}/reconnect
|
||||
POST /api/tool-connections/{connectionId}/catalog/refresh
|
||||
GET /api/tool-connections/{connectionId}/catalog
|
||||
GET /api/tool-connections/{connectionId}/activity
|
||||
```
|
||||
|
||||
Deleting a connection archives it rather than dropping it, and if it was the last connection on its application, that application is archived too.
|
||||
|
||||
A connection can be **installed** onto a `company` or an `agent`, which is how you say who gets to see it at all:
|
||||
|
||||
```
|
||||
GET /api/tool-connections/{connectionId}/installs
|
||||
PUT /api/tool-connections/{connectionId}/installs
|
||||
```
|
||||
|
||||
The `PUT` replaces the whole install set and requires `tools:manage_connections`.
|
||||
|
||||
Profiles, entries, and bindings:
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/tools/profiles
|
||||
POST /api/companies/{companyId}/tools/profiles
|
||||
PATCH /api/tool-profiles/{profileId}
|
||||
DELETE /api/tool-profiles/{profileId}
|
||||
POST /api/tool-profiles/{profileId}/duplicate
|
||||
POST /api/tool-profiles/{profileId}/entries
|
||||
PATCH /api/tool-profile-entries/{entryId}
|
||||
DELETE /api/tool-profile-entries/{entryId}
|
||||
POST /api/companies/{companyId}/tools/profiles/{profileId}/bind
|
||||
POST /api/companies/{companyId}/tools/profiles/{profileId}/unbind
|
||||
GET /api/companies/{companyId}/tools/profiles/effective/agents/{agentId}
|
||||
```
|
||||
|
||||
That last one answers the question you'll actually ask most often: *given everything I have configured, what can this agent really use?*
|
||||
|
||||
When a connection's catalog refresh turns up tools your profile has never seen, they are held back rather than silently allowed:
|
||||
|
||||
```
|
||||
GET /api/tool-profiles/{profileId}/new-tools
|
||||
POST /api/tool-profiles/{profileId}/new-tools/review
|
||||
```
|
||||
|
||||
Reviewing them returns an `allowedCount` and a `keptBlockedCount`, so an upstream server can't widen its own access just by adding a tool.
|
||||
|
||||
Policies:
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/tools/policies
|
||||
POST /api/companies/{companyId}/tools/policies
|
||||
PATCH /api/companies/{companyId}/tools/policies/{policyId}
|
||||
DELETE /api/companies/{companyId}/tools/policies/{policyId}
|
||||
POST /api/companies/{companyId}/tools/policies/{policyId}/duplicate
|
||||
POST /api/companies/{companyId}/tools/policies/reorder
|
||||
```
|
||||
|
||||
Reordering rewrites `priority` in steps of 100 and must include **every** non-trust policy in the company — a partial list returns `422` with `Reorder must include every non-trust policy for the company`. A duplicated policy is always created **disabled**, so you can edit the copy before it starts affecting anything.
|
||||
|
||||
---
|
||||
|
||||
## Testing Before You Trust
|
||||
|
||||
Three routes let you rehearse a call without committing to it.
|
||||
|
||||
The dry run answers "what *would* happen":
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/tools/policy/test
|
||||
```
|
||||
|
||||
This runs the real decision engine over a hypothetical call and returns `{ "decision": ..., "auditEvent": ... }`. Nothing is executed. Set `writeAuditEvent` to `true` if you want the rehearsal recorded; otherwise `auditEvent` comes back `null`.
|
||||
|
||||
The live test actually runs the tool, as a chosen agent:
|
||||
|
||||
```
|
||||
GET /api/tool-connections/{connectionId}/test-agents
|
||||
POST /api/tool-connections/{connectionId}/test-calls
|
||||
GET /api/tool-connections/{connectionId}/test-calls/{actionRequestId}
|
||||
```
|
||||
|
||||
All three need `tools:use` or `tools:manage_connections`, and you can only test as an agent you would be allowed to assign work to. `test-agents` lists those agents along with each one's `effectiveAccess` for that connection. `test-calls` takes an `agentId`, a `toolName`, and optional `parameters`. If the call needs approval, you get an action request back and poll the third route for its status. When the gateway service isn't configured at all, these respond `501 Not Implemented` with `{ "error": "Tool gateway service is not configured" }`.
|
||||
|
||||
You can also look up how decisions landed for a specific agent run:
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/tools/runs/{runId}/decisions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Smoke Lab
|
||||
|
||||
The Smoke Lab is a self-contained rehearsal environment: a fake OAuth provider and a set of loopback fixture services, so you can exercise the whole connect-authorize-call-audit path without touching a real vendor.
|
||||
|
||||
> **Gated twice.** Smoke Lab needs the `enableSmokeLab` experimental setting turned on, *and* it refuses to run on a publicly exposed instance. While it is off, every route here responds `404 Not Found` with `{ "error": "Smoke lab is disabled" }`.
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/smoke-lab/services
|
||||
POST /api/companies/{companyId}/smoke-lab/services/start
|
||||
POST /api/companies/{companyId}/smoke-lab/services/stop
|
||||
POST /api/companies/{companyId}/smoke-lab/install-fixtures
|
||||
POST /api/companies/{companyId}/smoke-lab/reset
|
||||
```
|
||||
|
||||
`install-fixtures` creates a ready-made application, connection, catalog, and profile, responding `201 Created` the first time and `200 OK` if they already exist. `reset` puts everything back.
|
||||
|
||||
Runs record what a rehearsal actually did:
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/smoke-lab/runs
|
||||
POST /api/companies/{companyId}/smoke-lab/runs
|
||||
GET /api/companies/{companyId}/smoke-lab/runs/{runId}
|
||||
PATCH /api/companies/{companyId}/smoke-lab/runs/{runId}
|
||||
POST /api/companies/{companyId}/smoke-lab/runs/{runId}/steps
|
||||
```
|
||||
|
||||
A run has a `trigger` and a `status`; each recorded step has a `path` and a `status`, and both board users and agents can create runs and record steps — which is what lets an agent drive its own end-to-end check.
|
||||
|
||||
The lab's fake OAuth provider lives under the same prefix and behaves like a real one, so a connection you configure against it needs no special handling:
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/smoke-lab/oauth/authorize
|
||||
POST /api/companies/{companyId}/smoke-lab/oauth/authorize
|
||||
POST /api/companies/{companyId}/smoke-lab/oauth/token
|
||||
GET /api/companies/{companyId}/smoke-lab/oauth/userinfo
|
||||
POST /api/companies/{companyId}/smoke-lab/oauth/revoke
|
||||
```
|
||||
|
||||
Redirect URIs are checked against the instance's own origin, so a fixture authorization code can never be handed to an outside host.
|
||||
|
||||
---
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [API Overview](./overview.md) — base URL, authentication, company scoping, and the shared error-code table.
|
||||
- [Company Skill Policy](./company-skill-policy.md) — the same guardrail idea applied to the skills your agents can pick up.
|
||||
- [Agents](./agents.md) — the agents that profiles and gateway sessions are scoped to.
|
||||
- [Secrets](./secrets.md) — where the credentials behind a connection are stored.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
paperclip_version: v2026.609.0
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Access, Profile & Instance Admin
|
||||
@@ -192,6 +192,7 @@ paperclipai sidebar badges --company-id <company-id>
|
||||
|
||||
paperclipai inbox dismissals --company-id <company-id>
|
||||
paperclipai inbox dismiss --company-id <company-id> --payload-json '{"itemKey":"run:<run-id>"}'
|
||||
paperclipai inbox dismiss --company-id <company-id> --payload-json '{"itemKey":"attention:<attention-key>","kind":"snooze","snoozedUntil":"2026-07-13T09:00:00.000Z"}'
|
||||
```
|
||||
|
||||
| Command | Method & path | Notes |
|
||||
@@ -202,7 +203,9 @@ paperclipai inbox dismiss --company-id <company-id> --payload-json '{"itemKey":"
|
||||
| `sidebar project-preferences:update` | `PUT /api/companies/<company-id>/sidebar-preferences/me` | Company-scoped. Requires `--payload-json`. |
|
||||
| `sidebar badges` | `GET /api/companies/<company-id>/sidebar-badges` | Company-scoped badge counts. |
|
||||
| `inbox dismissals` | `GET /api/companies/<company-id>/inbox-dismissals` | Company-scoped. |
|
||||
| `inbox dismiss` | `POST /api/companies/<company-id>/inbox-dismissals` | Company-scoped. Requires `--payload-json`. |
|
||||
| `inbox dismiss` | `POST /api/companies/<company-id>/inbox-dismissals` | Company-scoped. Requires `--payload-json`; it can dismiss or snooze an item. |
|
||||
|
||||
`inbox dismiss` is the one CLI command for both actions. Its payload needs an `itemKey` beginning with `approval:`, `join:`, `run:`, or `attention:`. Omit `kind` (or set it to `dismiss`) to hide the item until newer activity makes it relevant again. To snooze it, set `kind` to `snooze` and provide a future ISO timestamp in `snoozedUntil`. A dismissal must not include `snoozedUntil`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
paperclip_version: v2026.618.0
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Environment Variables
|
||||
@@ -46,6 +46,7 @@ These variables matter most once you move beyond a default local install.
|
||||
| `PAPERCLIP_AGENT_JWT_TTL_SECONDS` | Agent JWT lifetime in seconds. |
|
||||
| `PAPERCLIP_AGENT_JWT_ISSUER` | Agent JWT issuer. |
|
||||
| `PAPERCLIP_AGENT_JWT_AUDIENCE` | Agent JWT audience. |
|
||||
| `PAPERCLIP_TOOL_ACTION_SIGNING_SECRET` | Signing secret for tool action approvals. Unlike `BETTER_AUTH_SECRET` it has no fallback — if it is unset, the server cannot issue signed tool action approvals and raises `ToolActionSigningSecretMissingError`. Set it in the instance's own environment; worktrees inherit it from `.paperclip/.env`. For local development the `.env.example` ships `paperclip-dev-tool-action-signing-secret-change-me`. |
|
||||
|
||||
Related deployment variables:
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
paperclip_version: v2026.618.0
|
||||
paperclip_version: v2026.720.0
|
||||
---
|
||||
|
||||
# Skills Reference
|
||||
@@ -199,11 +199,13 @@ Catalog skills are split into two **kinds** (`CatalogSkillKind`):
|
||||
|
||||
| Kind | Meaning | Examples |
|
||||
|---|---|---|
|
||||
| `bundled` | Core skills the app considers part of the baseline kit. | `doc-maintenance`, `issue-triage`, `task-planning`, `qa-acceptance`, `github-pr-workflow`, `wireframe` |
|
||||
| `bundled` | Core skills the app considers part of the baseline kit. | `doc-maintenance`, `issue-triage`, `task-planning`, `qa-acceptance`, `github-pr-workflow`, `wireframe`, `summarize-status` |
|
||||
| `optional` | Extra skills you opt into when you need them. | `agent-browser`, `release-announcement`, `design-critique`, `last30days` |
|
||||
|
||||
Each catalog skill also carries a `category` you can filter on. Most of the baseline kit is coding- and process-oriented, but the catalog is not limited to engineering work — for example, the `product`-category **`wireframe`** skill teaches an agent to produce low-fidelity, black-and-white UI wireframes as standalone SVG files (recommended for `designer`, `product`, and `engineer` roles), and the `research`-category **`last30days`** skill (recommended for `researcher`, `marketer`, `product-manager`, and `analyst` roles) researches what people have said about a topic across Reddit, X, YouTube, and the rest of the web in the last 30 days. A bundled skill is not the same as a *required* one: `wireframe` ships in the baseline kit but installs only when you choose it (its `defaultInstall` is `false`), so designers and product folks can pull it in without it being forced on every agent.
|
||||
|
||||
Managers get their own entry in the baseline kit: the `paperclip-operations`-category **`summarize-status`** skill (recommended for the `general` and `manager` roles) turns the current state of a scope — a project, the workspaces overview, or a single project workspace — into a short written summary and writes it back to that scope's summary slot as a new revision. It is deliberately not a task list: it opens with the one or two decisions you actually have to make, each with a recommendation, and closes with what recently shipped and where it stands. Like `wireframe`, its `defaultInstall` is `false`, so it arrives only when you pick it.
|
||||
|
||||
**Where a catalog skill's bytes come from.** Most catalog skills are shipped inside the app itself. A few are **referenced** instead — they point at an external GitHub repository pinned to a single commit, and the app fetches their files for you. `last30days`, for example, is sourced from `github.com/mvanhorn/last30days-skill` at a pinned commit. You install and manage a referenced skill exactly like any other catalog skill; the only difference is that its `source` field tells you where it originally came from (owner, repo, the `ref`/`commit` it is pinned to, and a browseable `url`). Because the pin is a specific commit, you still get the same byte-exact, version-tracked guarantees as a fully bundled skill.
|
||||
|
||||
Once installed, a catalog skill becomes an ordinary company-skill row, but it is tagged as **catalog-managed**: its `sourceType` is `catalog` and its `metadata.sourceKind` is `catalog`. The catalog kind (`bundled` / `optional`) is carried through on `metadata.catalogKind`, and the byte-exact origin it was pinned to is stored in `metadata.originHash`.
|
||||
|
||||
@@ -3239,6 +3239,30 @@
|
||||
"server/src/services/secrets.ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "light/interactions/checkbox-confirmation-card.png",
|
||||
"theme": "light",
|
||||
"viewport": "1440x900",
|
||||
"route": "/ACME/issues/{issueId}",
|
||||
"captured_against": null,
|
||||
"captured_sha": null,
|
||||
"depends_on": [
|
||||
"ui/src/pages/IssueDetail.tsx",
|
||||
"ui/src/components/IssueThreadInteractionCard.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "dark/interactions/checkbox-confirmation-card.png",
|
||||
"theme": "dark",
|
||||
"viewport": "1440x900",
|
||||
"route": "/ACME/issues/{issueId}",
|
||||
"captured_against": null,
|
||||
"captured_sha": null,
|
||||
"depends_on": [
|
||||
"ui/src/pages/IssueDetail.tsx",
|
||||
"ui/src/components/IssueThreadInteractionCard.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "light/watchdogs/watchdog-thread-outcome.png",
|
||||
"theme": "light",
|
||||
|
||||
@@ -181,7 +181,8 @@ flowchart LR
|
||||
direction TB
|
||||
docsmain["main<br/>(docs.paperclip.ing — released)"]
|
||||
nightly["nightly<br/>(preview URL — draft state)"]
|
||||
nightly -- "release PR<br/>(when parent tags)" --> docsmain
|
||||
nightly -- "release PR (squash-merged)<br/>(when parent tags)" --> docsmain
|
||||
docsmain -- "realign-nightly.mjs<br/>(required after every release —<br/>squash severs ancestry)" --> nightly
|
||||
docsmain -- "merge main → nightly<br/>(at every nightly run,<br/>absorbs hot-fix typos)" --> nightly
|
||||
end
|
||||
|
||||
@@ -335,6 +336,7 @@ Both modes use **cumulative diffs** — always from `base_release_tag` in `.sync
|
||||
- Diffs `base_release_tag → new_tag`.
|
||||
- Most of the work is already done by nightly drafts; this mode mainly verifies completeness, stamps `paperclip_version` frontmatter, and opens a single PR: `Release docs for paperclip vX.Y.Z` (merges `nightly` → `main`).
|
||||
- On merge to `main`, tag this repo `docs/vX.Y.Z`, Cloudflare deploys live.
|
||||
- **Immediately after the squash-merge, run `node scripts/sync/realign-nightly.mjs release/vX.Y.Z --push`.** The repo is squash-only, so the merge severs `main`↔`nightly` ancestry; the realign fast-forwards `nightly` onto the shipped content and re-anchors the merge-base. Skipping it makes the next nightly run fail with add/add conflicts on every page drafted that cycle.
|
||||
|
||||
### Running the skill
|
||||
|
||||
@@ -376,6 +378,38 @@ The full list with example invocations lives in [Runbook → Helper scripts (ful
|
||||
|
||||
See [`skills/sync-docs/SKILL.md`](skills/sync-docs/SKILL.md) for the full operational playbook.
|
||||
|
||||
### Gotcha: `detect-renames.mjs` cannot see cross-package moves
|
||||
|
||||
`scripts/sync/detect-renames.mjs` matches **top-level directories** under the watched roots. It cannot see a move that crosses a package boundary into a *nested* path, so it reports one as `removed_dirs_no_match` — which reads as "this surface was deleted, consider removing its doc page."
|
||||
|
||||
That failure mode is silent and it points the wrong way: toward deleting docs that should be kept. **Always confirm a `removed_dirs_no_match` against the parent commit before acting on it.**
|
||||
|
||||
The case that exposed this (parent `eedc7dd`, #9238) had three layers the helper could not report:
|
||||
|
||||
1. **The runtime moved** — `packages/adapters/acpx-local/` → `packages/adapter-utils/src/acpx-engine/`. Different package, nested path, so no match.
|
||||
2. **The identifier stayed registered on purpose** — `server/src/adapters/registry.ts` keeps a retired `acpx_local` entry so stale DB rows fail loudly instead of falling back to the `process` adapter. Gone from `AGENT_ADAPTER_TYPES`, still resolvable at runtime.
|
||||
3. **Existing rows were migrated** — `packages/db/src/migrations/0136_acpx_default_engine_migration.sql`.
|
||||
|
||||
How to check, before you touch a page:
|
||||
|
||||
```sh
|
||||
# What actually happened to the directory?
|
||||
gh api "repos/paperclipai/paperclip/commits?path=<removed-path>/src/index.ts&sha=master&per_page=3" \
|
||||
-q '.[] | "\(.sha[0:9]) \(.commit.message | split("\n")[0])"'
|
||||
|
||||
# Did the basename reappear as a nested path elsewhere in the window?
|
||||
node -e "require('/tmp/paperclip-sync/window.json').files
|
||||
.filter(f => f.filename.includes('<basename>') && f.status === 'added')
|
||||
.forEach(f => console.log(f.filename))"
|
||||
|
||||
# Is the identifier still registered anywhere?
|
||||
gh api "search/code?q=<identifier>+repo:paperclipai/paperclip" -q '.items[].path'
|
||||
```
|
||||
|
||||
**When a user-facing surface is retired, keep the page and convert it to a stub.** Do not delete it and do not duplicate its body forward under a new name — retired pages usually document config fields that a migration has already rewritten, so copying them propagates dead config. The stub should say what replaced the surface, link to the pages that now cover it, and describe the migration path. Keep the `content.json` entry and mark the title (e.g. `ACPX Local (retired)`) so someone who meets the old identifier in a runtime error can still find it by browsing.
|
||||
|
||||
`docs/reference/adapters/acpx-local.md` is the worked example. `docs/reference/cli/commands.md` is the same pattern for a page that split rather than retired — that one is intentionally orphaned from `content.json`, since its readers arrive by direct URL rather than by browsing.
|
||||
|
||||
### Hot-fixes on released docs
|
||||
|
||||
If you spot a typo or broken example on `main` (released docs), fix it on `main` directly — it deploys to `docs.paperclip.ing` on merge. The sync skill merges `main` back into `nightly` at the start of every nightly run, so the fix isn't lost.
|
||||
|
||||
@@ -320,6 +320,17 @@ export const CAPTURE_TARGETS = [
|
||||
dependsOn: ["ui/src/pages/IssueDetail.tsx"],
|
||||
themes: ["light", "dark"],
|
||||
},
|
||||
// Checkbox confirmation cards render in the issue thread. Track the owning
|
||||
// issue route so the capture pipeline can refresh the documented card.
|
||||
{
|
||||
name: "interactions/checkbox-confirmation-card",
|
||||
route: "/{prefix}/issues/{issueId}",
|
||||
dependsOn: [
|
||||
"ui/src/pages/IssueDetail.tsx",
|
||||
"ui/src/components/IssueThreadInteractionCard.tsx",
|
||||
],
|
||||
themes: ["light", "dark"],
|
||||
},
|
||||
{
|
||||
name: "work-modes/standard-mode-run",
|
||||
route: "/{prefix}/issues/{issueId}",
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env node
|
||||
// realign-nightly.mjs — re-anchor `nightly` after a squash-merged release PR.
|
||||
//
|
||||
// This repo is squash-only. Squashing a release/vX.Y.Z PR into `main` breaks
|
||||
// the ancestry between `main` and `nightly`: the squash commit has no parent
|
||||
// link to the nightly commits that produced it, so the next nightly-mode
|
||||
// "merge main into nightly" sees pages both branches created as add/add
|
||||
// conflicts. This script heals that, and must run after EVERY release merge:
|
||||
//
|
||||
// 1. fast-forward `nightly` onto the release branch (content == shipped)
|
||||
// 2. merge `origin/main` into `nightly` (re-anchors the merge-base at the
|
||||
// squash commit, so the next cycle's divergence starts fresh)
|
||||
//
|
||||
// Usage: node scripts/sync/realign-nightly.mjs <release-branch> [--push]
|
||||
// e.g. node scripts/sync/realign-nightly.mjs release/v2026.720.0
|
||||
// Without --push, everything happens locally and the push command is printed.
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
function git(...args) {
|
||||
return execFileSync("git", args, { encoding: "utf8" }).trim();
|
||||
}
|
||||
function tryGit(...args) {
|
||||
try {
|
||||
return { ok: true, out: git(...args) };
|
||||
} catch (err) {
|
||||
return { ok: false, out: (err.stdout || "") + (err.stderr || err.message) };
|
||||
}
|
||||
}
|
||||
function die(msg) {
|
||||
console.error(`realign-nightly: ${msg}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2).filter((a) => a !== "--push");
|
||||
const push = process.argv.includes("--push");
|
||||
const releaseBranch = args[0];
|
||||
if (!releaseBranch) die("usage: realign-nightly.mjs <release-branch> [--push]");
|
||||
|
||||
if (git("status", "--porcelain") !== "") die("working tree is dirty — commit or stash first, never realign over uncommitted work");
|
||||
if (!tryGit("rev-parse", "--verify", releaseBranch).ok) die(`branch '${releaseBranch}' does not exist locally`);
|
||||
|
||||
git("fetch", "origin", "main", "nightly");
|
||||
|
||||
// The squash commit on origin/main must contain the release branch's tree.
|
||||
// Compare trees, not ancestry — ancestry is exactly what squashing destroyed.
|
||||
const releaseTree = git("rev-parse", `${releaseBranch}^{tree}`);
|
||||
const mainTree = git("rev-parse", "origin/main^{tree}");
|
||||
if (releaseTree !== mainTree) {
|
||||
die(
|
||||
`origin/main's tree does not match ${releaseBranch}'s tree.\n` +
|
||||
` Either the release PR has not been merged yet, or something else landed on main since.\n` +
|
||||
` If a hot-fix landed on main after the merge, this is fine to continue by hand:\n` +
|
||||
` the ff step still applies, and the main merge will bring in the hot-fix.`,
|
||||
);
|
||||
}
|
||||
|
||||
const startBranch = git("branch", "--show-current");
|
||||
git("checkout", "nightly");
|
||||
git("merge", "--ff-only", "origin/nightly");
|
||||
|
||||
// Step 1: bring nightly's content to exactly what shipped.
|
||||
const ff = tryGit("merge", "--ff-only", releaseBranch);
|
||||
if (!ff.ok) {
|
||||
// Something landed on nightly after the release branch forked. Same content
|
||||
// guarantees a clean three-way merge for the release files themselves.
|
||||
console.log("fast-forward not possible (nightly moved since the release forked) — doing a normal merge");
|
||||
const m = tryGit("merge", releaseBranch, "-m", `Merge ${releaseBranch} into nightly (post-release realign)`);
|
||||
if (!m.ok) die(`merge of ${releaseBranch} into nightly conflicted:\n${m.out}\nResolve by hand, then run the main-merge step yourself.`);
|
||||
}
|
||||
|
||||
// Step 2: merge main so the squash commit becomes an ancestor of nightly.
|
||||
// This is what actually re-anchors merge-base(main, nightly) — do not skip it
|
||||
// even though the trees already match.
|
||||
const anchor = tryGit("merge", "origin/main", "-m", "Merge main into nightly (re-anchor after squash-merged release)");
|
||||
if (!anchor.ok) die(`merge of origin/main conflicted (unexpected — trees matched):\n${anchor.out}`);
|
||||
|
||||
const base = git("merge-base", "origin/main", "nightly");
|
||||
const mainTip = git("rev-parse", "origin/main");
|
||||
if (base !== mainTip) die(`post-check failed: merge-base(main, nightly) is ${base.slice(0, 9)}, expected main's tip ${mainTip.slice(0, 9)}`);
|
||||
|
||||
console.log(`OK — nightly realigned; main's tip ${mainTip.slice(0, 9)} is now an ancestor of nightly.`);
|
||||
if (push) {
|
||||
git("push", "origin", "nightly");
|
||||
console.log("pushed nightly to origin.");
|
||||
} else {
|
||||
console.log("not pushed — run: git push origin nightly");
|
||||
}
|
||||
if (startBranch && startBranch !== "nightly") {
|
||||
const back = tryGit("checkout", startBranch);
|
||||
if (!back.ok) console.log(`(stayed on nightly; could not return to '${startBranch}')`);
|
||||
}
|
||||
+41
-1
@@ -70,6 +70,10 @@
|
||||
"title": "Blocked Inbox",
|
||||
"file": "../docs/guides/day-to-day/blocked-inbox.md"
|
||||
},
|
||||
{
|
||||
"title": "Decisions",
|
||||
"file": "../docs/guides/day-to-day/decisions.md"
|
||||
},
|
||||
{
|
||||
"title": "Approvals",
|
||||
"file": "../docs/guides/day-to-day/approvals.md"
|
||||
@@ -364,10 +368,22 @@
|
||||
"title": "Agents",
|
||||
"file": "../docs/reference/api/agents.md"
|
||||
},
|
||||
{
|
||||
"title": "Built-in Agents",
|
||||
"file": "../docs/reference/api/built-in-agents.md"
|
||||
},
|
||||
{
|
||||
"title": "Issues",
|
||||
"file": "../docs/reference/api/issues.md"
|
||||
},
|
||||
{
|
||||
"title": "Cases",
|
||||
"file": "../docs/reference/api/cases.md"
|
||||
},
|
||||
{
|
||||
"title": "Attention",
|
||||
"file": "../docs/reference/api/attention.md"
|
||||
},
|
||||
{
|
||||
"title": "Checkbox Confirmations",
|
||||
"file": "../docs/reference/api/checkbox-confirmations.md"
|
||||
@@ -384,6 +400,18 @@
|
||||
"title": "Resource Memberships",
|
||||
"file": "../docs/reference/api/resource-memberships.md"
|
||||
},
|
||||
{
|
||||
"title": "Folders",
|
||||
"file": "../docs/reference/api/folders.md"
|
||||
},
|
||||
{
|
||||
"title": "Company Skill Policy",
|
||||
"file": "../docs/reference/api/company-skill-policy.md"
|
||||
},
|
||||
{
|
||||
"title": "Inbox Agent Policy",
|
||||
"file": "../docs/reference/api/inbox-agent-policy.md"
|
||||
},
|
||||
{
|
||||
"title": "Costs",
|
||||
"file": "../docs/reference/api/costs.md"
|
||||
@@ -396,6 +424,14 @@
|
||||
"title": "Activity",
|
||||
"file": "../docs/reference/api/activity.md"
|
||||
},
|
||||
{
|
||||
"title": "Decision Training",
|
||||
"file": "../docs/reference/api/decision-training.md"
|
||||
},
|
||||
{
|
||||
"title": "Summary Slots",
|
||||
"file": "../docs/reference/api/summary-slots.md"
|
||||
},
|
||||
{
|
||||
"title": "Dashboard",
|
||||
"file": "../docs/reference/api/dashboard.md"
|
||||
@@ -416,6 +452,10 @@
|
||||
"title": "Plugins",
|
||||
"file": "../docs/reference/api/plugins.md"
|
||||
},
|
||||
{
|
||||
"title": "Tool Gateway",
|
||||
"file": "../docs/reference/api/tool-gateway.md"
|
||||
},
|
||||
{
|
||||
"title": "Instance Admin",
|
||||
"file": "../docs/reference/api/instance-admin.md"
|
||||
@@ -709,7 +749,7 @@
|
||||
"file": "../docs/reference/adapters/cursor-cloud.md"
|
||||
},
|
||||
{
|
||||
"title": "ACPX Local",
|
||||
"title": "ACPX Local (retired)",
|
||||
"file": "../docs/reference/adapters/acpx-local.md"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -38,6 +38,7 @@ The branch model is non-negotiable: end users on the latest *released* paperclip
|
||||
- `scripts/sync/check-drift.mjs` — Phase 1.5 drift detection (finds documented surfaces missing from parent).
|
||||
- `scripts/sync/detect-renames.mjs` — Phase 3 directory rename detection (distinguishes a renamed surface from a brand-new one).
|
||||
- `scripts/sync/verify-edit.mjs` — Phase 5.5 post-edit verification (checks that authored claims still match parent code).
|
||||
- `scripts/sync/realign-nightly.mjs` — post-release realign (Phase 7): fast-forwards `nightly` onto the merged release branch and re-anchors the merge-base after the squash-merge severs ancestry.
|
||||
|
||||
## Invocation
|
||||
|
||||
@@ -72,6 +73,7 @@ The branch model is non-negotiable: end users on the latest *released* paperclip
|
||||
- Release mode: ensure on `main`. Release mode is **self-sufficient** — it does not require the `nightly` branch to exist or to have drafts. If `nightly` exists with relevant drafts, they're used as a starting point; if not, the release run computes everything from scratch.
|
||||
- Nightly mode: ensure on `nightly`. If `nightly` doesn't exist, create it from `main`.
|
||||
5. Nightly mode only: **merge `main` into `nightly` first** to absorb any hot-fix typos that landed on released docs. Resolve trivially or abort if conflicts need human attention.
|
||||
- **Ancestry guard (squash-only repo).** Before merging, check `git merge-base --is-ancestor <last release squash commit> nightly` — practically: does the merge produce add/add conflicts on pages both branches created? This repo squash-merges release PRs, which severs the ancestry between `main` and `nightly`; if the post-release realign was skipped, this merge explodes into add/add conflicts on every page nightly drafted that cycle. If that happens, do NOT resolve by hand — abort and run `node scripts/sync/realign-nightly.mjs <release-branch>` (see Phase 8 and the special case below), then retry.
|
||||
|
||||
### Phase 2 — Resolve the diff window (cumulative)
|
||||
|
||||
@@ -290,6 +292,14 @@ Both commands spin up an isolated `local_trusted` / `loopback` Paperclip instanc
|
||||
- Nightly PR-tier edits → branch `nightly-draft/<short-sha>-<surface>` off `nightly`, open PR against `nightly`.
|
||||
- Release mode → branch `release/v2026.X.Y` off `nightly`, open PR against `main` titled `Release docs for paperclip v2026.X.Y`. PR body = manifest + screenshot staleness + structured sections (below) + checklist.
|
||||
|
||||
**Squash-merge consequence.** This repo is squash-only (`gh pr merge --squash`). Squashing the release PR breaks the ancestry between `main` and `nightly` — the squash commit has no parent link to the nightly commits that produced it. Therefore, **immediately after the release PR merges**, run:
|
||||
|
||||
```sh
|
||||
node scripts/sync/realign-nightly.mjs release/v2026.X.Y --push
|
||||
```
|
||||
|
||||
It fast-forwards `nightly` onto the release branch, merges `origin/main` back in to re-anchor the merge-base at the squash commit, verifies main's tip is an ancestor of nightly, and pushes. Skipping this arms an add/add conflict trap that detonates at the next nightly run (see the special case below). Do not run any nightly-mode sync between the squash-merge and the realign.
|
||||
|
||||
PR body structured sections (each omitted if empty — never silently dropped):
|
||||
|
||||
- `### ⚠ Drift` — every drift candidate from Phase 1.5 grouped by `kind`, high-confidence first, medium-confidence prefixed with `Verify:`. Never auto-resolved — the PR explicitly asks the reviewer to act on each entry (update path, delete section, or confirm false positive).
|
||||
@@ -338,6 +348,9 @@ Fix directly on `main` of this repo. The skill's "merge main into nightly at sta
|
||||
### Renames on parent (e.g. `cli/src/commands` → `apps/cli/src/cmds`)
|
||||
Anchor-map watchers will report "no changes detected" for many runs even though parent is clearly active. Fix `anchor-map.json` to the new paths. The next run will pick up the cumulative diff against the new paths correctly.
|
||||
|
||||
### Release PR was squashed and nightly was not realigned
|
||||
Symptom: the next nightly run's "merge `main` into `nightly`" fails with add/add conflicts on pages both branches created (nightly's unstamped drafts vs main's release-stamped copies). Cause: this repo is squash-only, so the release squash commit has no ancestry link back to nightly, and the realign step (Phase 7) was skipped. Recovery: abort the merge (`git merge --abort`), then run `node scripts/sync/realign-nightly.mjs <release-branch> --push`. If the local release branch was already deleted, recreate it from the last pre-merge SHA (`git branch release/vX.Y.Z <sha>` — find it in the merged PR's head) or, if main has had no commits since the squash, `git checkout nightly && git merge origin/main -X theirs` is NOT safe (it can silently drop post-tag nightly drafts) — prefer recreating the branch. Never `git reset --hard origin/main` on nightly: it destroys drafts for parent features that shipped after the release tag.
|
||||
|
||||
### Nightly branch has open PRs against it when a release ships
|
||||
The release PR merges `nightly` → `main`. If there are open nightly-draft PRs, they get included in the release if merged into `nightly` first, or remain on `nightly` for the next release cycle if not. The skill should list open `nightly-draft/*` PRs in the release-PR body so the human reviewer can decide.
|
||||
|
||||
|
||||
Reference in new issue
Block a user