diff --git a/.gitignore b/.gitignore index 92ddf01..1660e96 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ node_modules/ # Screenshot pipeline — ephemeral seed state and scratch outputs scripts/screenshots/.seed-ids.json + +# Local scratch area (probe scripts, throwaway captures) +scratchpad/ diff --git a/.sync-state.json b/.sync-state.json index 704d626..80338bb 100644 --- a/.sync-state.json +++ b/.sync-state.json @@ -1,9 +1,9 @@ { "branch_mode": "release", - "base_release_tag": "v2026.720.0", - "base_release_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", - "last_seen_parent_sha": "e55d702916c4d3ddbcac49b697f879808b160f59", - "last_applied_manifest_hash": "cf0e78427fa140aaba0f12122aec1304058c9359d9dba15c8346d0096434a300", - "last_run_at": "2026-07-23T07:50:00Z", + "base_release_tag": "v2026.817.0", + "base_release_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "last_seen_parent_sha": "b446ff59bfd4c22ce8042f0a8a5daad5c7adc02c", + "last_applied_manifest_hash": "fb9349f69d4a3117ed52fd98db942966b0825e8bcbef8e0c20343ef47b5be3a8", + "last_run_at": "2026-08-18T00:00:00Z", "last_run_outcome": "applied" } diff --git a/PENDING.md b/PENDING.md index 9fe0b84..ad315ec 100644 --- a/PENDING.md +++ b/PENDING.md @@ -1,73 +1,31 @@ -# PENDING — nightly docs sync +# Pending doc sync — nightly -Regenerated by `/sync-docs` in **nightly mode**. This is the current cumulative view from the last parent release, rebased onto the new release baseline. +_Regenerated by `/sync-docs` (nightly mode). Cumulative base `v2026.722.0` (`e55d702`) → parent `master` HEAD `f0e6c0f`. This file reflects the current run's manifest; it is overwritten each run._ -Cumulative window: parent release `v2026.720.0` (`903bd15`) → parent `master` HEAD `7985c1e`. The compare helper saw **41 commits / 176 changed files** with **0 truncated leaves**. +Run date: 2026-08-13 · Drift: clean · Reconciliation: clean -**Quarantine bit hard this run.** The 24-hour cutoff (`2026-07-21T05:33Z`) held back **32 of the 41 commits** — parent shipped heavily in the last day. Authoring scope was the **9 settled commits / 26 files** up to `230126d8`. The held-back work is listed at the bottom; it is not lost, it simply authors on the next run once it has had time to stick. +## Applied this run (PR-tier drafts on `nightly`) -This is the first nightly run on the `v2026.720.0` baseline, so the prior run's "surfaced but not applied" backlog (summary slots, the tool-access / MCP-gateway / Smoke Lab REST surface) is gone from this file — those pages shipped in the release and now live on `main`. +New parent surfaces from the incremental window `67001ec…f0e6c0f` (40 commits): -## ✍️ Applied this run +- **Adapter setup-token / device-login authentication** — new page `docs/reference/adapters/setup-token-authentication.md` + cross-refs in `claude-code.md`, `codex.md`, `overview.md`. Grounded in `setup-token-parse.ts`, `setup-token-runner.ts`, `setup-token-session.ts`, `adapter_auth_sessions.ts`, codex `device-login-*.ts`, `codex-device-login-service.ts`. +- **Company onboarding seed** — new "Onboarding seed" section in `docs/reference/api/companies.md`. Route `POST /api/companies/{companyId}/onboarding-seed` (`routes/onboarding-seed.ts`, `services/onboarding-seed.ts`, `company_onboarding_seeds` schema, migration 0216). +- **Routines webhook replay guard** — updated `docs/reference/api/routines.md` Fire Public Trigger section for the new HMAC replay rejection (409) on timestamped `hmac_sha256` signing (parent commit 676e20a). +- **opencode model probe** — updated `docs/reference/adapters/opencode.md`: the `opencode models` availability probe is now best-effort/non-fatal. -- **Secret folders** — new page `docs/administration/secret-folders.md`, plus a cross-reference paragraph in `docs/administration/secret-scopes.md` and a nav entry under Administration. +## ⚠ Drift - Parent PR #9913 (`b94907f8`) teaches the Secrets screen to read slash-delimited secret names as a folder tree. The load-bearing fact, and the one the page is built around: **folders are derived purely from names — there is no server-side folder record.** `ui/src/pages/secrets/secret-path.ts` computes the whole tree from the name strings, so "New folder" creates nothing; it stages a path and pre-fills the create dialog's Name field with it, and the folder only becomes real once a secret is saved inside it. +None. (`sync:check-drift` against master returned 0 records. The `sync:check` npm default flagged the new onboarding-seed route against the stale ref `67001ec` — confirmed false positive; the route exists on master.) - Also covered: the Folders/Flat toggle (persisted in `localStorage` under `paperclip.secrets.viewMode`, defaulting to Folders once any name contains a slash), `?path=` deep links (which force folder view for that visit), search staying deliberately flat and global, and the `12 secrets · 3 folders` counts — where the secret count includes *all* descendants, not just direct children. +## ⚠ Reconcile - Folded in from PR #9918 (`b9f4073a`): the new `AgentSelect` in `ui/src/components/AgentMultiSelect.tsx` gives the Agent access panel a searchable picker with a **Filter agents** box. Too small for its own entry, and it belongs next to the secret it grants. +None. No previously-applied entries disappeared from the cumulative diff. - Verification: 34/34 claims confirmed against parent code. +## Screenshots -- **Cloud-managed instances skip the first-admin claim** — `docs/reference/api/instance-admin.md`, one new `**Cloud-managed instances.**` paragraph in the existing claim section. +See `SCREENSHOTS_PENDING.md` — 86 of 310 tracked screenshots are stale (driven by `AgentConfigForm.tsx`, `AgentDetail.tsx`, `IssueDetail.tsx`, `ApprovalPayload.tsx`, `NewAgent.tsx`, `CompanyEnvironments.tsx`, `NewIssueDialog.tsx`). Flagged for human review; not auto-captured. - Parent PR #9912 (`2f42a496`) adds `isCloudManagedInstance()` to `server/src/middleware/auth.ts` and gates the health bootstrap check on it. Worth reading the subtlety before touching this again: the PR changed the **health** gate only. `POST /api/bootstrap/claim` in `server/src/routes/access.ts` did *not* get the same check, so on a cloud-managed instance the route still exists and still answers. What changed is that `GET /api/health` no longer reports `bootstrap_pending`, so the UI never routes anyone to the claim screen. The paragraph is worded around that user-visible outcome and deliberately does **not** claim the route 404s on cloud. +## Notes / not documented (internal or already-covered) - Verification: 5/5 claims confirmed. - -## 🚧 Surfaced, NOT applied — needs a human call - -- **`PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN` is undocumented.** It is the env var that flips `isCloudManagedInstance()`, and it appears nowhere under `docs/`. It reads as a control-plane-internal token rather than something a self-hoster sets, so it was left out of `docs/reference/deploy/environment-variables.md` on purpose. The instance-admin paragraph was written to avoid naming it, so the page stays correct either way. **Decide:** document it as internal-only, or leave it undocumented. - - Note the drift checker cannot catch this class of gap — it verifies documented → parent, not parent → documented. - -- **HTTP log redaction has no watcher.** `15683000` (PR #7977) added `server/src/middleware/http-log-redaction.ts` exporting `HTTP_LOG_REDACT_PATHS`, and widened pino's `redact` list from just `req.headers.authorization` to also cover `cookie`, `proxy-authorization`, `set-cookie` (both directions), `x-csrf-token`, `x-xsrf-token`, and `x-api-key`. - - This is a real operator-facing guarantee — "what does Paperclip put in its logs" is a question people ask before deploying — and nothing in `docs/` mentions it. It landed in `server/src/middleware/**`, which **no watcher in `anchor-map.json` covers**. See the watcher-maintenance note below. - -## ℹ️ Context-only (no doc edit warranted) - -- `a59aa128` — `resolveCommentCreatedByRunId` nulls out unknown/malformed run ids so a stale id can't 500 a comment insert. Internal robustness fix, no documented behaviour changes. -- `631cca50` — README roadmap and four-pillars asset refresh in the parent. Parent-repo presentation, not our surface. -- CI-only churn: `.github/workflows/*`, `scripts/e2e-shard.mjs`, and the CLI test-only files under `cli/src/__tests__/`. - -## 🔧 Watcher maintenance needed in `anchor-map.json` - -1. **`packages/db/migrations` is a stale path.** Preflight returned 404. The parent's migrations live at **`packages/db/src/migrations`** — this window contains `0182_connections_v3_schema_core.sql` and `0183_connection_user_authorization_state.sql` there. The `db-migrations` watcher is `context-only` so nothing broke, but it has been silently matching nothing. - -2. **`server/src/middleware/**` is unwatched.** The log-redaction change above proves the gap: security-relevant middleware can land with no watcher hit. Worth a watcher routing to `docs/administration/**` / `docs/reference/deploy/**`. - -All other concrete watcher paths preflighted OK. - -## ✅ Clean this run - -- **Drift: 0 records** against `master` (`7985c1e`) — 8 parent paths, 57 CLI commands, 66 env vars, 389 REST routes, 18 permission keys, 4 permission roles all still exist upstream. -- **Renames: none** detected by `detect-renames.mjs`. -- **Reconciliation: none** — no previously-applied entry vanished from the cumulative diff. - -## ⏳ Held by quarantine — expect these next run - -32 commits landed inside the 24-hour window. The substantial ones: - -- **Connections v3** (`7e00f671`, `d23fbf8a`, `cac3c0fa`) — the big one. New schema core (migrations `0182`/`0183`), an `AppDefinition` Wave 1 catalog under `packages/shared/src/app-definitions/` with twelve app JSON descriptors (anthropic, github, linear, notion, slack, sentry, vercel, zapier, google-sheets, context7, plus generic api-key and oauth), and runtime subjects/grants. `packages/shared/src/tool-app-gallery.ts` was **removed** in the same sweep — that is a rename-shaped change and will need the rename pass plus a look at whatever documents the app gallery. -- **Telemetry client contract** (`bf636563`, `c81a089c`) — chunking, deterministic `batchId`, batched retry, bounded store, and a new caps/backoff config surface in `packages/shared/src/telemetry/config.ts`. Likely env-var and deploy-doc impact. -- **Tool access service** (+465/-59 in `server/src/services/tool-access.ts`, +108/-5 in the routes) — check against `docs/reference/api/tool-gateway.md`. -- **Fail loudly on invalid config files** (`68ba7cca`) — new `server/src/__tests__/config-file.test.ts`, `server/src/config-file.ts` changes. Operator-visible startup behaviour. -- **Execution policy** (`3e1dc90b`) — final-stage approval now terminates the policy instead of rewinding to stage 1. Directly contradicts anything in `docs/guides/power/execution-policy.md` that describes the old rewind. -- **Codex-local `CODEX_HOME` allowlist** (`7985c1e0`) — new `packages/adapters/codex-local/src/server/codex-home.ts`. -- Smaller: inbox archive cache fix (`59eee482`), Office issue attachments (`b565603a`), issue-release terminal status (`1ba79d82`), CLI import-selection extension fix (`f4b65c12`). - ---- - -*Manifest hash: `a60671c76280275572e7e5ede33a1d5ea9bffa59e104979d216cbe859a98a87a`* +- Large web-ui churn (document-annotation popover refactor, AgentConfigForm/agents-route growth, task-chat components) — refinements to already-documented surfaces or internal; no new user-facing surface isolated this run. +- `tool-access.ts` / `access.ts` route edits — incremental changes to already-documented tool-gateway/access surfaces. diff --git a/SCREENSHOTS_PENDING.md b/SCREENSHOTS_PENDING.md index 79cd088..c26297a 100644 --- a/SCREENSHOTS_PENDING.md +++ b/SCREENSHOTS_PENDING.md @@ -1,47 +1,23 @@ -# Screenshots pending recapture +# Screenshots pending re-capture -Regenerated by `/sync-docs` **release** run. Window: parent `v2026.720.0` → `v2026.722.0`. +_No screenshots are pending. The v2026.817.0 release overhaul recaptured the whole +registry on 2026-08-18._ -Captured PNGs go to a PR for human review — they are **never auto-pushed**. For a targeted refresh: +Every entry in `docs/user-guides/screenshots/registry.json` (342 of 342, excluding +the two hand-annotated `dashboard-overview-annotated` images, which have no capture +target) is stamped: -```sh -npm run screenshots:refresh -``` +- `captured_sha` = `213dabab4f8e1f3bb1803a2924c0fea1289fcd4c` +- `captured_against` = `upstream/candidate/release-2026.817.0` -Staleness below is filtered to `depends_on` paths that changed with **real line edits** in this window; net-zero `+0/-0` UI touches (a large batch of `Inbox.tsx` / `IssueDetail.tsx` / design-system files) are excluded as false positives. +The previous edition of this file listed 228 of 310 entries as stale against the +window `v2026.722.0…v2026.817.0`. All of them were reshot with +`npm run screenshots:refresh:all`, and 34 net-new entries were added for the +surfaces that shipped in this release (Decisions, Status Cards, secret proposals, +Chat-Style Tasks). ---- - -## Stale entries - -### `agents` + agent-config surfaces (12 shots, light + dark) - -`ui/src/components/AgentConfigForm.tsx` changed with real edits — this window adds the **Secret access** editor for run-bound agent secret grants (PR #9921), so agent-config captures may now show new UI. Recapture: - -- `agents/adapter-type-dropdown.png` -- `agents/budget-and-heartbeat-fields.png` -- `agents/claude-local-config-filled.png` -- `agents/codex-local-config.png` -- `agents/heartbeat-toggle-disabled.png` -- `agents/heartbeat-toggle-enabled.png` -- `agents/http-adapter-config.png` -- `agents/test-environment-fail.png` -- `agents/test-environment-success.png` -- `company/company-budget-setting.png` -- `onboarding/budget-field.png` -- `org/reassign.png` - -### `secrets` - -- `secrets/dispatch-check.png` (light + dark) — `server/src/services/secrets.ts` (+338 this window) -- `secrets/user-secret-definition.png` (light + dark) — `ui/src/pages/Secrets.tsx` was reworked by PR #9913 (folder view): a Folders/Flat toggle, breadcrumb row, and folder rows. These captures are of that screen. -- `secrets/per-user-value-entry.png` (light + dark) — `ui/src/pages/Secrets.tsx`, `ui/src/pages/secrets/MyUserSecretsTab.tsx`. `MyUserSecretsTab.tsx` now renders names through `SecretPathName` (muted directory, bold leaf), so slash-named secrets look different. - -## 📸 New pages with no screenshots yet - -- `docs/administration/secret-folders.md` (drafted on nightly, ships in this release) has **no image references** — no capture exists for the folder view. Worth adding: the folder view (breadcrumbs, "Up to …" row, folder rows with `N secrets · N folders` counts, Folders/Flat toggle) and the create-secret dialog opened inside a folder (folder-path prefix chip on the Name field). Needs new registry entries with `depends_on: ["ui/src/pages/Secrets.tsx", "ui/src/pages/secrets/secret-path.ts"]`. -- `docs/experimental/connections-apps.md` (new this release) has no screenshots — the Apps surfaces are experimental and still moving, so hold captures until the UX settles. - -## ⚠️ Registry hygiene (carried over) - -**38 entries have no `captured_sha` at all**, so they can never be evaluated for staleness — invisible to this check every run. Areas: `company`, `dashboard`, `export-import`, `org`, `skills`, `tasks`, `work-modes` (light + dark). Recapturing them once would stamp a SHA and bring them back into the staleness system. +`/sync-docs` regenerates this file the next time a parent change invalidates a +`depends_on` path. See +[SCREENSHOTS_PLAN_v2026.817.0.md](docs/user-guides/screenshots/SCREENSHOTS_PLAN_v2026.817.0.md) +for what changed in the pipeline itself, including the targets that had to be +repointed because the pages behind them were deleted or renamed upstream. diff --git a/docs/administration/company.md b/docs/administration/company.md index 948f5af..3ef80c7 100644 --- a/docs/administration/company.md +++ b/docs/administration/company.md @@ -104,7 +104,7 @@ Clicking **Edit** opens a dialog with three controls: 2. **Membership status** — `Active`, `Pending`, or `Suspended`. 3. **Grants** — a two-column grid of explicit permission grants. A read-only panel above the grid lists the grants the selected role provides implicitly. Checking a box here stores the grant explicitly on the member, so it persists even if the role later changes. -The full set of permission keys is `agents:create`, `skills:create`, `environments:manage`, `users:invite`, `users:manage_permissions`, `tasks:assign`, `tasks:assign_scope`, `tasks:manage_active_checkouts`, `pipelines:write`, and `joins:approve`. Each box is labelled with a human name and its underlying key for reference. Three of these — `tasks:assign_scope`, `tasks:manage_active_checkouts`, and `pipelines:write` — are never part of a role's implicit grants, so the only way a member gets them is by checking the box here. For what each key gates and which roles include it by default, see [Roles & Permissions](./roles-and-permissions.md). +The keys you will reach for most often are `agents:create`, `skills:create`, `environments:manage`, `users:invite`, `users:manage_permissions`, `tasks:assign`, `tasks:assign_scope`, `tasks:manage_active_checkouts`, `pipelines:write`, `audit:view_agent_actions`, and `joins:approve`. Each box is labelled with a human name and its underlying key for reference. Four of these — `tasks:assign_scope`, `tasks:manage_active_checkouts`, `pipelines:write`, and `audit:view_agent_actions` — are never part of a role's implicit grants, so the only way a member gets them is by checking the box here. For the complete list of keys, what each one gates, and which roles include it by default, see [Roles & Permissions](./roles-and-permissions.md). Click **Save access** to persist the changes, or **Cancel** to close the dialog without saving. diff --git a/docs/administration/plugins.md b/docs/administration/plugins.md index b1c9aca..ec77f5f 100644 --- a/docs/administration/plugins.md +++ b/docs/administration/plugins.md @@ -56,7 +56,7 @@ An errored plugin stays installed — you don't need to uninstall and reinstall ![Install a plugin](../user-guides/screenshots/light/plugins/install.png) -There are two ways to get a plugin into Paperclip. +There are two ways to install a plugin yourself. (A small set of sandbox provider plugins also arrives on its own at startup — see *Plugins that install themselves* below.) **From the Available Plugins list.** Bundled plugins ship inside the Paperclip repository. On the Plugin Manager page, scroll to the **Available Plugins** section and click **Install** next to the one you want. Paperclip installs it straight from the local checkout — no registry round trip. @@ -81,6 +81,56 @@ In both cases the install flow does the same underlying work: download the packa --- +## Plugins that install themselves + +A few plugins never wait for you to click **Install**. Sandbox provider plugins — the ones that let an agent run on external compute instead of on the machine hosting Paperclip — need to be present before the first agent run, so Paperclip provisions them while the server is starting up. If you open the Plugin Manager on a fresh instance and find a provider already sitting in **Installed Plugins** that you didn't put there, this is why. + +This used to be a Kubernetes-only shortcut. It's now a general path with a fixed list of plugins it's allowed to provision: + +| Key | Plugin it installs | +|---|---| +| `cloudflare` | `paperclip.cloudflare-sandbox-provider` | +| `daytona` | `paperclip.daytona-sandbox-provider` | +| `e2b` | `paperclip.e2b-sandbox-provider` | +| `exe-dev` | `paperclip.exe-dev-sandbox-provider` | +| `kubernetes` | `paperclip.kubernetes-sandbox-provider` | +| `modal` | `paperclip.modal-sandbox-provider` | +| `novita` | `paperclip.novita-sandbox-provider` | + +The table is the whole allowlist: nothing outside it can arrive through auto-provisioning, no matter what an instance is configured to ask for. Everything else — reference examples, first-party plugins, third-party packages — still gets installed by you, the ordinary way. + +Once provisioned, these behave like any other installed plugin: same status badges, same detail page, same Configuration tab. You still choose whether and how to use one, by configuring a sandbox environment under **Settings → Instance settings → Environments**. See [Sandbox Providers](../reference/adapters/sandbox-providers.md) for each provider's fields. + +### When it runs, and what you might not see + +Provisioning happens once per boot, before Paperclip activates the plugins that are already marked ready — so by the time the instance is serving requests, a newly provisioned provider is installed, `ready`, and running its worker. + +Two quiet outcomes are worth knowing about: + +- **The bundle isn't on disk.** Paperclip looks for the plugin's built bundle (a `dist/manifest.js` inside the bundle folder) and skips the entry without complaint when it isn't there. A local development checkout, or an image built without a particular provider, both land here. Nothing is broken — the plugin simply doesn't appear. +- **The install or load failed.** The failure is logged and boot carries on. Paperclip will never crash-loop over a plugin it couldn't provision; the cost is that this one provider is unavailable until you sort it out. So if a provider you expected is missing from the list, the server log is where the reason is. + +### What it won't undo + +Auto-provisioning is deliberately careful about decisions you've already made: + +- **Already installed?** Skipped. Any state other than uninstalled counts — including `disabled`. Turning a provisioned provider off and restarting the server does not quietly turn it back on. +- **Uninstalled it on purpose?** On a self-hosted instance that sticks: restarts won't bring it back, so uninstalling is a real decision rather than a temporary one. On a cloud-managed instance the fleet owns provisioning, so an uninstalled bundled plugin comes back on the next boot — disable it instead if you want it out of the way for good. +- **Nothing is ever auto-uninstalled.** Dropping a key from an instance's provisioning list stops future installs; it never removes a plugin that's already there. Removal is always something a person does from the Plugin Manager. + +### If you self-host + +A self-hosted instance auto-provisions exactly one key: `kubernetes`. Every other provider in the table is yours to install when you actually want it, either from **Available Plugins** or by npm package name. That's unchanged from how Paperclip behaved before this path was generalised. + +Two environment variables let you point provisioning somewhere else on disk, which is mostly useful for development images and tests: + +- `PAPERCLIP_BUNDLED_PLUGIN_ROOT` — relocates the bundled plugin catalog root. It defaults to `/app/packages/plugins`, the location inside the release image. +- `PAPERCLIP_KUBERNETES_PLUGIN_PATH` — points the `kubernetes` entry at a specific bundle path wherever it lives. This one predates the catalog and is kept for compatibility. + +On a cloud-managed instance the list comes from the fleet instead, as part of the managed configuration document the harness hands the instance ([Cloud-managed instances](../reference/deploy/environment-variables.md#cloud-managed-instances) has the details). That list is checked strictly: a key that isn't in the table above, or a bundle path that resolves outside the catalog root, makes the instance refuse to start rather than load something unexpected into the host. Failing loudly at boot is the point. + +--- + ## Enabling and disabling Each installed plugin has a power button in the actions column. The button toggles between two lifecycle transitions: @@ -233,5 +283,6 @@ A cheat sheet for operating the Plugin Manager day-to-day: | Change configuration | Detail page → Configuration tab → edit form → **Save Configuration** | | Verify a config before committing | Detail page → Configuration tab → **Test Configuration** (when available) | | Permanently remove a plugin | Trash icon on the plugin row → confirm | +| Work out why a plugin appeared without you installing it | It's probably a sandbox provider — see *Plugins that install themselves* | If you get stuck, the Status tab on any plugin detail page is the place to start — worker status, recent runs, and recent logs will usually point at the cause faster than poking at configuration. diff --git a/docs/administration/roles-and-permissions.md b/docs/administration/roles-and-permissions.md index 441bc62..087d495 100644 --- a/docs/administration/roles-and-permissions.md +++ b/docs/administration/roles-and-permissions.md @@ -36,7 +36,7 @@ A fifth option, **Unset**, appears in the role drop-down. It leaves the member w ## The permission keys -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. +There are twenty-one permission keys. Eight of them show up as defaults on one or more roles; thirteen 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 | |---|---|---| @@ -50,6 +50,7 @@ There are twenty permission keys. Eight of them show up as defaults on one or mo | `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) | +| `audit:view_agent_actions` | Open the company's **Audit** feed — the record of what agents did — and download it as CSV | — (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) | @@ -70,13 +71,14 @@ Two of the keys come in matched pairs — a *direct* key that applies a change i ### About the explicit-only keys -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: +Thirteen 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. +- **`audit:view_agent_actions`** opens the **Audit** page in the sidebar — the company-wide feed of what your agents did, and the matching **Audit** tab on an individual agent. It also covers the **Export CSV** button on that page. Without the grant, the page still loads but shows a permission notice instead of the feed, so you can hand it out to an auditor or a compliance reviewer without giving them anything else. Two exceptions bypass the check: instance admins, and a board running in local trusted mode. Worth knowing before you grant it — the export is itself recorded in the log, together with who ran it, which filters they used, and how many rows left the system. This key is separate from `tools:view_audit`: that one covers the tool gateway's call trail, this one covers agent actions across the company. - **`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. @@ -98,6 +100,27 @@ There is no "deny" layer — explicit grants only ever *add* capability, and the --- +## How agents write across issues + +The permission keys above are the human side of the model. Agents run through the same engine, but issue *write* channels — commenting, changing fields, creating child issues, and assigning work — follow one extra rule you should understand when you reason about what your agents can reach. + +For a standard-trust agent, those four channels are **default-open on any issue the agent can already read**. There is no separate per-channel grant for "comment on this issue" or "reassign that one": visibility is the single gate they share, so if an agent can read an issue, it can influence it, and if it can't read the issue, all four channels are closed. This replaces the older patchwork where each channel had its own narrow ownership, parent, or mention rule. + +Two boundaries sit on top of that default-open rule and are always enforced: + +- **A responsible user bounds every write.** An agent acts *on behalf of* a real user, and a write only lands if that responsible user is themselves authorized for it. The agent never gains reach its responsible human doesn't have. +- **Company isolation, trust, and run locks still apply.** Cross-company writes are refused at the company boundary. Agents under the low-trust review preset, agents acting outside their trust scope, and issues locked to another in-flight run are all still blocked — visibility opens the door, it doesn't remove the other walls. + +When a write *is* refused, the denial is written to explain itself: it names the boundary that fired, who can act instead, and the sanctioned path forward, so a blocked agent can route around the wall (for example, by creating a child issue for the right assignee) instead of stalling on an opaque refusal. + +### The per-run cross-issue cap + +To stop a single runaway agent run from cascading writes across the whole board, each heartbeat run may make at most **20 cross-issue writes** — comments and field updates combined — to issues *other* than the one it woke up for (`CROSS_ISSUE_INFLUENCE_LIMIT`). The count is per write, not per issue: twenty comments on the same other issue reach the cap just as twenty single writes to twenty issues would. Today the cap runs in **log-only** mode — every cross-issue write is observed and recorded, but none are rejected. Hard **enforcement begins 2026-08-11** (`CROSS_ISSUE_INFLUENCE_ENFORCE_AT`); from that date a run's 21st cross-issue write is refused. Comment records store the responsible user separately from the acting agent, so the accountability trail holds regardless of which agent did the typing. + +You can review what agents actually did — including these cross-issue writes and the before/after of each field change — from the company **Audit** feed, which requires the `audit:view_agent_actions` grant described above. + +--- + ## Instance admin — the layer above companies Everything above is company-scoped. One role sits outside any single company: **instance admin** (`instance_admin`). @@ -110,6 +133,34 @@ An instance admin can: Instance admin is granted from the Instance Access page or the CLI (`paperclipai admin user promote `), not from the company role drop-down. It is independent of company roles — a person can be an instance admin without being a member of a given company, in which case the Access page shows a banner noting the admin-level access without membership. See [Settings](./settings.md#instance-access) for the Instance Access surface, and the first instance admin is established through the one-time [board claim](./cli-auth.md) flow. +### Owner instance admin on a cloud-managed instance + +If your instance is managed by Paperclip Cloud, you probably want the person who owns the stack to be able to open Instance Settings on their own dedicated instance without asking anyone. That's what **Owner Instance Admin** is for. + +On a cloud-managed instance, people don't claim the instance the way a self-hosted operator does — they arrive from the Paperclip Cloud side carrying a *stack role*: `owner`, `admin`, `member`, or `support`. On its own, none of those roles brings instance admin with it; the control plane owns identity, and the instance never writes an instance-admin role for them. + +Owner Instance Admin is the one exception. While it's on, the person holding the `owner` stack role is treated as an instance admin of their own instance. `admin`, `member`, and `support` are not — they stay company-scoped exactly as before. + +Two things are worth understanding about how the elevation behaves: + +- **Nothing is written down.** Paperclip works the elevation out fresh on every request, at the point where the instance trusts the identity Paperclip Cloud hands it. No instance-admin role row is ever created for it. Turn the feature off and the owner drops back to company scope on the very next request, with nothing to clean up. If the instance can't read its own settings for some reason, it assumes the feature is off: the safe answer wins. +- **Stale role rows don't ride along — for cloud arrivals.** When someone arrives through the trusted Paperclip Cloud path, the authorization service deliberately skips the `instance_user_roles` lookup, and their own leftover `instance_admin` rows are purged as they authenticate. So a hand-inserted row can't quietly re-elevate a cloud tenant. Be precise about the scope, though: that exclusion follows the *actor*, not the instance. A user who signs in through an ordinary session or a board API key on the same instance is still evaluated against `instance_user_roles` the normal way, so a row left behind for that user does elevate them. Cleaning up stray rows is still worth doing. +- **The switch is `enableOwnerInstanceAdmin`.** It's on by default for cloud-managed instances and off for self-hosted, and Paperclip Cloud can set it for a whole fleet through the managed configuration document. It is a `managed`-tier flag with no card on the [Experimental](../experimental/overview.md) page, so there is no toggle to look for in the UI — on a cloud-managed instance this is the platform's setting, not yours. + +**Running Paperclip yourself?** This feature does nothing. Self-hosted instances have no trusted cloud identity path — that path only exists when the platform sets `PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN` — so the flag is off by default and there is nothing for it to elevate. Instance admin on a self-hosted instance works exactly as described above. + +#### What an elevated owner still can't do + +Instance admin on a cloud-managed instance is not the same thing as owning the machine. A handful of surfaces belong to the platform, and their limits are set in code: they apply to *every* actor on a cloud-managed instance, instance admins included, and no flag or grant lifts them. + +- **Installing adapter code.** Installing or reinstalling an external adapter package at runtime is refused (`adapter_install_platform_managed`). Adapter code runs inside the server process, so on cloud-managed instances it ships with the platform image instead of being fetched on demand. +- **Platform-provisioned environments.** Environments the platform created can't be updated or deleted (`environment_platform_managed`). The one deliberate exception is a metadata-only patch that does nothing but clear the platform markers — the recovery path for a row left stamped after it stopped being a live platform slot. Their environment variables and credential-shaped config keys are never shown to anyone, admin or not; the structural details — provider, image, template, region — stay visible so the environment still renders properly. +- **The instance execution mode.** `executionMode` in the instance's general settings is pinned by the platform (`execution_mode_platform_managed`). Saving the same value back is fine, so settings forms that echo the whole object keep working — actually changing it is refused, because switching provider would strand runs on something the platform never provisioned. +- **Manual database backups.** Triggering a database backup by hand is refused (`database_backups_platform_managed`). Backups on cloud-managed instances are the platform's job. +- **Managed feature flags.** Anything Paperclip Cloud pins for the fleet keeps its lock badge. Being an instance admin doesn't unlock those toggles — see [Experimental features](../experimental/overview.md) and [Cloud-managed instances](../reference/deploy/environment-variables.md#cloud-managed-instances). + +Outside those platform-owned surfaces, an elevated owner has the instance-admin reach described above — every company on the instance, including ones they aren't a member of. One thing not to expect, though: handing instance admin to someone *else* on a cloud-managed instance isn't done from inside the instance. Promotion writes an instance-admin role row, and those rows are deliberately ignored for people who arrive through Paperclip Cloud. Elevation there comes from the stack role plus this feature, and nothing else. + --- ## Good to know (current limits) diff --git a/docs/administration/secret-scopes.md b/docs/administration/secret-scopes.md index cc61553..44ca51a 100644 --- a/docs/administration/secret-scopes.md +++ b/docs/administration/secret-scopes.md @@ -32,7 +32,7 @@ Under a user scope, each person stores their own value for the definition, in th Definitions bind to the things that need them — an environment, an agent, a project, and other targets — which is how a run comes to need a secret in the first place: a run inherits the bound definitions of the target it runs under, and those are the secrets it must supply. -The last ingredient is attribution. Every run has a responsible user, the human behind it, and that identity is what the secret lookup resolves against. When dispatch evaluates a run, company-scoped needs resolve to the shared value, and user-scoped needs resolve to whatever the responsible user has stored. +The last ingredient is attribution. Every run has a responsible user, the human behind it, and that identity is what the secret lookup resolves against. When dispatch evaluates a run, company-scoped needs resolve to the shared value, and user-scoped needs resolve to whatever the responsible user has stored. Runs are the usual path here but not the only one — see [Actions you take yourself](#actions-you-take-yourself) for the handful of things that resolve a user-scoped secret with no run behind them. The check itself is deterministic and happens before dispatch. If the responsible user has supplied every user-scoped value the run needs, the run proceeds and the values are injected at runtime like any other secret. If any value is missing, the run does not start. @@ -42,6 +42,28 @@ The check also surfaces early. Creating a task whose runs will need a user-scope The check confirms presence, not correctness. It guarantees the responsible user has supplied a value; a value that is wrong or expired still fails wherever it is actually used. What you are buying is the elimination of one specific failure class: a run that starts, does work, and then dies mid-flight because nobody ever stored the credential it needed. +## Actions you take yourself + +Not every use of a secret is a run. A couple of things in the product reach for one the moment you click, with no run and no dispatch check involved: testing an adapter's environment while you are still setting an agent up, and signing an agent in to Claude from its settings. + +For those, you are the responsible user. Paperclip works that out from the request itself — the signed-in user, or the user an agent is acting on behalf of when the request arrives with an agent's token. It is never something the request body can nominate, so the badge presented is always the one you are actually carrying. + +If Paperclip cannot work out who you are, and the config needs a user-scoped value that is marked required, the action stops with `responsible_user_missing`. That is the same fail-closed answer dispatch gives a run whose responsible user hasn't supplied a value: nothing proceeds on a fallback identity. + +Testing a config you haven't saved yet is the interesting case. Normally a user-scoped value has to be bound to the thing asking for it, and that binding is what says this definition belongs at this key in this agent's config. A config you are still filling in has no bindings, because it hasn't been saved. So for that one action Paperclip resolves your own value directly from the definition, checking only that the value is yours. Your teammates' values stay out of reach, and the test runs on your credentials or not at all. + +Saved agents keep the stricter path. Signing a saved agent in to Claude resolves against the bindings recorded for that agent, so a required user-scoped reference that was never bound there is refused with `binding_missing` rather than quietly resolving. Company-scoped references resolve the same way in both cases. + +## Seeing who reached for a secret + +Each runtime resolution leaves a redacted entry behind, and you can read them. Open Company Settings → Secrets, select a company secret, and switch to the **Access events** tab. An entry names the consumer that asked, the outcome, and — where a user is involved — the responsible user, plus the credential owner when that turns out to be someone else. + +The consumer is recorded honestly rather than rounded off to the nearest agent. Testing an environment records the environment you selected, or a `system` consumer of `adapter_test` when you didn't select one, so an environment probe never appears in the log as though an agent had done it. + +Listing or syncing an agent's skills records the agent as the consumer with you as the actor behind it. Those two actions deliberately skip user-scoped secrets altogether, so what they contribute to the log is attribution on the company secrets the agent's config already referenced — not any new access to per-user values. + +Open a user-scoped definition and its **Access events** tab won't list entries: access events for a user-scoped secret are recorded on each member's own stored value at the moment runtime resolution occurs, not on the shared definition. + ## Choosing a scope Scope follows ownership of the credential. Keys that belong to shared infrastructure, such as a database the whole team reads, fit company scope: there is one real value, and splitting it per user would only add busywork. Keys issued to a person, such as an individual API key for an external service, fit user scope: each human already has their own, and the point is that runs use the right one. diff --git a/docs/administration/settings.md b/docs/administration/settings.md index 2bb4194..3627d77 100644 --- a/docs/administration/settings.md +++ b/docs/administration/settings.md @@ -259,7 +259,7 @@ Each flag has its own page in the [Experimental](../experimental/overview.md) se - **[Enable External Objects](../experimental/external-objects.md)** — detects external URLs in issues and shows live status for referenced pull requests and tickets. - **[Task Plan Decomposition Panel](../experimental/plan-decomposition-panel.md)** — accepted-plan decomposition history on task detail pages, for debugging sub-task creation. - **[Task Watchdogs](../experimental/task-watchdogs.md)** — per-task watchdog agents that verify stopped task subtrees and restore live paths. -- **[Cloud Sync](../experimental/cloud-sync.md)** — connect a local instance to a Paperclip Cloud upstream: preview, push, and activation review. +- **[Cloud Sync](../experimental/cloud-sync.md)** — **retired.** Removed upstream together with its experimental toggle, so it no longer appears in this list on a current build. To move a company between instances, use [company Import/Export](../how-to/back-up-and-restore-a-company.md). - **[Server Info Debug View](../experimental/server-info-debug-view.md)** — a "Server" section in the account drawer with restart time, running commit, and checkout state. - **[Auto-Restart Dev Server When Idle](../experimental/auto-restart-dev-server.md)** — only relevant under `pnpm dev:once`; restarts a stale dev boot once all local agent runs finish. For development on Paperclip itself, not production. - **[Auto-Create Recovery Tasks](../experimental/auto-create-recovery-tasks.md)** — lets the heartbeat scheduler create recovery tasks for stalled task dependency chains, with a lookback window and preview. diff --git a/docs/administration/trust-and-low-trust-review.md b/docs/administration/trust-and-low-trust-review.md index 93bc798..79e2a68 100644 --- a/docs/administration/trust-and-low-trust-review.md +++ b/docs/administration/trust-and-low-trust-review.md @@ -6,7 +6,7 @@ paperclip_version: v2026.609.0 Some work in a Paperclip company is born from input you cannot vouch for: an untrusted pull request, an external ticket, a dependency diff, or output an agent generated from any of those. The goal of trust presets is simple — keep that kind of work *contained* while it runs, and make sure nothing it produces is silently fed into a more trusted agent before a human (or a trusted reviewer) has looked at it. -This page explains the two trust presets Paperclip ships with, what actually changes when an agent or its work is treated as low-trust, and how that work gets reviewed and promoted before it counts as accepted. +This page explains the two trust presets Paperclip ships with, what actually changes when an agent or its work is treated as low-trust, how an agent still reports progress up to its parent issue without punching a hole in the boundary, and how contained work gets reviewed and promoted before it counts as accepted. It helps to read [Execution Policy](../guides/power/execution-policy.md) first — trust presets layer on top of the same execution-policy plumbing, and a low-trust run leans on the isolated workspace and sandbox mechanics described there. @@ -81,6 +81,51 @@ If you have been doing untrusted-PR review by hand with a local Docker workflow, --- +## Reporting up to a parent issue + +Most real work is split into a parent issue and children. An agent working a child usually needs to tell the parent how things are going — "found the cause, still digging" or "this is blocked on a decision you need to make". That report has to cross from one issue to another, which is exactly the kind of move the trust boundary is suspicious of. So Paperclip grants one narrow, audited hop for it, and nothing more. + +### A standard run may comment on its direct parent + +Under the `standard` preset, an agent can post a comment on the direct parent of the issue it currently has checked out — even when that parent is assigned to a different agent. Before, that comment was refused as an arbitrary cross-issue write, which could quietly stall parent/child coordination. Now the request is allowed with the decision reason `allow_direct_parent_report`. + +Every one of these has to be true for the grant to apply: + +- The action is a comment (`issue:comment`) — nothing else qualifies. +- The agent is acting through a run, and that run belongs to the same agent and the same company. +- The run's context names the issue the agent is working on. +- That issue is assigned to the acting agent (`assigneeAgentId`). +- That issue is genuinely **checked out by this run** (`checkoutRunId`) — being assigned is not enough. +- The target issue is that issue's direct `parentId`. +- The effective preset resolves to `standard`. + +Successful grants are audited. The `issue.comment_added` activity entry carries `directParentReportGrant: true`, so you can tell later which comments crossed an issue boundary on this grant rather than on ordinary access. + +### Where the line sits + +One hop, comments only. Everything else stays denied: + +- **The grandparent, and anything above it.** Ancestry is not walked here — only the immediate `parentId` counts. A comment aimed two levels up is refused. +- **Siblings**, including other children of the same parent. +- **Changing the parent issue.** An `issue:mutate` request against the parent — a status change, for instance — is still refused. +- **Writing documents on the parent.** +- **Reopening or resuming a closed parent.** If the parent is already closed, the comment itself is accepted, but any `reopen` or `resume` intent on that request is dropped and the parent's status stays exactly as it was. That holds even when the closed parent is unassigned, and even when it happens to be assigned to the reporting agent — the grant is comment-only in every case. It is the same comment-only treatment mention-based grants already get. + +### Contained children report through a sanitized relay + +A `low_trust_review` run does **not** get this grant. A direct-parent comment from a low-trust run is denied, because letting it through would carry untrusted prose straight into a higher-trust parent's context — precisely what containment exists to prevent. The one exception is the pre-existing mention path: if a trusted comment on the parent mentions the low-trust agent, that mention grant still applies (`allow_issue_mention_grant`) and the comment lands. + +That would leave a parent waiting forever when a contained child gives up, so Paperclip relays the *fact* without relaying the prose. When an issue whose effective preset is not `standard` moves to `blocked` or `cancelled` and it has a parent, Paperclip writes a short comment on the direct parent authored by `system`: a link to the child and the status it moved to. Nothing the child produced is copied — not its comment bodies, not its output. If the parent has an agent assignee and is not closed, that agent is woken so someone actually picks the stall up. + +Two details worth knowing: + +- **Only `blocked` and `cancelled` relay.** Other transitions do not — this is a stop signal, not a progress feed. +- **The relay is idempotent.** One relay per child per stop status, so an issue that flaps in and out of `blocked` does not spam its parent. Each relay is logged as `issue.comment_added` with `source: "child_stop_relay"`. + +This follows the same principle as promotion, below: the parent learns that something stopped, and a trusted reviewer still has to open the quarantined output to learn *why*. + +--- + ## Reviewing and promoting low-trust output Containment at runtime is only half the story. The other half is what happens to whatever the low-trust agent writes. diff --git a/docs/experimental/cloud-sync.md b/docs/experimental/cloud-sync.md index 0e1a814..cea30f7 100644 --- a/docs/experimental/cloud-sync.md +++ b/docs/experimental/cloud-sync.md @@ -4,6 +4,8 @@ paperclip_version: v2026.707.0 # Cloud Sync +> **Deprecated — being removed.** Host-to-host Cloud Sync has been retired upstream and replaced by full-fidelity **company Import/Export**, which moves a company — agents, projects, issues, skills, portability files — between any two instances (local-to-local, over a URL, or from GitHub) without the experimental flag or a live upstream connection. See [Back up and restore a company](../how-to/back-up-and-restore-a-company.md). This page is kept temporarily for anyone on an older build and will be removed in a future release. + **Cloud Sync** connects a locally-run Paperclip instance to a Paperclip Cloud upstream so you can push a local company — agents, projects, issues, skills, portability files — into a cloud stack. You preview exactly what would change, resolve conflicts, push, and then review activation before any automation resumes on the cloud side. ![The Cloud upstream screen](../user-guides/screenshots/light/experimental/cloud-upstream.png) diff --git a/docs/experimental/connections-apps.md b/docs/experimental/connections-apps.md index 87dc065..942a81a 100644 --- a/docs/experimental/connections-apps.md +++ b/docs/experimental/connections-apps.md @@ -58,4 +58,4 @@ This is a foundation shipped behind an experimental flag, and it's still moving: ## Where to go next - [Experimental features overview](overview.md) — how experimental flags work and what "no compatibility guarantees" means. -- [Cloud Sync](cloud-sync.md) — another connection-oriented experimental surface. +- [Cloud Sync](cloud-sync.md) — **retired**, removed upstream along with its toggle. To move a company between instances, use [company Import/Export](../how-to/back-up-and-restore-a-company.md) instead. diff --git a/docs/experimental/isolated-workspaces.md b/docs/experimental/isolated-workspaces.md index e206e97..acee7fc 100644 --- a/docs/experimental/isolated-workspaces.md +++ b/docs/experimental/isolated-workspaces.md @@ -33,6 +33,38 @@ The instance flag alone doesn't isolate anything — each project opts in: The full tour of the workspace detail screen — tabs, runtime controls, logs — is in the [Workspaces guide](../guides/projects-workflow/workspaces.md). +### When two runs want the shared checkout + +Isolated workspaces solve concurrency by giving each run its own tree. But runs that stay on the project's **shared** checkout can still collide — two agents editing the same working copy at once corrupt each other's work. The **Shared workspace concurrency** control in **Execution Workspaces** decides what happens then: + +- **Auto** *(the default)* — Concurrent runs on local/SSH runners; runs take turns in cloud sandboxes. Paperclip picks the safe behaviour for where the run actually executes. +- **Serialize** — Runs always take turns in the shared project workspace. A second run waits until the first releases the checkout, everywhere. +- **Allow** — Runs never wait for the workspace; concurrent edits are possible. Choose this only when you know your runs won't step on each other. + +The setting lives on the project policy, and if the project allows per-task overrides a single task can pick its own mode. When nothing is set, a run resolves to **Auto**. + +## When the run happens on an environment + +If a task also runs on an [environment](environments.md) — an SSH machine or a provider sandbox — there's one more question to settle: which filesystem the agent actually edits. Paperclip records that as the run's *workspace realization*, and it has two modes. + +In `copy` mode — the familiar one — your isolated workspace is shipped out to the environment, the agent works on that copy, and the result is brought back when the run ends. + +In `in_place` mode nothing is copied. The environment already holds the authoritative files and the right toolchain, so the run works directly in the environment's own tree, at the path Paperclip calls the `authoritativeRoot`, and there's no sync-back step afterwards. Your local worktree and branch still exist; the run just isn't the thing editing them. + +You don't pick this. The environment's driver or sandbox provider declares it, and Paperclip follows — there is no setting for it in project or task configuration. On a Codex run you can tell which mode you got from two places: the run log says `[paperclip] Syncing CODEX_HOME to …` in place mode instead of `[paperclip] Syncing workspace and CODEX_HOME to …`, and the agent's process gets `PAPERCLIP_WORKSPACE_REALIZATION_MODE` and `PAPERCLIP_WORKSPACE_AUTHORITATIVE_ROOT` so your own scripts can check too. + +### Writes outside the workspace now fail out loud + +This part is worth knowing even if you never touch environments, because it changes what a broken run looks like. + +In `copy` mode, an agent sandboxed to its workspace can only be given writable paths that live inside the synchronized tree — or that the environment has mapped back out of it. Hand it a writable path that satisfies neither and the run stops before the agent starts, with an error naming the path: + +> Writable sandbox path "…" is outside synchronized workspace "…" and has no outbound restore mapping. + +That failure is the point. Before, such a path was writable during the run and then quietly discarded when the workspace was copied back, so an agent could finish, verify its own work, and report success on files nobody would ever see. An error at the start is much easier to act on: move the path inside the workspace, or make it read-only. + +Absolute paths the environment declares as aliases — a `/app` that really means "the workspace" — are bound to the synchronized tree for you, and are rejected the same visible way if they point anywhere else. + ## When it's off The Workspaces sidebar item disappears (visiting `/workspaces` redirects to your issues), project and task workspace controls are hidden, and — because of the run-time enforcement above — runs execute on the primary checkout even for tasks that previously had isolated settings. Project policies and per-task settings are kept in the database and come back when you re-enable. @@ -42,6 +74,8 @@ The Workspaces sidebar item disappears (visiting `/workspaces` redirects to your - Isolation requires **both** the instance flag and the project's *Enable isolated task checkouts* policy. - Git worktree is the only implementation — the UI labels it *"Host-managed implementation: Git worktree."* - The advanced **Environment** dropdown in the same section needs the [Environments](environments.md) flag and more than one selectable environment. +- Workspace-scoped sandbox confinement — including the writable-path check above — runs on Linux hosts only. +- A Codex agent pinned to the ACP engine can't run in an in-place environment: *"In-place workspace realization requires the Codex CLI engine; ACP archive staging is not supported."* Leave the engine unset and Paperclip picks the CLI for you. ## Where to go next diff --git a/docs/experimental/overview.md b/docs/experimental/overview.md index 86783f8..997a2c0 100644 --- a/docs/experimental/overview.md +++ b/docs/experimental/overview.md @@ -29,6 +29,16 @@ All experimental flags live in one place, and they're instance-wide (they apply Changes take effect immediately — no restart, no migration. Each feature page in this section repeats the exact toggle name to look for. +## If a toggle is locked + +On an instance managed by Paperclip Cloud, some of these features are decided for you. Open the Experimental page there and you'll see a small lock badge reading **Managed by Paperclip Cloud** next to the feature's name, and its toggle greyed out — the switch still shows you whether the feature is on or off, you just can't move it. Clicking it does nothing. + +That's not a permission problem with your account. Paperclip Cloud sets those values for the fleet your instance runs in, and the app re-applies them every time it reads the settings, so the answer can't drift — nothing you or anyone else does inside the instance changes a managed value. If you need one of these features turned on or off, that's a change on the Paperclip Cloud side rather than something you flip on this page. + +Only some features are managed this way. Anything without the badge is still yours to turn on and off exactly as described above, and the two kinds sit side by side in the same list — so the badge is what tells you which is which, on any given release. + +**Self-hosted instances are unaffected.** If you run Paperclip yourself, there is no managed configuration, no lock badge, and every card on the page stays editable. What makes the difference is a single environment variable, [`PAPERCLIP_MANAGED_CONFIG`](../reference/deploy/environment-variables.md#cloud-managed-instances), which only the Paperclip Cloud harness sets. + ## The features | Feature | What it adds | @@ -38,8 +48,10 @@ Changes take effect immediately — no restart, no migration. Each feature page | [Experimental File Viewer](file-viewer.md) | Task-detail controls for browsing and previewing workspace files relative to a task. | | [External Objects](external-objects.md) | Detects external URLs in issues and shows live status for referenced pull requests, tickets, and other work objects. | | [Task Plan Decomposition Panel](plan-decomposition-panel.md) | Shows accepted-plan decomposition history on task detail pages. | +| [Chat-Style Tasks](task-chat.md) | Rebuilds the task detail page as a live conversation: chat bubbles, streaming activity that folds into a one-line summary, a three-mode composer, and a resizable Properties · Plan · Artifacts pane. | | [Task Watchdogs](task-watchdogs.md) | Watchdog agents that verify stopped task subtrees and restore live paths when work should continue. | -| [Cloud Sync](cloud-sync.md) | Connect a local instance to a Paperclip Cloud upstream: preview, push, retry, and activation review. | +| [Status Cards](status-cards.md) | A shared board of living summaries: one message per card, compiled into a watch query and kept current by the Summarizer. | +| [Cloud Sync](cloud-sync.md) | **Retired.** Host-to-host Cloud Sync has been removed upstream, toggle included — you won't find it on the Experimental page any more. Use [company Import/Export](../how-to/back-up-and-restore-a-company.md) to move a company between instances. | | [Server Info Debug View](server-info-debug-view.md) | A "Server" section in the account drawer with the server restart time and running commit. | | [Auto-Restart Dev Server When Idle](auto-restart-dev-server.md) | For Paperclip developers: restarts a stale `pnpm dev:once` boot once all local runs finish. | | [Auto-Create Recovery Tasks](auto-create-recovery-tasks.md) | Lets the scheduler create recovery tasks for stalled task dependency chains. | diff --git a/docs/experimental/plan-decomposition-panel.md b/docs/experimental/plan-decomposition-panel.md index 839146b..1642778 100644 --- a/docs/experimental/plan-decomposition-panel.md +++ b/docs/experimental/plan-decomposition-panel.md @@ -33,6 +33,17 @@ There's nothing to configure and nothing to click besides the links — the pane > **Note:** the panel only renders on tasks that actually have decomposition history. A task whose plan never spawned children shows nothing, even with the flag on. +## Where it lives with Chat-Style Tasks on + +This page describes the classic task page. If you also run the **Chat-Style Tasks** experimental feature, the same decomposition history moves into the task's properties panel, on a **Plan** tab that shows the plan document itself with the accepted-plan history underneath it — so you read the plan and what it created in one place. + +Two things worth knowing about that arrangement: + +- The centre-column **Plan decomposition** section described above is hidden while Chat-Style Tasks is on. It hasn't gone anywhere; it's in the Plan tab. +- The Plan tab shows the history on its own, without this **Task Plan Decomposition Panel** flag. Turning this flag off hides the section from the classic task page, but the Plan tab keeps showing accepted plans as long as Chat-Style Tasks is on. + +See [Issues](../guides/day-to-day/issues.md#tabs-in-the-properties-panel-experimental) for the rest of what the Plan tab holds, including approving a plan from the panel. + ## When it's off Purely hidden. Decomposition records still exist and the exactly-once guards still run — the flag only controls whether the history is displayed. Flip it back on and the history reappears. diff --git a/docs/experimental/status-cards.md b/docs/experimental/status-cards.md new file mode 100644 index 0000000..a7c05e6 --- /dev/null +++ b/docs/experimental/status-cards.md @@ -0,0 +1,148 @@ +# Status Cards + +You already know the handful of questions you ask every day. *Is the launch still blocked?* *What changed on the migration this week?* *Are we shipping, and if not, what's the next action?* Answering them means opening the board, filtering, reading, and holding it all in your head — again tomorrow. + +A **status card** does that for you and keeps the answer on a shelf. You write one message describing what you care about, and Paperclip's Summarizer turns it into a live summary that updates as the underlying work moves. The cards sit together on a shared **Status** board, so the answer is the same one everybody else sees. + +## Turn it on + +1. Go to **Settings → Instance settings → Experimental**. +2. Turn on **Status Cards** — *"Enable the experimental shared status-card board, update engine, and gated API."* + +This flag gates the whole feature, not just the UI. With it off, the **Status** item disappears from the sidebar, `/status` redirects you to the dashboard, and every status-card API route responds `404 Not Found` with `{ "error": "Status cards are not enabled" }`. + +Status Cards is one of the features Paperclip Cloud decides for you. On a Cloud-managed instance the toggle carries a **Managed by Paperclip Cloud** badge and is greyed out — see [If a toggle is locked](overview.md#if-a-toggle-is-locked). It is off by default on both Cloud and self-hosted instances, so if you run Paperclip yourself, the switch is yours to flip. + +You also need the built-in **Summarizer** agent provisioned and ready for the company. Cards can't be set up without one — the request fails with *"Summarizer built-in agent is not configured"*. + +## Creating your first card + +Open **Status** in the sidebar (it carries a *beta* badge) and click **New card**. There is one field that matters: + +> **What do you want to keep an eye on?** + +Write it the way you'd ask a colleague. The dialog's own examples are *"issues about evals"*, *"everything blocked this week"*, and *"is feature X live? if not, the exact next actions to ship it"*. A longer one works just as well: *"Keep an eye on the ID and Cloud projects. Tell me whether the service is live, and if not, the exact three actions needed to get it to production."* + +That one message does double duty, and this is the part worth internalising: it's both **what to watch** and **how to write the update**. There's no second "summary instructions" box to fill in. If you want the update to read as three bullets and a next action, say so in the same message. + +Under it, **Agent** picks who runs the card. Leave it on *Summarizer (default)* unless another agent in the company should own this one. + +![The New card dialog: a single What do you want to keep an eye on? field with worked examples beneath it, and an Agent picker](../user-guides/screenshots/light/status-cards/new-card-dialog.png) + +Click **Create card** and the card appears immediately in a *Setting up* state while the agent works. Behind the scenes Paperclip creates a real task, assigns it to that agent, and the agent: + +1. Compiles your message into structured company-search queries and writes them back with an auto-generated card title. +2. Runs those queries and writes the first full summary — in the same run, without waiting for a second task. + +Give it a moment and the tile fills in. If you want to watch it happen, the tile links to **View setup task**. + +## Reading the board + +Each tile shows the card's title, a state dot, the current summary, and a footer with how fresh it is, its update policy, and today's token and cost totals. Click a tile to open the detail drawer, which has four tabs: + +![The Status board: one tile per card, each with a state dot, its title, the live summary, and a footer showing freshness, update policy, and today's tokens and cost](../user-guides/screenshots/light/status-cards/board.png) + +- **Summary** — the current summary, plus a revision picker to read earlier ones and a list of the changes each update integrated. + +![The card detail drawer on the Summary tab, showing the current summary and its revision picker](../user-guides/screenshots/light/status-cards/detail-summary.png) +- **Settings** — the card name, the message that drives it, the agent, the update policy, and a read-only **Query debug** section showing the compiled query JSON and its version. +- **Watched issues** — the issues the compiled query matches right now, live. + +![The Watched issues tab listing the issues the compiled query matches right now](../user-guides/screenshots/light/status-cards/detail-watched-issues.png) +- **History** — every update with its trigger, tokens, cost, model, and any error. + +![The History tab, one row per update with its trigger, tokens, cost, and model](../user-guides/screenshots/light/status-cards/detail-history.png) + +Naming is automatic: leave **Card name** blank and the agent names the card when it compiles. Type a name and it's pinned — later recompiles won't overwrite it. + +Editing **What this card watches & reports** rebuilds the card: saving a changed message queues a fresh compile and the next update is a full rebuild rather than a patch. + +## Card states + +The stored state of a card is one of five values, and the board turns those into the label you see on the tile: + +| State | On the board | What it means | +| --- | --- | --- | +| `compiling` | Setting up | The card was just created or its message changed; the agent is compiling the query and writing the first summary. | +| `active` | Fresh, Stale, or Updating | The card is working normally. It reads **Fresh** when nothing is pending, **Stale** when changes have piled up since the last update, and **Updating** while a run is in flight. | +| `error` | Error | The last run ended without writing a summary. The last good summary stays on the tile and **Retry** is offered. | +| `paused_budget` | Paused — budget | The card hit its daily token cap. Automatic updates are suspended until tomorrow. | +| `paused_hours` | Paused — hours | It's outside the card's active hours. Changes batch up into one update when the window opens. | + +Archived cards get their own **Archived** row below the board. + +A card lands in `error` when its generation task reaches `done`, `cancelled`, or `blocked` without a summary being written — a blocked task is stuck waiting on a human, so Paperclip treats it as stalled rather than in flight and releases the card so you can run it again. + +## How updating works + +The important thing to know first: **noticing a change is free, writing an update is not.** Paperclip re-runs the card's compiled query in the database, compares the result with the fingerprint it stored last time, and only spends model tokens when something meaningful actually moved. Every update is a real task assigned to the card's summarizer agent, and it costs what an agent run costs. + +You choose how eager the card is in **Settings → Auto-update policy**: + +| Mode | Behaviour | What it needs | +| --- | --- | --- | +| `manual` | The default. Changes mark the card **Stale**; nothing runs until you press Refresh. | Nothing. | +| `interval` | Checks on a schedule and updates only if something changed. | An interval — the UI offers 5, 15, 30, or 60 minutes. | +| `reactive` | Updates as soon as something changes, after a debounce window. | A debounce — the UI offers 30, 60, 120, or 300 seconds — plus a cap on updates per hour (6 by default). | + +![The Settings tab showing the auto-update policy modes and the Advanced section of change triggers](../user-guides/screenshots/light/status-cards/detail-settings.png) + +### What counts as a change + +In `interval` and `reactive` mode an **Advanced** section lets you decide which changes are worth an update. Four are on by default and one is off: + +| Trigger | Default | Fires when | +| --- | --- | --- | +| `statusTransitions` | on | A watched issue became blocked, needs review, done, or cancelled. | +| `membershipChanges` | on | A new issue starts matching the query, or one drops out of it. | +| `humanComments` | on | A person comments on a watched issue. | +| `assigneeChanges` | on | A watched issue changes hands. | +| `anyUpdate` | off | Anything at all changed — noisy, and includes in-progress churn. | + +Anything you untick still shows up in the next update; it just doesn't trigger one on its own. + +### Guardrails + +Two more settings sit under **Advanced**, and both only apply to automatic updates — a manual Refresh always runs: + +- **Active hours** — a start time, an end time, and a timezone. Outside the window the card goes to **Paused — hours** and changes batch into a single update when the window reopens. Windows that cross midnight work fine. +- **Daily token cap** — once the card's updates have spent this many tokens today, it goes to **Paused — budget**. Leave it blank and Paperclip applies a default cap of 100,000 tokens per day. The day boundary is UTC. + +### Full rebuilds vs. patches + +Most updates are **incremental**: the agent gets the previous summary and only the issues that changed, and patches the text. Paperclip switches to a **full rebuild** — the whole summary rewritten from a bounded snapshot — when you ask for one explicitly, when there's no summary yet, when more than 10 issues changed at once, after you edit the message or switch the agent, after the query is recompiled, after restoring from archive, or after nine incremental updates in a row as a drift guard. + +### Issues the summary mentions + +If a summary references an issue by its identifier (`ABC-123`) or links to it, that issue joins the card's watched set even when the compiled query doesn't match it — so later changes to it still trigger updates. Unknown identifiers and issues from other companies are dropped, and the set is capped at 200. They show up under **Mentioned in the latest update** on the Watched issues tab. + +## What it costs + +Every card is honest about its spending. The tile footer shows today's tokens and cost, the board header shows the total across all active cards, and the History tab breaks it down per update with the model that was used. Archived rows show lifetime cost. + +Change detection itself costs nothing — it's database work. What you pay for is each update the card decides to write. + +## Archiving and restoring + +Archiving a card disarms it completely: no automatic updates, no watching, and any generation task in flight is cancelled. The card and its summary history are kept. + +Click **Show archived** below the board to see archived cards, then **View** to read the last summary or **Restore** to bring it back. A restored card returns stale and schedules a **full** rebuild rather than silently resuming its old schedule, so you never read a summary that quietly went out of date while it was away. + +Deleting a card is separate and permanent — it's available through the API. + +## Agents can own cards too + +Agents with permission to assign tasks can create and manage status cards through the API, and those cards appear on the same shared board. They come with tighter limits than yours: an agent can only manage cards it authored, can hold at most 20 of them, and its card message is capped at 4,000 characters instead of 20,000. The full rules are on the [Status Cards API](../reference/api/status-cards.md) page, and the bundled [Status Card Query skill](../reference/skills/bundled/paperclip-operations/status-card-query.md) teaches an agent how to use them. + +## Caveats + +- Compiles and refreshes are ordinary assigned tasks, so a busy reactive card is a recurring agent cost. Start on `manual`, then loosen it once you know the card is useful. +- The compiled query is agent-generated. If a card is watching the wrong things, check **Query debug** and the **Watched issues** tab, then reword the card's message rather than fighting the query. +- The **Query debug** section is a temporary window into the compiler while it's being tuned — don't build a workflow around it. +- Cards are company-scoped and shared. Anyone with access to the company sees every card on the board. + +## Where to go next + +- [Status Cards API](../reference/api/status-cards.md) — every route, its permissions, and the agent-authoring rules. +- [Summary Slots](../reference/api/summary-slots.md) — the other place the Summarizer writes, fixed to a project or workspace header. +- [Experimental features overview](overview.md) diff --git a/docs/experimental/task-chat.md b/docs/experimental/task-chat.md new file mode 100644 index 0000000..33ac7b5 --- /dev/null +++ b/docs/experimental/task-chat.md @@ -0,0 +1,141 @@ +# Chat-Style Tasks + +Watching an agent work on a task usually means reading two things at once: the comment thread for what it *said*, and the transcript for what it actually *did*. The interesting part — the thinking, the commands, the diffs — sits off to one side, and once the run finishes you scroll past a wall of it to find the reply. + +**Chat-Style Tasks** turns the task detail page into a single live conversation. People and agents get chat bubbles. Everything an agent does between two replies streams inline as it happens — thinking, tool calls, file diffs — and then folds itself away into a one-line summary the moment the turn is done. Properties, the plan, and the task's artifacts move into a side pane you can widen or maximize. + +## Turn it on + +1. Go to **Settings → Instance settings → Experimental**. +2. Turn on **Chat-Style Tasks** — *"Reimagines the task detail page as a live conversation with your agents: chat bubbles for people and agents, streaming activity — thinking, tool calls, diffs — that folds into a one-line summary when a turn finishes, inline plan/question/permission cards, a three-mode composer (Agent · Plan · Ask), and a resizable Properties · Plan · Artifacts pane."* + +The card carries an **Experimental** badge, and under the description the app tells you how reversible this is: *"Turning this off instantly restores the classic task page. No task data is affected."* + +This one is yours to decide. Unlike most flags on this page it's classed as a taste setting rather than a fleet setting, so it never picks up a **Managed by Paperclip Cloud** lock badge — see [If a toggle is locked](overview.md#if-a-toggle-is-locked). It's off by default on Cloud and self-hosted instances alike. + +Like every experimental flag it's instance-wide: flipping it changes the task page for everyone on the instance, not just for you. + +## What changes on the task page + +With the flag on, the thread *is* the page. Open any task and you'll notice: + +- The **Chat / Activity / Related work** tab strip is gone. Chat is the only surface, and it fills the center column with its own scroll viewport. +- The breadcrumb, title, and header badges scroll away with the messages instead of staying pinned above them. +- The description, sub-tasks table, Documents, work products, attachments, and workspace sections no longer sit in the center column — the plan and the artifacts live in the side pane instead. +- The work-mode badge disappears from the header. Mode is now something you pick per message in the composer, and each agent reply carries the mode it ran under, so a task-wide badge would be misleading. + +Everything is still there. It just moved. + +![The task page with Chat-Style Tasks on: the thread fills the centre column as a conversation, with the properties pane on the right](../user-guides/screenshots/light/task-chat/thread.png) + +## Reading the thread + +### Bubbles + +Your messages sit on the right in a solid accent bubble. Agent replies sit on the left with an author header — the agent's icon, its name, and a small chip naming the mode that request ran in. System notices are centered and quiet. Hover a bubble to reveal its timestamp. + +A message you just sent shows as **Sending…** (or **Queued**) until the server confirms it. + +### Turns + +Everything an agent does between two replies is grouped into one **turn**. While the run is live the turn is open and streaming — you watch thinking, tool calls, and diffs land in order. + +When the run finishes the turn folds itself shut and leaves a single line behind: + +> ✓ Worked · 38s · 3 tools · +34 −3 · 12.3k tokens + +Parts you don't have data for are simply left out. A run that ended badly reads **Stopped** instead of **Worked**. Click the line to unfold the whole turn again; click it once more to fold it back. + +The fold is animated when it happens in front of you, and instant for turns that were already finished when you opened the page — so history loads compact and only live work expands. + +### Thinking + +An agent's reasoning renders as a quiet block against a left rail. While it streams the header shimmers **Thinking…**; when it settles it becomes **Thought for 12s** and collapses. Click the header to read it again. + +### Tool calls and diffs + +Each tool call is one compact row: an icon for the kind of tool, its name, and the thing it acted on in monospace, with a status marker on the right. Paperclip recognises the common families — terminal, search, read, edit, web, delegation, and MCP tools — and gives each its own glyph; genuinely unknown tools get a wrench. + +Click a row to expand its result. If the call changed a file, a diff panel appears underneath with the path, a `+34 −3` count, and the changed lines. + +Where an agent had to ask permission for a call, the row also shows whether it was **allowed** or denied. + +### The live status line + +While a run is in flight, a status line sits at the bottom of the thread with a pulsing dot, a ticking elapsed timer, and a label for what's happening right now — **Thinking**, **Responding**, **Searching**, **Running a command**, **Reading files**, **Editing files**, **Fetching the web**, **Delegating**, or **Using *ToolName*** for an MCP tool. A run that hasn't started yet reads **Queued · Waiting to start**. + +When the run ends, that line is replaced in place by the folded turn summary — the same spot, so your eye doesn't have to move. + +Some states get more than a line. An agent waiting on your approval elevates into a card with the actual choices as buttons, and interruptions, refusals, and truncated runs each get their own marked state. + +### Usage + +Where the adapter reports it, a second-tier readout shows concrete progress rather than a spinner: context used out of context size with a percentage and a small fill bar, input and output tokens, and cost in dollars. It's deliberately understated so it never competes with the conversation. + +### Cards and markers + +Plan confirmations, questions, and suggested-task cards stay inline in the thread, in the place in the conversation where they happened, with their accept and reject buttons intact. A confirmation that has since expired demotes to a plain divider — superseded asks are history, not something to act on. + +When a plan document is written or revised, a divider marks the spot: **Plan created**, or **Plan updated · rev 3 — see the Plan tab**. + +### Scrolling + +The thread follows new content while you're at the bottom, and stops following the moment you scroll up. Instead of yanking you back down it shows a small round **Scroll to latest** button; click it and the thread glides back to the bottom and resumes following. Scrolling during that glide cancels it and leaves you where you are. + +## The composer + +The composer sits pinned at the bottom of the thread: a text box over a row of controls. + +**Pick a mode per message.** The chip on the left names the mode this submission will run in, tinted to match it. Open it and you get all three: + +| Mode | What it does | +| --- | --- | +| **Agent mode** | Make changes and run work | +| **Plan mode** | Draft a plan before acting | +| **Ask mode** | Answer questions only, no changes | + +The placeholder text follows your choice, so you can tell at a glance what will happen — *"Message Ada — describe what you want done…"*, *"Plan with Ada — shapes the plan doc, no code changes…"*, or *"Ask Ada a question — read-only, nothing runs…"*. Press **Shift+Tab** in the text box to cycle through the three. The mode is applied when you send, not when you pick it. + +![The composer's mode picker open, offering Agent mode, Plan mode, and Ask mode](../user-guides/screenshots/light/task-chat/composer-modes.png) + +**Attach files** with the **+** button, by pasting them, or by dropping them onto the composer. Each file becomes a chip that reads *Uploading…* and then *Attached*, with a thumbnail for images, and a link to it is added to your message. + +**Reassign as you reply.** When reassignment is available, a control on the right names the current assignee and lets you hand the task to someone else with the same message. Sending to an agent on a task that's `done`, `cancelled`, or `blocked` reopens it. + +**Send with ⌘+Enter** (or Ctrl+Enter). Plain **Enter** inserts a newline — it was too easy to send half a thought otherwise. + +## The side pane + +The properties pane on the right becomes resizable and maximizable. + +Drag its left edge to set the width; the pane remembers it, and **double-clicking the edge** resets it to the default. Use **Maximize panel** in the header to glide it out across the page for a proper read — the center column doesn't reflow while it's out — and **Restore panel** to send it back. + +Its header holds up to three tabs, and a tab only appears when it has something in it: + +- **Properties** — the same task properties as always: assignee, project, blockers, and the rest. +- **Plan** — the task's plan document with its revision number and last-updated time, above the accepted-plan history. Before there's anything to show it reads *"No plan yet. The plan document, accepted plans, and their revisions will appear here."* +- **Artifacts** — a read-only list of the task's attachments and work products with their file sizes, each one a link. Empty, it reads *"No artifacts yet. Attachments and work products will appear here."* + +With neither a plan nor artifacts, the header just says **Properties** rather than showing a one-tab strip. + +![The side pane on the right of the task page, with its Properties, Plan, and Artifacts tabs](../user-guides/screenshots/light/task-chat/side-pane.png) + +One nice touch on the Plan tab: while a plan is waiting on your confirmation, its **accept** and **send back** buttons pin to the bottom of the pane and stay there while you scroll the plan itself — so you can read the whole thing and decide without hunting for the card back in the thread. + +## When it's off + +Purely presentational. Turning the flag off restores the classic task page exactly as it was, immediately — no migration, nothing to re-enable. Comments, runs, transcripts, plans, and attachments are the same records either way; only the rendering changes. + +## Caveats + +- This is a redesign of a page you probably use all day, and it's under active iteration. Expect details — labels, spacing, which things fold — to move between releases. +- The flag is instance-wide, so turning it on changes the task page for every person on the instance. There's no per-user opt-in. +- The Artifacts tab is read-only for now. Uploading, previewing, and deleting attachments still happen on the existing attachment surfaces. +- Live structured plan checklists (an agent ticking off plan steps as it goes) depend on adapter support and aren't wired into the live thread yet — the Plan tab shows the plan document and accepted-plan history. + +## Where to go next + +- [Work Modes](../guides/day-to-day/work-modes.md) — what Agent, Plan, and Ask modes actually change about a run. +- [Issues](../guides/day-to-day/issues.md) — the task detail page, sub-tasks, and blockers. +- [Artifacts](../guides/day-to-day/artifacts.md) — where a task's outputs come from. +- [Experimental features overview](overview.md) diff --git a/docs/experimental/task-watchdogs.md b/docs/experimental/task-watchdogs.md index 2f18417..7b4dad0 100644 --- a/docs/experimental/task-watchdogs.md +++ b/docs/experimental/task-watchdogs.md @@ -24,6 +24,10 @@ Watchdogs are opt-in, per task — there's no global "watch everything" switch: Once set, the scheduler evaluates the watched subtree automatically. When everything under the task has stopped, the watchdog agent runs, posts its reasoning in the thread, and — if the stop was a mistake — creates or unblocks follow-up work inside the watched subtree. +You only hear from it once per distinct stopped state. Each time a watchdog reviews a stop, Paperclip stores a snapshot of what it saw: for every stopped task in the tree, its status, who it's assigned to, what's blocking it, and which questions or approvals it's waiting on. Later evaluations compare against that snapshot and stay quiet while it still matches — so a stopped task picking up a fresh comment, document, or work product won't re-trigger the watchdog, and neither will part of the tree finishing and dropping out. It wakes again when something material moves: a stopped task changes status, gets a different assignee, gains or loses a blocker, starts or stops waiting on a question or approval, or a new stopped task appears under the watched one. + +When it does wake, the stopped-state comment it posts lists each stopped task with what it's waiting on, so you can see at a glance why the tree came to rest. + The full lifecycle — when a watchdog wakes, what it's allowed to do, and how it differs from the other things called "watchdog" — is covered in the [Task Watchdogs guide](../guides/projects-workflow/task-watchdogs.md). ## When it's off @@ -33,7 +37,8 @@ The flag hides the configuration UI only. **Watchdogs you already configured kee ## Caveats - Watchdog runs are deliberately scope-limited: they can't change watchdog configuration, and any issues they create must stay inside the watched subtree. -- A watchdog pass costs a normal agent run — attach them to trees that matter, not everything. +- A watchdog pass costs a normal agent run — attach them to trees that matter, not everything. Deduplication means you don't pay twice for the same stopped state, but every genuinely new one is a new run. +- If a watchdog accepts a stop and you disagree, it won't take a second look at that same state. Move the tree yourself — change a status, clear a blocker, reassign — and the next evaluation sees a new stopped state. ## Where to go next diff --git a/docs/guides/day-to-day/activity-log.md b/docs/guides/day-to-day/activity-log.md index 19cdac5..6419615 100644 --- a/docs/guides/day-to-day/activity-log.md +++ b/docs/guides/day-to-day/activity-log.md @@ -4,6 +4,8 @@ The Activity Log is the complete record of everything that has ever happened in The log exists for two reasons. First, accountability: you can always see what actually happened and who did it. Second, debugging: when something goes wrong, the Activity Log is your first place to look. +If the question you're really asking is "what did my *agents* do, and on whose behalf?", you don't need a different page anymore. Activity is now one page for both questions: flip the scope toggle to narrow from every actor down to agent actions only, without leaving the feed. See [Focusing on agent actions](#focusing-on-agent-actions) below. + ![Activity Log page showing a full chronological feed of events with actor names, action descriptions, and timestamps](../../user-guides/screenshots/light/activity/activity-log-full.png) --- @@ -41,13 +43,57 @@ Each row in the feed shows the actor's name, what they did, and when. Clicking a ## Filtering -When you're looking for something specific, use the filter at the top of the Activity page. In the current UI, that filter narrows the list by entity type. +When you're looking for something specific, use the filters at the top of the Activity page. -![Activity Log with the filter bar open, showing options for agent, entity type, and time range](../../user-guides/screenshots/light/activity/activity-filters.png) +![Activity Log with the filter bar open, showing the scope toggle plus agent, responsible user, action, entity, and date range filters](../../user-guides/screenshots/light/activity/activity-filters.png) + +Start with the **scope toggle**. It switches between two views of the same feed: + +- **All actors** — everything that happened, from every kind of actor: humans, agents, plugins, and the system itself. +- **Agent actions only** — just the rows where an agent did the work. This is the default. + +Below the toggle you can narrow further: + +- **Agent** — one agent, or all of them. +- **Responsible user** — everything done on one person's behalf. +- **Action** — a category of events (for example task, agent, run, approval, project, goal, cost, or company events) rather than a single event name. +- **Entity** — the kind of thing that changed, such as a task, agent, project, goal, or company. +- **From** and **To** dates — a day range. -**Filter by entity type** — narrow to a specific category of events: tasks only, agents only, approvals only, or budget events only. ![Activity Log filtered to show one category of events](../../user-guides/screenshots/light/activity/activity-filtered-by-agent.png) +The feed loads newest first and pages as you scroll, so there's no 200-row ceiling — you can follow a thread as far back as it goes. + +--- + +## Focusing on agent actions + +Sometimes you don't want *everything* that happened; you want to answer a narrower question — what did my agents do, and who was responsible for each of those actions? Flip the scope toggle to **Agent actions only** and the feed answers exactly that, without sending you to a different page. + +Two things stand out in this view: + +- **Every row has an agent attached.** Things you did yourself from the board, or that the system did on its own, drop out — you're left with agent work. +- **It carries the responsible person.** Agents act on behalf of someone. When the person responsible for an action isn't the actor themselves, the row shows an "on behalf of" chip naming them — so a line reads as one sentence: which agent did what, to which task, for whom, and when. + +Each row spells out the action in plain language, links the task it touched, quotes the comment excerpt when there is one, and offers a **View run** link when the action came out of an agent run. The raw action name (for example `issue.comment_added`) sits at the end of the row in small type, in case you need the exact string. + +You can also read a single agent's history without leaving its page: open the agent and switch to its **Activity** tab. It's the same feed, pinned to that one agent. + +### Who can see what + +Everyone on your team can open Activity, but there are two tiers of detail: + +- **Any company member** can read the feed with the scope set to **All actors**. You'll see every kind of actor — human, agent, plugin, and system — but the sensitive attribution fields (which agent, which run, and the responsible person) are stripped out. +- **Members with the `audit:view_agent_actions` permission** see complete rows. They can turn on the attribution filters — agent, responsible user, and actor type — and they can export the feed to CSV. No role hands this permission out by default, so it's always a deliberate grant; see [Roles & Permissions](../../administration/roles-and-permissions.md). + +If you set an attribution filter without the permission, Paperclip tells you the filter needs `audit:view_agent_actions` rather than silently returning stripped-down rows. + +### Exporting + +With the `audit:view_agent_actions` permission you'll see an **Export CSV** button. It downloads whatever the current filters are showing, one row per action, with the timestamp, action name, actor, agent, run, responsible user, entity, and the task identifier and title where there is one — the shape you want when you're handing evidence to someone outside Paperclip. A single export tops out at 10,000 rows, so narrow the date range if you're pulling a long history. + +One thing to know before you press it: the export is itself logged as an `audit.exported` event. Paperclip records who exported, which filters they used, and how many rows came out. + --- ## Using Activity to Debug diff --git a/docs/guides/day-to-day/artifacts.md b/docs/guides/day-to-day/artifacts.md index 26fde2f..31d995a 100644 --- a/docs/guides/day-to-day/artifacts.md +++ b/docs/guides/day-to-day/artifacts.md @@ -76,6 +76,18 @@ Where an artifact came from a real file, the card gives you the means to open it --- +## Reaching artifacts from a task + +The Artifacts page is the company-wide shelf. Sometimes you're already inside one task and just want to see what *this* task produced — without going back out to the shelf and filtering your way in. + +If your instance has the **Chat-Style Tasks** experimental feature turned on, the task's properties panel grows an **Artifacts** tab for exactly that. It lists the files attached to the task, one row each with the filename and its size; click a row to open the file in a new tab. The tab only appears once the task actually has an attachment, and it's read-only — a quick answer to "what came out of this task?", not a place to upload or delete. + +> **Experimental:** the tab only exists when **Chat-Style Tasks** is enabled in **Settings → Instance settings → Experimental**. With the flag off, a task's attachments stay in the **Attachments** section on the task page. See [Experimental features](../../experimental/overview.md). + +Either way, the Artifacts page remains the place to see everything at once, across every task and project. See [Issues](./issues.md#tabs-in-the-properties-panel-experimental) for the rest of what those tabs hold. + +--- + ## Reading a file in the in-app viewer When an artifact is a real file from an issue's workspace, you can read it without downloading anything. Click the file and Paperclip opens an **in-app file viewer** — a slide-over sheet that loads the file's contents right there in the app. @@ -124,5 +136,6 @@ Each file row shows a **download button** when the file can be downloaded. Click - **Stacks** — artifacts grouped by their task (or rolled up by parent task), so related outputs stay together. - **Cards** — previews you can read or watch in place, each linking back to the issue it came from. - **File viewer** — a slide-over that opens a workspace file inline (with a side file browser), reachable from a file artifact or from an inline file chip an agent left in its writing. +- **Artifacts tab on a task** — with **Chat-Style Tasks** enabled, one task's files listed in its properties panel. You now know how to find, filter, and review everything your agents have made. When you want to dig into the work behind a given artifact, follow its link back to the issue and pick up the thread there. diff --git a/docs/guides/day-to-day/blocked-inbox.md b/docs/guides/day-to-day/blocked-inbox.md index 799d69c..60d6778 100644 --- a/docs/guides/day-to-day/blocked-inbox.md +++ b/docs/guides/day-to-day/blocked-inbox.md @@ -45,6 +45,57 @@ A small coloured dot in front of the chip flags severity: red for `critical`, or The line below the title repeats the stopped age and owner on mobile so you don't lose it when the trailing columns collapse. +The owner column is more reliable than it used to be — see [Who owns the unblock](#who-owns-the-unblock) below. + +--- + +## Who owns the unblock + +The frustrating thing about stopped work has always been the guessing: something is blocked, and you're left reading the description to work out whose move it is. Paperclip now asks for that answer up front. When a task moves into **Blocked**, whoever blocks it records an **unblock owner** — the specific party who can clear the jam — plus a short **action** describing what that party needs to do. Both travel with the task, so the Blocked tab can name a party instead of leaving you to infer one. + +An unblock owner is one of three things: + +- **A specific agent.** The agent is woken once, straight away, and told this task is waiting on it. You don't have to nudge anything. +- **A specific person.** Anyone who's an active member of the company. People aren't woken — nothing pings their terminal — so the task instead surfaces for them to pick up. +- **The board.** That's you, collectively. Use this when the call genuinely belongs to the board rather than to one named person. + +Blocks owned by a human land in an attention feed rather than waking anything: a board-owned block appears for everyone on the board, and a person-owned one appears only in that person's feed. Either way it arrives as something needing a decision, marked high severity, with the recorded action as the "why now" line. You get two offered next steps: **Unblock** — described by that same action text — and **Reassign**, *"Route this blocked issue to another owner."* The item clears when the task leaves blocked status. Agent-owned blocks skip the feed, because the agent has already been woken. + +The action itself is plain text, required, and capped at 2,000 characters. It's the line you'll read in the attention feed when you come back to this in three days, so write it for your future self: "Confirm which pricing tier the trial converts to" beats "needs a decision". + +### Blocking a task now needs a real reason + +A task can no longer slip quietly into **Blocked** with nothing behind it. Moving a task to Blocked is accepted only when at least one of these is true: + +- it's genuinely waiting on other tasks that aren't done or cancelled, +- it has a pending interaction or a pending approval sitting on it, or +- it names an unblock owner and an action. + +If none of those hold, Paperclip turns the change down with *"Entering blocked requires unresolved blockers, a pending interaction/approval, or unblockDescriptor"*. That's the rule that keeps this whole tab honest: every row you see has something concrete you can act on. + +An unblock owner also only makes sense on a blocked task. Try to attach one to a task in any other status and you'll get *"unblockDescriptor requires blocked status"*. + +### What agents are allowed to name + +An agent can only name **itself** as the unblock owner. It can't hand a block to you, to another person, or to the board — that would give any agent a way to drop items into your attention feed at will. If an agent tries, Paperclip refuses with *"Agents may only name themselves as an unblock owner"*. + +When you block a task from the board side, you can name anyone, within two sanity checks: + +- An agent owner has to belong to the same company as the task, or you'll see *"Unblock owner agent must belong to the issue company"*. +- A person owner has to be an active company member, or you'll see *"Unblock owner user must be an active company member"*. + +### The wake happens once + +An agent owner is woken exactly once per block, under the reason `issue_unblock_requested`. Repeated processing of the same blocked task won't wake it again, so you won't get notification storms from a task that keeps getting touched. + +If a task bounces out of Blocked and later goes back in, that's a fresh block: a new owner and action get recorded, and the new owner is woken again. Leaving Blocked clears the recorded owner and action entirely — there's no stale ownership hanging around on a task that's moving again. + +### Tasks blocked before this shipped + +Recording an unblock owner started on **23 July 2026, at 18:13 UTC**. Anything blocked before that instant keeps the previous behaviour: no owner is recorded, no one was woken, and nothing appears in your attention feed on its behalf. Paperclip deliberately doesn't backfill old blocked work, because doing so would have woken half your company at once on upgrade. + +So if you have long-stalled rows in the Blocked tab with no owner to show, that's the reason rather than a bug. Move one out of Blocked and back in and it picks up the new routing. + --- ## Filtering and searching @@ -89,12 +140,33 @@ The chip is a triage hint, not a verdict. Here's how to clear each variant: --- +## What the task itself tells you + +Open a row from the Blocked tab and the task shows an amber notice at the top explaining, in plain language, why it isn't moving. The wording used to lean on Paperclip's internal vocabulary; it now describes your situation instead. + +The line you'll see depends on the shape of the block: + +- Nothing is linked, the task is just parked in blocked: *"Work on this task is blocked until someone moves it back to To do. Comments still notify the assignee for questions or triage."* +- It's waiting on linked tasks: *"Work on this task is blocked by the linked tasks until they are complete. Comments still notify the assignee for questions or triage."* +- A comment won't restart it: *"A message won't restart this task yet — it stays blocked by the linked tasks until they are done, then it reopens automatically. Comments still notify the assignee for questions or triage in the meantime."* A follow-up line names the deepest task still holding things up, as **Still blocked by** *identifier* with its status. +- The chain is stuck in review: *"Work on this task is blocked by the linked tasks, but the chain is stalled in review without a clear next step. Resolve the stalled reviews below or remove them as blockers."* + +If the chain is healthy and something in it is actually running, the notice turns blue and reads **Waiting on live work**: *"Queued behind 3 tasks being worked in order. This task resumes automatically when the chain is done. Comments still notify the assignee."* It draws the queue as a progress bar with an *"N of M done"* count, and the last step in the list is your task, labelled *"This task — resumes automatically when the chain is done"*. Nothing needs doing here — this is the reassuring variant. + +The recurring promise across all of these is worth internalising: **comments still notify the assignee**. A blocked task isn't a dead task. You can ask a question on it at any time and the assignee hears you, even when your message won't move the status by itself. + +A task sitting in backlog with an assignee gets its own version of the same courtesy: *"Parked — [assignee] will not be asked to work on this until status changes to To do or In progress."* + +--- + ## How the data is built Each issue carries an optional `blockedInboxAttention` payload from the server. The Blocked tab requests it explicitly — it calls `/api/issues` with `attention=blocked` and `includeBlockedInboxAttention=true` — and the UI only renders rows that have attention metadata, so an issue with `status = blocked` but no attention payload won't appear. The same contract powers the badge count next to the **Blocked** tab in the Inbox toolbar, so the count and the list stay in lockstep. +Your attention feed draws on the same classification but is deliberately noisier now. Stalled chains have always reached it, described as *"Blocked dependency chain is stalled and needs a human to choose the next owner or action."* Chains that merely need a look now reach it too, as *"Blocked dependency chain needs human attention."* Both arrive at high severity. If you used to work only from the Blocked tab, the feed is now a reasonable second entry point. + --- ## Related diff --git a/docs/guides/day-to-day/costs.md b/docs/guides/day-to-day/costs.md index 8edcc67..c0a7f38 100644 --- a/docs/guides/day-to-day/costs.md +++ b/docs/guides/day-to-day/costs.md @@ -16,6 +16,16 @@ Paperclip records every one of these calls: which agent made it, which model it > **Note:** A token is roughly equivalent to one word, though technically it's a fragment of text slightly smaller than that. A busy agent doing coding or writing work might process 100,000–500,000 tokens per month. At typical Anthropic pricing, that's roughly $3–$15 per month for a moderately active worker agent — but this varies significantly based on the model used and how much context each task requires. +### Why the dollars are lower than the token count suggests + +If you multiply an agent's token count by the provider's list price, you'll usually land somewhere above what Paperclip reports. That gap is normally prompt caching doing its job, not an error in the arithmetic. + +Agents re-send a lot of the same text on every heartbeat — their identity, their standing instructions, the task they're working on. Providers cache that repeated context and charge much less for a cached token than for a fresh one. You can see the split yourself in the per-model rows on the **Providers** tab, where cached-input tokens are counted separately from input tokens. + +Paperclip records the **cache-adjusted** cost: what the provider actually billed after the cache discount, rather than an estimate derived from raw token counts. When an adapter reports a discounted amount separately, that discounted amount is the one that lands in the cost ledger, counts against your budgets, and rolls up into every total on this page. When an adapter reports only a single cost figure, Paperclip treats that figure as the billed amount. + +The practical rule: the dollars you see are billed dollars. A run with a large cached-input count and a small dollar figure is healthy — it means the agent's context is being reused instead of re-billed. If you want to reason about a run in detail, read the token columns and the dollar figure together rather than trying to derive one from the other. + Beyond per-request inference costs, there are also **account-level** charges — monthly subscription fees, credit top-ups, invoice adjustments, refunds — that don't map to a single API call. Paperclip tracks those separately in the finance ledger so you can reconcile your actual provider invoices against what Paperclip thinks you spent. --- diff --git a/docs/guides/day-to-day/decisions.md b/docs/guides/day-to-day/decisions.md index 6489a34..1a41042 100644 --- a/docs/guides/day-to-day/decisions.md +++ b/docs/guides/day-to-day/decisions.md @@ -8,15 +8,44 @@ The **Decisions** page gives you one place to see work that needs a human call. Open it from the sidebar, or go directly to `/decisions`. +![The Decisions page: a ranked queue of items needing a human call, each row naming why it needs you and linking to the work behind it](../../user-guides/screenshots/light/decisions/queue-overview.png) + +The Training library lives with this page. Click the graduation-cap **Training** button in the page header, or go directly to `/decisions/training`, to browse the decisions your company has kept as training examples. Older `/training` links still resolve — they redirect to the new address. + --- ## 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. +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, agent-error alerts, or a decision an agent has proposed. 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. +Rows that Paperclip can complete in place — approvals, issue-thread interactions, join requests, and proposed decisions — 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. + +--- + +## Answer a decision an agent proposed + +Some rows are more than a notification. When an agent works out what should happen next but isn't allowed to do it alone, it raises a **decision**: a short question, an explanation, and up to eight options. Each option spells out exactly what Paperclip would do if you picked it — post a comment, create an issue, change a status, reassign work, clear a blocker, or cancel an issue and everything under it. + +You answer it right in the row. Options that need a little context from you show up to four fields first; a required field is marked, and the options stay disabled until you fill it in. + +![An agent-proposed decision expanded in the queue: the question, who proposed it and during which task, and each option as its own button](../../user-guides/screenshots/light/decisions/decision-card.png) + +A few things make this safe to do quickly: + +- **Nothing has drifted behind your back.** When the agent proposed the decision, Paperclip recorded what the affected issues looked like. If one of them has moved since, the row says so — *"1 target changed since this was proposed"* — and disables any option that depends on the old state. +- **Destructive options ask twice.** Choosing an option that cancels an issue tree opens a confirmation panel listing every issue that would be cancelled, and asks you to type the issue identifier before it will run. +- **Saying no is a real answer.** **Dismiss — no effects** closes the decision without changing anything, and records it as a "no" rather than letting it quietly time out. Paperclip counts dismissals separately from expiries, so agents get honest feedback about which proposals are worth making. +- **Decisions expire on their own.** A proposal that nobody answers lapses — after seven days by default — and so does one whose target issue has since been cancelled. + +Once you answer, Paperclip runs the option's changes one at a time and shows you the outcome, including anything it had to skip because a target had moved. If an agent asked to be woken when the decision resolves, that happens automatically. + +Two collapsible sections at the bottom of the page, **Decided** and **Expired**, keep the recent history so you can look back at what was chosen and what it did. + +![The Decided section expanded at the bottom of the queue, showing a resolved decision and the option that was chosen](../../user-guides/screenshots/light/decisions/history-curtains.png) + +You may also meet a decision from the other direction. When a proposal raised elsewhere would change an issue you are looking at, that issue shows a strip saying so and links you back here — decisions are always answered from this one page. --- @@ -28,10 +57,50 @@ Use the toolbar to make a busy queue manageable: - **Group by** offers `none`, `date`, `type`, `project`, and `severity`. - **Sort** offers `newest` and `oldest`. +![The Decisions toolbar with the filter panel open, showing the Type, Severity, Project, and Workspace sections](../../user-guides/screenshots/light/decisions/toolbar-filters.png) + Your filter, grouping, and sort choices stay with your current browser, so you can set up the view that fits your review rhythm. --- +## Group work into named queues + +A busy queue mixes very different kinds of work. **Queues** let you split it into named lanes — a pull-request lane, a plan-review lane, a lane for one project's questions — without changing anything about the work itself. + +A queue is a label that points at items. Adding an item to a queue doesn't move it, doesn't change its status, and doesn't remove it from anywhere else: an item can sit in several queues at once, and taking it out again leaves the underlying approval, question, or run exactly as it was. + +Paperclip fills three queues for you as it builds the feed, creating each one the first time something matches: + +| Queue | What lands in it | +|---|---| +| **PRs** | Items whose issue has a pull request attached. | +| **Plans** | Plan revisions waiting for you to confirm them. | +| **Questions** | Structured questions an agent has asked and is waiting on. | + +You can turn that automatic filling off per queue if you would rather curate a lane by hand. Turning it off leaves the queue and everything already in it alone — Paperclip just stops adding more. + +You can also create queues of your own, add and remove items, and read a queue's contents through the [Decisions API](../../reference/api/decisions.md). One thing worth knowing: a queue only ever shows you items you are allowed to see, and its item count is counted the same way, so two people can look at the same queue and see different totals. + +![A named decision queue showing only the items routed into that lane](../../user-guides/screenshots/light/decisions/named-queue.png) + +--- + +## Say when something needs you + +Filters tidy your view. **Triage** does something different: it records, for everyone, when a piece of work actually needs a decision. + +You can give any item a decide-by target of **today**, **this week**, **whenever**, or a specific calendar date, and you can snooze it until a moment in the future. Both stick to the item itself rather than to your browser, so the deadline your colleague set is the deadline you see. Paperclip keeps a history of who changed what. + +![The triage strip on an expanded row: a When to decide row offering Today, This week, Whenever, and Pick date, alongside Queues and Snooze controls](../../user-guides/screenshots/light/decisions/triage-chips.png) + +Triage changes how the queue reads in two ways. The feed can rank by deadline instead of recent activity — soonest first, then the *whenever* pile, then everything nobody has triaged yet. And the sidebar badge deliberately counts only what is due today, so it stays a signal about this morning rather than a running total of everything outstanding. + +Triage is set through the [Decisions API](../../reference/api/decisions.md) today. + +> Triage snoozing is not the same as the personal snooze below. A triage snooze hides the item for the whole company; the snooze in your queue is yours alone. + +--- + ## 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. @@ -46,6 +115,8 @@ These choices are personal to your board user. They tidy your queue without chan **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. +![The empty Decisions queue reading You're all caught up](../../user-guides/screenshots/light/decisions/empty-state.png) + --- ## Related @@ -54,3 +125,5 @@ These choices are personal to your board user. They tidy your queue without chan - [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. +- [Decisions API](../../reference/api/decisions.md) — propose and resolve decisions, and manage queues and triage. +- [Decision Training](../../reference/api/decision-training.md) — keep a decision as a labelled training example, and export what you have collected. diff --git a/docs/guides/day-to-day/issues.md b/docs/guides/day-to-day/issues.md index 25d4ce2..3a7fb84 100644 --- a/docs/guides/day-to-day/issues.md +++ b/docs/guides/day-to-day/issues.md @@ -346,6 +346,49 @@ Above the tabs, separately from the Properties panel, the detail view also rende - An **Issue Workspace card** that summarises the issue's project and its execution workspace binding — the same underlying concept the Workspace / Branch / Folder rows describe, but surfaced as a single card so it is visible even when the sidebar is collapsed. - An **Attachments** section. Images appear as thumbnails that open in a gallery modal; video attachments play back inline in a built-in player; non-image attachments render as file rows with their content type and size. Each attachment offers two actions: **open** previews it inline (images in the gallery, video in the player), while **download** saves the file to disk. Supported media includes images, PDF, text, CSV, JSON, video (mp4, webm, mov/quicktime), and zip archives. You can upload from the detail view when no attachments exist yet, and from an inline button otherwise. +### Tabs in the properties panel (experimental) + +Once a task has a plan or a few files hanging off it, one long scroll of properties stops being the fastest way to find things. With the **Chat-Style Tasks** experimental feature turned on, the properties panel splits into tabs so the plan and the task's files each get their own space. + +> **Experimental:** these tabs only appear when **Chat-Style Tasks** is enabled in **Settings → Instance settings → Experimental**. With the flag off, the panel keeps the single stacked list of properties described above, and nothing on this page changes. See [Experimental features](../../experimental/overview.md) for how that page behaves. + +The tab strip sits in the panel's header bar, to the left of the window controls, and offers up to three tabs: + +- **Properties** — exactly the fields listed above, unchanged. This is always the first tab and the one you land on. +- **Plan** — the task's `plan` document plus its accepted-plan history. Appears only when the task has a plan document or at least one accepted plan. +- **Artifacts** — the task's attachments. Appears only when the task has at least one attachment. + +Tabs are earned, not permanent: a task with neither a plan nor attachments shows a plain **Properties** title in the header bar instead of a one-tab strip. If you're sitting on the Plan or Artifacts tab and its content goes away — or you move to a task that never had any — the panel falls back to **Properties** rather than showing you an empty pane. + +The panel itself also gains a resize grip on its left edge and a **Maximize panel** button in the header (which becomes **Restore panel** once expanded), so you can widen the pane when you're reading a long plan and shrink it again afterwards. The width you drag it to is remembered. + +#### The Plan tab + +Open **Plan** and you get the plan itself, rendered, rather than a link to go find it. Above the text a small line tells you which version you're reading — for example *"Revision 3 · updated Aug 3, 2:15 PM"* — so you can tell at a glance whether the agent has revised the plan since you last looked. + +Below the plan sits the accepted-plan history: each accepted revision and the child tasks it created. That's the same **Plan decomposition** view described in [Task Plan Decomposition Panel](../../experimental/plan-decomposition-panel.md), moved here so the plan and what it produced read together. + +#### Approving a plan from the panel + +When an agent asks you to confirm a plan, the decision doesn't hide at the bottom of the chat thread. A small action bar pins itself below the panel's scroll area, so it stays visible while you scroll through the plan above it. (On the mobile Properties sheet it renders in place instead of pinning.) + +You get two choices: + +- **Confirm** — accepts the plan and lets the agent proceed. While it's saving, the button reads *"Confirming..."*. +- **Decline** — sends the plan back for revision. Choosing it opens a text box, prompting *"Optional: what would you like revised?"*, so you can say what should change; **Cancel** backs out without sending anything. The button reads *"Sending back..."* while it saves. + +Agents can customise both button labels and the prompt in the text box when they ask, so a particular request might read something other than Confirm and Decline. Some requests make the reason mandatory — decline without typing one and you'll see *"A decline reason is required."* Others skip the text box entirely, in which case Decline sends immediately. + +If either action fails, the bar tells you plainly — *"Couldn't confirm — try again."* or *"Couldn't send that back — try again."* — and leaves your choice intact so you can retry. And if the same request gets answered somewhere else in the meantime (in the chat thread, or by a colleague), the bar clears itself. + +#### The Artifacts tab + +**Artifacts** is a plain list of the files attached to this task — one row each, showing the filename and its size. Click a row to open the file in a new tab. + +It's there to answer "what did this task produce?" without scrolling the thread. The tab is read-only, so there's no uploading or deleting from it, and the company-wide view of every output still lives on the [Artifacts page](./artifacts.md). + +With **Chat-Style Tasks** on, the centre column's **Attachments**, **Output**, keyed-documents, and **Plan decomposition** sections all step aside — the plan and the task's files live in this panel instead, leaving the middle of the page to the conversation. + ### Output When an agent finishes a piece of work, it can hand back the result directly on the issue. The **Output** section surfaces those deliverables — the work products the agent produced — so you can inspect them right on the board without ever opening the agent's workspace. @@ -506,6 +549,35 @@ The Activity tab does not accept input — it is read-only. Use Chat for anythin --- +## When an agent writes on another issue + +Your agents don't only touch the issues they own. A standard-trust agent can now **comment, change fields, create child issues, and assign work on any issue it can already read** in the company — not just issues it's assigned to, that it created, or where it was mentioned. The goal is that an agent can help wherever it can already see the work, instead of being blocked by narrow ownership rules that used to differ from one write channel to the next. + +Two things still bound every one of those writes, so this stays safe: + +- **The agent has to be able to see the issue.** Visibility is the single gate all four write channels share — if an agent can't read an issue, it can't comment on it, edit it, add a sub-issue under it, or reassign it either. Company boundaries are always enforced, so an agent never reaches across into another company's work. +- **A responsible human still bounds the action.** Every agent write is made *on behalf of* a real user, and the write only goes through if that responsible user is also allowed to make it. Low-trust agents, agents outside their trust scope, and run-lifecycle locks (like an issue that's mid-run for someone else) are all still enforced on top of visibility. + +### Seeing who did what, and on whose authority + +Because agents can now write more widely, Paperclip makes each write easy to account for. Three surfaces answer "who did this, on whose authority, and was it allowed?": + +- **Attribution on comments.** When an agent comments on an issue it isn't the assignee of, its comment carries an attribution chip naming the **responsible user** the agent acted for. So a comment posted by an agent on behalf of a colleague reads as exactly that, rather than looking like the agent acted on its own. +- **Field-level receipts.** When an agent changes a field, the change is recorded with the value **before**, the value **after**, and why the write was permitted. You'll find these receipts in the [Activity tab](#the-activity-tab), alongside the other status and property transitions. +- **Actionable denials.** When a write is refused, the message doesn't stop at an opaque "not allowed". It names the boundary that fired, tells you **who can act instead**, and points at the sanctioned path forward — for example, creating a child issue whose assignee can carry the request. So a blocked agent (or the person reading over its shoulder) knows the next move, not just that the door was closed. + +### Keeping one run from cascading across issues + +An agent's heartbeat run is meant to focus on the issue it woke up for. To stop an unbounded run from fanning writes out across the whole board, each run has a **per-run cross-issue limit**: a single run may make at most **20** writes — comments and field updates combined — to issues *other* than the one it woke up for (`CROSS_ISSUE_INFLUENCE_LIMIT`). It's a count of writes, not of issues, so twenty comments on one other issue reach the cap just the same. + +Right now that cap is in **log-only** mode — Paperclip observes and records each cross-issue write but doesn't reject any. Hard enforcement begins on **2026-08-11** (`CROSS_ISSUE_INFLUENCE_ENFORCE_AT`); from that date, a run's 21st cross-issue write is refused with the same kind of actionable denial described above. Comment records also persist the responsible user separately from the acting agent, so the "on behalf of" trail survives independently of who typed the words. + +### Addressing an interaction to another agent + +Issue-thread interactions — confirmations, questions, suggested tasks — can now be addressed to **another agent** to resolve, not only to board users. When an agent is asked to resolve an interaction, the resolution is governed at the company level: it carries an auditable resolver identity, honours terminal and withdrawal expiry semantics, and is filtered into the right attention feed. Board control, company isolation, and the audit trail are all preserved, so handing a question to an agent is a governed, on-the-record move rather than a side channel. + +--- + ## Recovery actions Sometimes an issue's run finishes without choosing a next step, or an assigned issue gets stranded with no live execution path. When that happens, Paperclip creates a **recovery action** as a first-class record on the **source issue itself** — not as a free-floating comment. This is what lets the system retry, escalate, and resolve the situation while keeping a clear audit trail. diff --git a/docs/guides/getting-started/installation.md b/docs/guides/getting-started/installation.md index 5e838bb..cefe904 100644 --- a/docs/guides/getting-started/installation.md +++ b/docs/guides/getting-started/installation.md @@ -79,7 +79,20 @@ Paperclip opens automatically in your browser. If it doesn't, navigate to [http: You'll land in Paperclip ready to start onboarding. You haven't created a company yet — that's the next step. -> **Note:** To run Paperclip again after restarting your machine, run `npx paperclipai run` from your terminal. For persistent background runs, see the [advanced deployment docs](../../reference/deploy/overview.md). +> **Note:** To run Paperclip again after restarting your machine, run `npx paperclipai run` from your terminal. + +### Keeping Paperclip running + +The command above holds your terminal open — close the window and Paperclip stops. That's fine while you're finding your feet, but once Paperclip is something you use daily, let your computer look after it instead: + +```bash +npx paperclipai install +paperclipai service install +``` + +The first command installs Paperclip properly (so you can also update and roll it back later) and offers to add it to your shell's `PATH` — say yes, then open a new terminal window so the `paperclipai` command is available. The second registers Paperclip as a background service that starts when you log in and restarts itself if it crashes. Nothing needs administrator rights, and `paperclipai service status` tells you how it's doing. + +See [Service](../../reference/cli/service.md) for the full set of commands, and the [advanced deployment docs](../../reference/deploy/overview.md) for server-side deployment patterns. --- diff --git a/docs/guides/org/agents.md b/docs/guides/org/agents.md index c566fd5..5760588 100644 --- a/docs/guides/org/agents.md +++ b/docs/guides/org/agents.md @@ -135,6 +135,17 @@ The action cluster in the header works on any tab: When the agent is in `pending_approval` status the Run Heartbeat button is disabled and a banner at the top of the page reminds you the agent cannot be invoked until the board approves the hire. +### Banners at the top of the page + +Two amber banners can appear above the tab bar, and they mean different things: + +- **Invalid org chain** — the agent's reporting chain is broken (a terminated manager, a manager that no longer exists, or a loop). This one blocks work: you can't assign to the agent until you repair the chain, and the banner tells you how. +- **Escalation path is paused** — the agent can run perfectly well, but somewhere up its `reports_to` chain sits a **paused** agent. This one is a warning only. Nothing is blocked; the agent just has nowhere useful to escalate to, and any escalation an agent tries to route there is refused rather than accepted. The banner text names the paused manager and gives you the two fixes: unpause that manager, or change who this agent reports to. + +The escalation warning only shows for agents that can themselves receive and run work. If the agent is itself paused or terminated, its escalation path doesn't matter yet, so the banner stays quiet. And if the chain is outright invalid, you see the invalid-chain banner instead — fixing that is the more urgent job. + +This is worth checking after an instance import, which pauses every agent by default. Unpausing your workers and forgetting their manager is the classic way to end up with a team that works fine and escalates into nothing. + --- ## Dashboard Tab @@ -552,6 +563,8 @@ On the Runs tab, each run shows a **session ID before** and **session ID after** The status dot on every row and badge on the agent header reflect one of these values directly. The **Active** filter on the agent list expands to include `active`, `running`, and `idle`, because all three mean "this agent is healthy and doing (or ready to do) work". **Paused** and **Error** are their own filters for triage. +**One thing to know about `paused`:** you can still assign work to a paused agent — staging a task for an agent you plan to resume later is a legitimate workflow, and the pause state is right there on screen so you know what you're doing. Other **agents** can't. When an agent tries to assign a task to a paused agent, Paperclip refuses it with `Cannot assign work to a paused agent. Assign an invokable agent, leave the issue unassigned, or escalate to a board operator instead.` Without that refusal, agent escalations to a paused manager would be accepted and then never run. See [Delegation → Guardrails on agent-to-agent delegation](./delegation.md#guardrails-on-agent-to-agent-delegation). + ### Why this matters day-to-day The heartbeat model has a few practical consequences worth keeping in mind: diff --git a/docs/guides/org/delegation.md b/docs/guides/org/delegation.md index 03e74bb..b46398b 100644 --- a/docs/guides/org/delegation.md +++ b/docs/guides/org/delegation.md @@ -121,6 +121,50 @@ This lets you start small and scale the team based on actual work, not upfront p --- +## Guardrails on agent-to-agent delegation + +Delegation is powerful precisely because you don't have to watch it. That's also the risk: when an agent hands work to the wrong place, the task can sit in a queue nobody will ever pick up, and you won't hear about it. Paperclip refuses a couple of these hand-offs outright, so the agent gets an error it can act on instead of a silent dead end. + +Both guardrails apply only when an **agent** is doing the assigning. You can still assign work anywhere you like — staging a task for an agent you plan to unpause later is a perfectly reasonable thing to do, and you can see the agent's state right there in the UI. + +### An agent can't assign work to a paused agent + +A paused agent never runs. If another agent assigns it a task — typically an escalation routed up the `reports_to` chain to a paused manager — the task is accepted, nothing picks it up, and the work quietly vanishes. + +So when the assigning actor is an agent and the assignee is paused, Paperclip refuses the assignment with a conflict and this message: + +> Cannot assign work to a paused agent. Assign an invokable agent, leave the issue unassigned, or escalate to a board operator instead. + +The message names the three ways out on purpose: pick an agent that can actually run, leave the task unassigned so a human can route it, or escalate to a board operator. The existing refusals for terminated agents, agents still pending approval, and agents with an invalid org chain are unchanged and still apply to everyone. + +### An agent can't delegate work back around a loop + +Here's the shape this catches. Agent A hits something it can't do — say it can't push to GitHub — and creates a child task for agent B. Agent B can't do it either, so it "resolves" its blocker by creating a grandchild task assigned straight back to A. Neither agent gained a capability, the chain of blocked tasks keeps growing, and nothing reaches the human who could actually fix the gap. + +When an agent creates a child task and the assignee is the agent that created a **still-open** ancestor in the same chain, Paperclip refuses the creation with a conflict carrying the code `delegation_cycle`. The message names the ancestor task, then spells out the alternatives: + +> Complete the remaining work in your own issue, leave the child unassigned, or escalate to a board operator — do not delegate the work back to the agent that delegated it to you. + +A few things deliberately don't trip this: + +- **You aren't affected.** Re-routing work around the org chart is your call to make. +- **Closed ancestors don't count.** Only tasks that aren't `done` or `cancelled` are considered — handing new work to the agent that created something finished is normal. +- **It keys on the *creator*, not the assignee.** Passing a subtask to whoever is working the parent task is a common, legitimate pattern, and it still works. + +When you see a `delegation_cycle` refusal in a run transcript, treat it as a signal rather than a bug: two of your agents are both missing something one of them assumed the other had. Usually that's a credential, a tool, or a permission — fix it once and the chain unblocks. If the gap is GitHub push access specifically, [Connect an agent to a GitHub repo](../../how-to/connect-agent-to-github.md#the-credential-preflight-catches-a-missing-token-before-the-work) covers the preflight that now asks you for the token before any work is burned. + +### When an escalation path is already dead + +The guardrails above fire at the moment an agent tries to do something. There's also a standing warning for the hazard itself. + +Agents escalate up the `reports_to` chain, and a paused manager doesn't break that chain — the agents beneath it stay perfectly invokable, so nothing looks wrong at a glance. This bites most often after an instance import, which pauses every agent by default: you unpause the workers, get on with your day, and leave the manager paused. + +Paperclip now spots this. When an agent that can itself run work reports (directly or further up) to a paused agent, its detail page shows an amber **Escalation path is paused** banner. Unlike the invalid-org-chain banner it sits next to, this one is a warning and doesn't block anything — the agent keeps working, it just has nowhere useful to escalate. The banner text names the paused manager and the two fixes: unpause them, or change who the agent reports to. + +You'll find more on the banner and where it appears in [Agents → The Agent Detail Page](./agents.md#the-agent-detail-page). + +--- + ## Troubleshooting: CEO isn't delegating If you've set a goal but nothing seems to be happening, work through these common causes: @@ -138,7 +182,8 @@ If you've set a goal but nothing seems to be happening, work through these commo | Check | What to look for | |---|---| | **Reports have heartbeats** | Go to each agent's detail page. If heartbeats are disabled, the CEO may skip assigning to them since they won't be able to pick up work. | -| **Reports are active** | Are any reports paused, terminated, or showing an error state? The CEO won't assign to agents it can't reach. | +| **Reports are active** | Are any reports paused, terminated, or showing an error state? The CEO won't assign to agents it can't reach — an agent assigning to a paused agent is refused outright. | +| **Escalation banners** | Open each agent's detail page and look for an amber **Escalation path is paused** banner. A paused manager means escalations from that agent have nowhere to go. | | **CEO's budget** | At 80% of its monthly budget, Paperclip warns you. At 100%, it auto-pauses entirely. | ### CEO is assigning everything to itself diff --git a/docs/guides/org/skills.md b/docs/guides/org/skills.md index 51366f4..79152d4 100644 --- a/docs/guides/org/skills.md +++ b/docs/guides/org/skills.md @@ -203,6 +203,29 @@ If you started from a folder — say, from inside **My Skills** — the new skil --- +## Renaming a skill + +Names age. The `code-review` skill you wrote in week one turns out to be your PR triage procedure, and every agent that uses it is now working from a name that misleads whoever reads the roster next. You can rename it without unpicking anything. + +Renaming changes three things at once, because in Paperclip they travel together: + +- The **name** — what you and your team see in the library. The `name:` line in the skill's `SKILL.md` frontmatter is rewritten to match, so the file and the library never disagree. +- The **slug** — the `kebab-case` shortname used in URLs. Give it explicitly if you want to, or leave it out and Paperclip derives it from the new name. +- The **key** — the stable identifier agents reference. It follows the slug, so a new slug means a new key. + +That last one is where renaming would normally hurt, and it's the part Paperclip handles for you. **Every agent that had the old skill attached is moved to the new key automatically**, keeping any pinned skill version as it was. Nothing silently detaches, and nobody has to go around the Agents list re-ticking checkboxes. + +A few things worth knowing before you rename: + +- **Only Paperclip-managed skills can be renamed.** Skills from the built-in catalog, GitHub, skills.sh, or a project scan are read-only for the same reason their contents are — see [Editing a skill that isn't yours to edit](#editing-a-skill-that-isnt-yours-to-edit) if you want an editable copy you *can* rename. +- **The new slug has to be free.** If another skill in the company already uses it, the rename is refused rather than overwriting anything, and you'll be told which one clashed. +- **It's audited.** The rename is written to the company activity log as `company.skill_renamed`, recording the previous and new name, slug, and key, plus every agent that was reassigned. +- **It obeys the skill policy.** Renaming counts as an edit, so if your company restricts who may change skills, the same rule applies here — see [Who is allowed to change skills](#who-is-allowed-to-change-skills). + +If you'd rather script it, the endpoint is `POST /api/companies/{companyId}/skills/{skillId}/rename`. It takes a `name` (required, and it has to be a single line) and an optional `slug`; leave the slug out and it's derived from the name. The response hands back the renamed skill along with the previous name, slug, and key, and the list of agents that were reassigned. + +--- + ## 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. @@ -279,6 +302,37 @@ A **View company skills library** link at the top of the tab jumps back to the c --- +## Pinning a beta release of the Paperclip skill + +The bundled `paperclip` skill — the base procedure every agent follows on each run — keeps moving. Most of the time that is exactly what you want: better guidance ships and every agent picks it up. But sometimes you want one agent held still, either because you are comparing how two versions of the procedure behave or because a particular snapshot suits a run you would rather not disturb while you evaluate a newer one. + +**Beta releases** let you do that. A release is a frozen snapshot of the Paperclip core skill, seeded into your company library alongside the live version. Pin an agent to one and that agent runs the snapshot; every other agent keeps running the live default. + +### Turn it on first + +The picker stays hidden until someone enables it. Open **Settings → Instance settings → Experimental** and flip the **Beta skills** card: *"Allow agents to pin beta releases of the Paperclip core skill. Disabling this returns every agent to the default live skill without removing saved pins."* + +It is off by default everywhere — cloud and self-hosted alike — and it is not one of the cloud-managed flags, so it stays yours to turn on and off on any instance. See [Experimental features](../../experimental/overview.md) for how that page behaves. + +### Pick a release + +Open the agent, go to the **Skills** tab, and find the Paperclip core skill row — it needs to be enabled for that agent. A **Skill release** dropdown appears alongside it: + +- **Default — current (recommended)** — no pin at all. The agent runs the live skill and picks up every future change. This is where every agent starts. +- Each frozen release, shown as its name plus the day it was cut — for example **V7 — Roster champion · released 2026-07-21** — carrying a small **Beta** badge. + +Two releases ship today: **V0 — Original**, the original snapshot of the core skill, and **V7 — Roster champion**. Both are historical by design. **V7 — Roster champion** predates three later edits to `SKILL.md`, `references/api-reference.md`, and `references/company-skills.md`, and the live default keeps that newer guidance — which is the trade-off worth holding in mind. A pin buys you a procedure that will not move, and costs you every improvement made since it was cut. That is why the default option says *recommended*. + +Choosing a release saves immediately rather than waiting for the usual autosave, and the skill row picks up a **Beta · V7** style badge so you can see at a glance which agents are pinned. Switch back to **Default — current (recommended)** to clear the pin. + +### What happens when the flag is off + +Turning **Beta skills** back off erases nothing. Saved pins stay on the agent — they are simply ignored, and every agent goes back to the live default skill on its next run. Turn the flag on again and those pins apply again. + +The same rule holds over the API: saving a version pin while the flag is off is rejected with *"Beta skill version pins require the Beta skills experimental setting to be enabled."* For the request shape, the release fields, and where the snapshots come from, see [Skills reference → Beta releases of the bundled `paperclip` skill](../../reference/skills.md#beta-releases-of-the-bundled-paperclip-skill). + +--- + ## Trust level Skills are loaded into agent runs as additional instructions, so where they come from matters. Paperclip uses the **Source** badge on every skill row to make that provenance obvious: @@ -323,7 +377,7 @@ You can also scan and import skills from project workspaces when you already hav Most of the time you will manage skills from the UI, but the same operations are available as REST endpoints when you want to script onboarding or reproduce a known agent setup from another company. - **List the skills attached to an agent**: `GET /api/agents/{agentId}/skills` -- **Sync the attached set**: `POST /api/agents/{agentId}/skills/sync` with `{ "desiredSkills": [...] }`. Each entry is a skill UUID, canonical key, or unique slug. The server reconciles attachments to match — adding any missing and removing any not in the list. +- **Sync the attached set**: `POST /api/agents/{agentId}/skills/sync` with `{ "desiredSkills": [...], "mode": "add" | "remove" | "replace" }`. Each `desiredSkills` entry is a skill UUID, canonical key, or unique slug. The required `mode` chooses how the list applies: `add` unions the named skills into the current set, `remove` drops only the named skills, and `replace` reconciles the set to match exactly — adding any missing and removing any not in the list. - **Provision skills at hire time**: pass `desiredSkills` on `POST /api/companies/{companyId}/agents` so the agent comes online with the right set already attached. The skills must already be installed at the company level before you can attach them. For the full reference — file shape, install pipeline, canonical keys, versioning, and troubleshooting — see: diff --git a/docs/guides/power/execution-policy.md b/docs/guides/power/execution-policy.md index 3869112..07de1f8 100644 --- a/docs/guides/power/execution-policy.md +++ b/docs/guides/power/execution-policy.md @@ -63,6 +63,47 @@ An issue can have review only, approval only, both in sequence, or neither (just --- +## When the loop doesn't end: review rounds and escalation + +The changes-requested loop is meant to converge. Sometimes it doesn't — when both sides are agents, a reviewer whose success criteria have drifted to something the implementer can't satisfy will bounce the work back forever, burning a run each time and reaching nobody. + +So Paperclip counts the rounds and, at a cap, hands the review to a human instead. + +**How the counting works.** `executionState.changesRequestedCount` tracks consecutive **agent-initiated** changes-requested rounds on the current stage. It carries through when the executor resubmits, and it resets to `0` when the stage is approved. + +**Your decisions don't count.** If a *human* requests changes, the counter resets to `0`. The cap exists to stop unattended agent-to-agent ping-pong, never to limit your own review. A workflow with a human in the loop is effectively unbounded. + +**What happens at the cap.** With the default cap of `3`, the third consecutive agent changes-requested decision behaves differently from the first two. The decision is still recorded with the reviewer's comment, but instead of handing the work back to the implementer, the runtime: + +- keeps the stage `pending` and the issue in `in_review`, +- makes the issue's **responsible human** the stage participant — `responsibleUserId`, falling back to `createdByUserId` — and assigns the issue to them, +- surfaces the pending review through the same attention and review UI you already use. + +From there it's yours: approve it, request changes yourself (which resets the counter and hands it back to the implementer), or re-scope the work. + +**The hold is sticky.** Once a review has escalated, only the escalated human can move it. Anyone else attempting to advance the stage or reassign it gets `Only the escalated reviewer can advance the current execution stage`. If the issue's status or assignee has drifted away in the meantime, the runtime puts it back. Without this, an unrelated update could silently undo the escalation. + +**If the issue has no responsible human** — neither `responsibleUserId` nor `createdByUserId` — nothing changes: the work hands back to the implementer as it always did. The counter still climbs, so you can still see the churn. + +### Tuning the cap + +Set `maxReviewRounds` on the issue's `executionPolicy` to override the default: + +```json +{ + "stages": [ + { "type": "review", "participants": [{ "type": "agent", "agentId": "qa-agent-id" }] } + ], + "maxReviewRounds": 5 +} +``` + +The value is a positive integer up to `50`. Leave it out (or set it to `null`) and the server default of `3` applies. Set it to `1` if you want the very first agent changes-requested decision to land on a human. + +Existing issues from before this behaviour shipped read as `changesRequestedCount: 0` — there's nothing to migrate. + +--- + ## Policy variants **Review only:** @@ -123,6 +164,7 @@ interface IssueExecutionPolicy { mode: "normal" | "auto"; commentRequired: boolean; // always true, enforced by runtime stages: IssueExecutionStage[]; // ordered list of review/approval stages + maxReviewRounds?: number | null; // 1–50; null uses the server default of 3 } interface IssueExecutionStage { @@ -153,6 +195,7 @@ interface IssueExecutionState { completedStageIds: string[]; lastDecisionId: string | null; lastDecisionOutcome: "approved" | "changes_requested" | null; + changesRequestedCount?: number; // consecutive agent-initiated rounds on this stage } ``` @@ -182,6 +225,7 @@ Every decision is recorded with actor, outcome, comment, and run ID. The full re - Only the **active reviewer or approver** (the `currentParticipant` in execution state) can advance or reject the current stage. - Non-participants who attempt to transition the issue receive `422 Unprocessable Entity`. +- Once a stage has escalated to the responsible human, only that person can advance it — everyone else, including the configured stage participants, is refused. - Both approvals and change requests **require a comment** — empty or whitespace-only comments are rejected. --- @@ -270,7 +314,7 @@ For existing issues, the properties panel shows editable **Reviewer** and **Appr ## Design principles 1. **Runtime-enforced, not prompt-dependent.** Agents don't need to remember to hand off work. The runtime intercepts status transitions and routes accordingly. -2. **Iterative, not terminal.** Review is a loop — request changes, revise, re-review — not a one-shot gate. The system returns to the same stage on re-submission. +2. **Iterative, but bounded.** Review is a loop — request changes, revise, re-review — not a one-shot gate. The system returns to the same stage on re-submission. An *unattended* loop still terminates: after `maxReviewRounds` agent rounds it lands on a human decision rather than burning runs forever. 3. **Flexible roles.** Participants can be agents or users. Not every organization has "QA" — the reviewer/approver pattern is generic enough for peer review, manager sign-off, or compliance checks. 4. **Auditable.** Every decision is recorded with actor, outcome, comment, and run ID. 5. **Single execution invariant preserved.** Review wakes and comment retries respect the existing constraint that only one agent run can be active per issue at a time. diff --git a/docs/guides/power/export-import.md b/docs/guides/power/export-import.md index 37024df..3c541f1 100644 --- a/docs/guides/power/export-import.md +++ b/docs/guides/power/export-import.md @@ -1,9 +1,32 @@ # Export & Import -Once you've built a company — given it a goal, hired agents, configured their adapters, and set up projects — that configuration has real value. Export and import let you capture that configuration as a portable package you can back up, share with others, or reuse as a starting point for a new company. +Once you've built a company — given it a goal, hired agents, configured their adapters, and set up projects — that configuration has real value. Export and import let you capture that configuration as a portable package you can back up, share with others, move to another Paperclip instance, or reuse as a starting point for a new company. Exports are human-readable markdown files. Anyone with the package can understand the company's structure without reading a database dump. +> **Moving a company between instances?** This is the way to do it. Export the company on one instance, import the package on the other. Import/export is the full-fidelity replacement for the old host-to-host Cloud Sync, which has been retired — you no longer need to connect two instances to each other to move work between them. + +--- + +## What "full fidelity" means + +A package isn't just a sketch of your company. It carries the working state too, so a restore or a migration lands you somewhere you recognize rather than somewhere you have to rebuild. + +**What travels with the package:** + +- Company name, description, branding, logo, and hiring policy +- Agents — identity, role, title, reporting lines, instructions, adapter and runtime configuration, permissions and permission grants, per-agent budgets +- Projects, their workspaces, and their sidebar order +- Skills, either as upstream references or vendored in full +- Tasks with their comments, status, priority, labels, blocked-by relationships, documents, work products, and monitors +- Routines and their schedule triggers +- Attachments and images, stored as content-addressed files and re-attached on import +- The names of the environment variables each agent needs + +**What never travels:** secret values — API keys, tokens, passwords. Machine-specific paths. Internal database IDs. These are environment-specific and wouldn't be valid on another machine anyway. The package declares which env vars an agent expects; you supply the values on the receiving side. + +**What is deliberately left behind:** approvals, cost history, and activity log entries. These are records of what happened on the source instance rather than configuration, so they stay there. If your company has any of them, the export page tells you so — and how many — before you download anything. + --- ## What a package contains @@ -12,20 +35,25 @@ An exported company package looks like this: ``` my-company/ +├── README.md ← A human-readable summary of the package ├── COMPANY.md ← Company name, goal, and metadata ├── agents/ -│ ├── ceo/AGENT.md ← Agent identity, role, and instructions -│ └── cto/AGENT.md +│ ├── ceo/AGENTS.md ← Agent identity, role, and instructions +│ └── cto/AGENTS.md ├── projects/ │ └── main/PROJECT.md ├── skills/ │ └── review/SKILL.md +├── tasks/ +│ └── onboarding/ +│ ├── TASK.md +│ └── documents/brief.md +├── images/ ← Company logo and inline images +├── blobs/ ← Attachment bytes, named by content hash └── .paperclip.yaml ← Adapter types, environment variable declarations, budgets ``` -**What's included:** Company name, description, and goal. Agent names, roles, reporting structure, and instructions. Project definitions. Skills. Adapter type declarations and the names of environment variables that need values. - -**What's never included:** Secret values — API keys, tokens, passwords. Machine-specific paths. Internal database IDs. These things are environment-specific and wouldn't be valid on another machine anyway. +Everything except `blobs/` is text, so a package diffs cleanly in Git and reads fine in any editor. --- @@ -35,9 +63,15 @@ You can export and import from the Paperclip UI now. The terminal commands still ### In the UI -Open **Org** and use the **Export company** or **Import company** buttons in the header. You can also reach the same pages from **Company Settings**. +Open **Company Settings** and look for the **Company Packages** section. It has two buttons: **Export** and **Import**. -On export, Paperclip shows you the package contents before you download them. On import, it previews what will be created, renamed, or skipped before anything is applied. +The export page lets you pick what to include with a small set of toggles — **Agents**, **Projects**, **Skills**, **Routines**, **Tasks**, and **Attachments** — and everything is on by default. Attachments ride along with the tasks and routines that reference them, so the Attachments toggle only applies while Tasks or Routines is still selected. + +Paperclip shows you the package contents before you download them, and you can uncheck individual files if you want to trim the bundle further. When you're happy, the export downloads as a single `.zip` named after the package. + +On import, Paperclip previews what will be created, renamed, or skipped before anything is applied. + +If the company has data the bundle can't carry — approvals, cost events, or activity log entries — the export page shows a **Not included in this export** panel listing exactly how many of each are being left behind. It's not an error; it's there so nothing surprises you six months later when you restore. ### In the terminal @@ -63,28 +97,45 @@ paperclipai company export --out ./full-export \ |---|---| | `--out ` | Where to save the export (required) | | `--include ` | What to include: `company`, `agents`, `projects`, `issues`, `tasks`, `skills` — comma-separated | -| `--skills ` | Export only specific skills by name | -| `--projects ` | Export only specific projects | +| `--skills ` | Export only specific skills by name | +| `--projects ` | Export only specific projects | +| `--issues ` | Export only specific tasks | +| `--project-issues ` | Export every task inside the named projects | +| `--expand-referenced-skills` | Copy skill contents into the package instead of pointing at the upstream source | +| `--force` | Overwrite a folder that already has files in it, without asking | -> **Tip:** Run the export regularly as a backup — especially before making significant changes to your agent configuration or org structure. +> **Tip:** Run the export regularly as a backup — especially before making significant changes to your agent configuration or org structure. `--force` is the flag you want for an automated nightly export that keeps writing to the same folder. --- ## Importing a company -You can import from a local folder, a GitHub repository, or a shorthand GitHub reference. +An import can come from three kinds of source: a local path, a URL, or GitHub. ```sh # From a local folder paperclipai company import ./my-export -# From a GitHub repository +# From a local .zip package +paperclipai company import ./my-export.zip + +# From a GitHub repository URL paperclipai company import https://github.com/org/repo -# From a GitHub subfolder +# From a GitHub subfolder, using owner/repo shorthand paperclipai company import org/repo/companies/acme ``` +For GitHub sources you can pin an exact version with `--ref`, which takes a branch, a tag, or a commit: + +```sh +paperclipai company import https://github.com/org/repo --ref v1.2.0 +``` + +`--ref` only applies to GitHub sources — pass it with a local path and the command stops and tells you so. URLs must point at GitHub or GitHub Enterprise; a plain HTTP link to some other host is rejected rather than fetched. + +> **Note:** When you import a local package from the UI, choose a `.zip` that Paperclip exported. An archive you re-zipped yourself in Finder or Explorer may not import correctly. The CLI is happy with either a `.zip` or an unpacked folder. + ### Creating a new company from a package When you import without specifying an existing company, Paperclip creates a fresh one: @@ -126,13 +177,25 @@ When importing into an existing company, agent or project names may conflict wit > **Warning:** The `replace` collision strategy overwrites your existing agent configurations. Make sure you have a backup export before using it. +### Importing a large company + +A busy company with years of tasks and attachments makes a big package, and Paperclip handles that for you. The UI uploads the package as its compressed `.zip` rather than unpacking it in your browser first, and the server runs the import as a background job while the page watches its progress — so a slow network or a proxy dropping the connection doesn't lose the import. + +Two guards protect you along the way. If any part of the upload goes missing in transit, the import refuses to run rather than importing a fragment, and asks you to retry. And every attachment in the package is checked against its content hash before a single record is written, so a corrupted or tampered-with package can't leave you with a half-imported company. + +By default the server accepts import packages up to **1 GB**. If your export is bigger, an operator can raise the cap by setting the `PAPERCLIP_IMPORT_ZIP_MAX_BYTES` environment variable (in bytes, up to 64 GiB) — see [Environment Variables](../../reference/deploy/environment-variables.md). + --- ## Common use cases **Backing up your company configuration** -Run a full export periodically and store it in a safe place — a cloud drive, a private Git repository. If something goes wrong, you can restore from the package. +Run a full export periodically and store it in a safe place — a cloud drive, a private Git repository. If something goes wrong, you can restore from the package. For the full backup-and-restore recipe, including a scheduled nightly export, see [Back up and restore a company](../../how-to/back-up-and-restore-a-company.md). + +**Moving a company to another instance** + +Export on the instance you're leaving, then import the package on the instance you're moving to — a laptop to a server, a self-hosted box to Paperclip Cloud, or the other way around. Because the package carries tasks, routines, and attachments as well as configuration, the company picks up roughly where it left off. This is the supported path now that host-to-host Cloud Sync has been retired. **Starting a new company from a template** @@ -170,11 +233,13 @@ Review the dry-run output, then run without `--dry-run` to apply. Imported agents always start with scheduled heartbeats disabled. This is intentional — it gives you a chance to review the imported configuration and set your own budget and heartbeat settings before any agents start spending. +The UI goes one step further. **Start imported agents and routines paused** is ticked by default on the import screen, and when the import finishes you get an **Activate imported agents and routines** list. Tick the ones you're ready to wake and activate them from there — nothing runs until you say so. + After an import: 1. Open each imported agent and verify the adapter configuration looks right 2. Set per-agent budgets appropriate for your usage 3. Add any API keys or environment variables that the package declared but didn't include values for -4. Enable heartbeats when you're ready for agents to start working +4. Enable heartbeats, and activate the agents and routines you paused, when you're ready for work to start --- diff --git a/docs/guides/welcome/glossary.md b/docs/guides/welcome/glossary.md index ad2cb27..a359fa5 100644 --- a/docs/guides/welcome/glossary.md +++ b/docs/guides/welcome/glossary.md @@ -158,4 +158,12 @@ The unit of work in Paperclip. Each task has a title, description, priority, sta --- +## U + +### Unblock owner + +The party recorded as being able to clear a blocked task, together with a short action saying what they need to do. An unblock owner is either a specific agent (which gets woken once so it knows the task is waiting on it), a specific person who's an active company member, or the board. It's what lets the [Blocked Inbox](../day-to-day/blocked-inbox.md) name whose move it is instead of leaving you to guess from the description. + +--- + [Go to Installation →](../getting-started/installation.md) diff --git a/docs/how-to/back-up-and-restore-a-company.md b/docs/how-to/back-up-and-restore-a-company.md index 6fdba50..e2e9b46 100644 --- a/docs/how-to/back-up-and-restore-a-company.md +++ b/docs/how-to/back-up-and-restore-a-company.md @@ -2,25 +2,32 @@ Backups are exports. Restores are imports. Both flow through the same portable markdown package — the same files you'd hand to a teammate or version-control. This recipe walks the API path: preview → export → import → verify, with the safety rails that the CEO-scoped routes enforce. +The same pair also moves a company from one instance to another: export on the old host, import on the new one. That is now the supported migration path, since host-to-host Cloud Sync has been retired. + The CLI shortcut (`paperclipai company export | import`) is fine for one-off work — see [Export & Import](../guides/power/export-import.md). The HTTP routes below are what you'll wire into a routine, a CI job, or any agent that needs durable backups. --- ## What a package contains -A bundle is a directory of markdown files plus a `.paperclip.yaml` sidecar: +A bundle is a tree of markdown files plus a `.paperclip.yaml` sidecar and a content-addressed blob store: ```text my-company/ +├── README.md ├── COMPANY.md ├── agents/ -│ └── ceo/AGENT.md +│ └── ceo/AGENTS.md ├── projects/ │ └── main/PROJECT.md ├── skills/ │ └── review/SKILL.md -├── issues/ -│ └── 2026-04-27-onboarding/ISSUE.md +├── tasks/ +│ └── 2026-04-27-onboarding/ +│ ├── TASK.md +│ └── documents/brief.md +├── images/ +├── blobs/ └── .paperclip.yaml ``` @@ -28,15 +35,50 @@ The `include` flags decide which slices ride along: | Flag | Default | What it covers | |---|---|---| -| `company` | `true` | `COMPANY.md` + branding, budget, hiring policy | -| `agents` | `true` | `agents//AGENT.md` + adapter type + env-var declarations | +| `company` | `true` | `COMPANY.md` + branding, logo, budget, hiring policy | +| `agents` | `true` | `agents//AGENTS.md` + adapter type, runtime config, permissions, budgets, env-var declarations | | `projects` | `false` | `projects//PROJECT.md` + workspace config | | `skills` | `false` | `skills//SKILL.md` (referenced or vendored) | -| `issues` | `false` | `issues//ISSUE.md` (use sparingly — bundles get large) | +| `issues` | `false` | `tasks//TASK.md` and routines, with comments, labels, blockers, documents, work products, monitors, and attachments (use sparingly — bundles get large) | In addition to the include flags, you can scope by id with `agents`, `skills`, `projects`, `issues`, and `projectIssues` arrays. Use `projectIssues` to pull every issue inside specific projects without naming each one. -**Never in the bundle.** Secret values, API keys, machine paths, database ids, run history. Anything environment-specific. The package declares the env vars an agent needs; the values stay on the source machine. +Attachment bytes never sit inline in a markdown file. They live in `blobs/`, named by the SHA-256 of their contents, and each task's manifest entry points at the blob it needs. That's what makes the bundle self-verifying on the way back in. + +**Never in the bundle.** Secret values, API keys, machine paths, database ids. Anything environment-specific. The package declares the env vars an agent needs; the values stay on the source machine. + +**Deliberately left behind.** Approvals, cost events, and activity log entries. These describe what happened on the source instance rather than how the company is set up, so they don't travel. The next section shows you how to find out, before you export, whether your company has any. + +--- + +## 0. Check what the bundle won't carry + +Ask the fidelity route what will be left behind: + +```bash +curl "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/export/fidelity" \ + -H "Authorization: Bearer $PAPERCLIP_API_KEY" +``` + +The response counts the records in each category the bundle handles, and returns a `warnings` array — one entry per category that has rows but can't be exported: + +```json +{ + "schema": "paperclip-export-fidelity-v1", + "companyId": "…", + "counts": { "issueDocuments": 14, "issueAttachments": 6, "approvals": 3, "costEvents": 812, "activityLogEntries": 4210, "…": 0 }, + "warnings": [ + { "code": "approvals_not_exported", "severity": "warning", "message": "3 approvals are not included in the export bundle." }, + { "code": "cost_history_not_exported", "severity": "warning", "message": "812 cost events are not included in the export bundle." }, + { "code": "activity_history_not_exported","severity": "warning", "message": "4210 activity log entries are not included in the export bundle." } + ], + "generatedAt": "2026-08-01T02:00:00.000Z" +} +``` + +Warnings are informational — none of them stop an export. Log them alongside the bundle so a future restore knows exactly what it isn't getting. The UI shows the same list as a **Not included in this export** panel. + +> **Who can call it.** Same rule as the export routes: the CEO agent of the route company, or a board caller with company access. --- @@ -60,16 +102,19 @@ The response (`CompanyPortabilityExportPreviewResult`) shape: "rootPath": "my-company", "counts": { "files": 12, "agents": 3, "skills": 2, "projects": 1, "issues": 0 }, "fileInventory": [ - { "path": "my-company/COMPANY.md", "kind": "company" }, - { "path": "my-company/agents/ceo/AGENT.md", "kind": "agent" }, - { "path": "my-company/projects/main/PROJECT.md", "kind": "project" } + { "path": "COMPANY.md", "kind": "company" }, + { "path": "README.md", "kind": "readme" }, + { "path": "agents/ceo/AGENTS.md", "kind": "agent" }, + { "path": "projects/main/PROJECT.md", "kind": "project" } ], - "manifest": { "schemaVersion": 1, "...": "..." }, - "files": { "my-company/COMPANY.md": "name: ...\n" }, + "manifest": { "schemaVersion": 6, "...": "..." }, + "files": { "COMPANY.md": "name: ...\n" }, "warnings": [] } ``` +Paths are relative to `rootPath`, not prefixed with it — `rootPath` is only the folder name the package unpacks into. + `fileInventory` is the inventory you skim before keeping anything. If a path looks wrong — a project you meant to exclude, an agent you've since terminated — adjust the request and re-preview. > **Who can call it.** The CEO agent of the route company, or a board caller with company access. Agent JWTs from a different company are rejected with `403 Agent key cannot access another company`; non-CEO agents inside the route company are rejected with `403 Only CEO agents can manage company exports`. @@ -97,10 +142,10 @@ To narrow further — say, drop a noisy project's `PROJECT.md` from a backup tha { "include": { "company": true, "agents": true, "projects": true }, "selectedFiles": [ - "my-company/COMPANY.md", - "my-company/agents/ceo/AGENT.md", - "my-company/agents/cto/AGENT.md", - "my-company/projects/main/PROJECT.md" + "COMPANY.md", + "agents/ceo/AGENTS.md", + "agents/cto/AGENTS.md", + "projects/main/PROJECT.md" ] } ``` @@ -122,7 +167,12 @@ curl -X POST "$PAPERCLIP_API_URL/api/companies/import/preview" \ -H "Content-Type: application/json" \ -d @- <<'JSON' { - "source": { "type": "inline", "rootPath": "my-company", "files": { "...": "..." } }, + "source": { + "type": "inline", + "rootPath": "my-company", + "files": { "...": "..." }, + "expectedFileCount": 12 + }, "target": { "mode": "new_company", "newCompanyName": "Horizon Labs (restored)" }, "include": { "company": true, "agents": true, "projects": true }, "collisionStrategy": "rename" @@ -130,6 +180,8 @@ curl -X POST "$PAPERCLIP_API_URL/api/companies/import/preview" \ JSON ``` +`expectedFileCount` is optional but worth sending: it is your assertion of how many files you put in the body, and the server refuses the import if fewer arrive. See [The two integrity guards](#the-two-integrity-guards) below. + The preview returns a `CompanyPortabilityPreviewResult` with the agent, project, and issue plans (`create` / `update` / `skip`) plus any required env inputs. Read it carefully — this is the contract for what the apply step will do. When the plan looks right, apply it: @@ -143,8 +195,38 @@ curl -X POST "$PAPERCLIP_API_URL/api/companies/import" \ The response includes the new `company.id` and per-entity `created` / `updated` / `skipped` actions. **Imported agents always start with timer heartbeats off** — set budgets, fill in env vars, then turn heartbeats back on when you're ready. +Add `"pauseAutomations": true` to the request body if you also want the imported agents and routines to land paused. That's a separate switch from the heartbeat default: heartbeats-off stops them waking on a timer, `pauseAutomations` stops them running at all until you unpause each one. The UI ticks it by default; the CLI leaves it off. + > **Why this is instance-admin only.** Creating a new company is an instance-level action: it allocates a tenant, an issue prefix, and a budget policy. This route requires board access with instance-admin rights. Agents can't do that, even CEOs. The CEO-safe routes in the next section handle imports into the route company only. +### Restoring a large bundle + +A full-fidelity bundle for a busy company gets big, and a single inline JSON body is the fragile way to ship it. Two mechanisms exist for that case, and both are worth wiring into any restore you expect to run under pressure. + +**Upload the zip instead of inline JSON.** `POST /api/companies/import` (and its `/preview` sibling) also accept the compressed package directly — as `multipart/form-data` with the archive in a `package` file field and the rest of the import request as a JSON `meta` field, or as a bare `application/zip` body with `meta` in the query string. The server unzips it into exactly the same `{ rootPath, files }` bundle an inline source carries, so import semantics are identical either way. The compressed upload is roughly a third the size and survives proxies that would truncate the inflated JSON. + +```bash +curl -X POST "$PAPERCLIP_API_URL/api/companies/import?async=1" \ + -H "Authorization: Bearer $BOARD_TOKEN" \ + -F "package=@my-company.zip" \ + -F 'meta={"target":{"mode":"new_company","newCompanyName":"Horizon Labs (restored)"},"collisionStrategy":"rename"}' +``` + +**Run it as a background job.** Add `?async=1` and the route answers `202` immediately with a job id and a `statusUrl` instead of holding the connection open: + +```json +{ "job": { "id": "…", "status": "running" }, "statusUrl": "/api/companies/import/jobs/…", "retryAfterMs": 1000 } +``` + +Poll that URL until `job.status` is `succeeded` or `failed`; a successful board job carries the full import result under `job.importResult`. A job is readable only by the identity that created it, and lives in memory — a server restart, an unknown id, or someone else's id all return the same `404`. Only one import runs per account at a time: resubmitting the *same* request while it's still running returns `409` pointing at the job already in flight so you can adopt it, and a *different* import returns `409` with no job attached. + +### The two integrity guards + +Restores fail closed rather than half-writing a company. + +1. **Truncation.** An inline source may declare `expectedFileCount` — the number of files the client believes it sent. If fewer arrive, the import is rejected with `import_payload_incomplete` and the expected/received counts, so a cut-off upload can never import a fragment. More files than declared is not a truncation symptom and is allowed; omitting the field keeps older callers working. +2. **Tampering or corruption.** Before a single row is written, every `blobs/` entry is hashed and compared against the name it's filed under. A mismatch aborts the whole import naming the offending file. + --- ## 4. Import into the same company @@ -245,6 +327,19 @@ A bundle that round-trips cleanly today will round-trip cleanly in six months. A --- +## 8. Migrating to another instance + +Moving a company to a different host — laptop to server, self-hosted to Paperclip Cloud, or back again — is the same two steps with the second one pointed somewhere else: + +1. Export on the source instance (sections 1 and 2), with `include` covering everything you want to keep: `company`, `agents`, `projects`, `skills`, and `issues`. +2. Import on the target instance with a board token and `target.mode: "new_company"` (section 3), using the zip upload and `?async=1` if the bundle is large. + +Then finish the restore checklist: env-var values, budgets, adapters, and finally heartbeats. Nothing connects the two instances to each other — the package is the only thing that travels, which is what makes it safe to move it through a Git repo, object storage, or a USB stick. + +This replaces the retired host-to-host Cloud Sync. If you have scripts pointed at `cloud connect` or `cloud push`, port them to `POST /api/companies/{id}/export` plus `POST /api/companies/import`, or to `paperclipai company export` and `paperclipai company import`. + +--- + ## See also - [Export & Import](../guides/power/export-import.md) — CLI walkthrough and package format. diff --git a/docs/how-to/connect-agent-to-github.md b/docs/how-to/connect-agent-to-github.md index 7a48e10..cfd4150 100644 --- a/docs/how-to/connect-agent-to-github.md +++ b/docs/how-to/connect-agent-to-github.md @@ -151,6 +151,21 @@ It is fine for **local development on a single host**. It is not fine for: Treat host-keyring auth as the smoke-test option. Promote to Option A as soon as you have a second agent or move past your laptop. +### The credential preflight catches a missing token before the work + +Getting told about a missing token *after* the agent has implemented and reviewed a change is the worst possible time to hear it. So before dispatching a run on a local, git-sensitive adapter, Paperclip checks whether the run is going to need push credentials — and if it does, whether `GH_TOKEN` or `GITHUB_TOKEN` is actually bound at agent or project scope. If neither is, the run is blocked up front with the reason `push_write_credential_missing` and this remediation: + +> GitHub PR workflow requires GH_TOKEN or GITHUB_TOKEN bound at project or agent scope. + +There are two ways a run gets flagged as needing push credentials: + +1. **The issue mentions the GitHub PR workflow skill.** The explicit signal. +2. **The issue's title or description states a PR deliverable in plain text.** Routine-created tasks and agent-to-agent hand-offs rarely name the skill, even when the text literally says to push a branch and open a PR — so Paperclip reads the text too. + +The text check is deliberately conservative: it looks for a **verb** attached to the deliverable — opening, creating, raising, or submitting a pull request, or pushing a branch or pushing to a remote. A passing mention like "the PR merged yesterday" or "PR feedback addressed" won't trigger it, and neither will "push back on the upstream dependency change". + +This means a task worded like *"Fix the null org id crash, then open a pull request"* asks you for a token before a single run is spent. If you ever do get a false positive — a configuration-incomplete blocker asking for a GitHub token on work that genuinely doesn't need one — bind the token anyway or reword the task; the blocker names the exact remediation either way. + --- ## 4. The agent instructions: PR-driven work @@ -234,6 +249,9 @@ If your team uses Paperclip's [Execution Policy](../guides/power/execution-polic **`Permission denied (publickey)` on push.** The worktree's remote is HTTPS, but the agent's environment doesn't have `GITHUB_TOKEN` set. Check the agent's adapter config and confirm the `env` block resolves the secret. The hello-world test for this: `cd && env | grep -E 'GH_TOKEN|GITHUB_TOKEN'` from the same shell the heartbeat runs in. +**The run never started — it's blocked with `push_write_credential_missing`.** +The preflight decided this run needs push credentials and couldn't find `GH_TOKEN` or `GITHUB_TOKEN` bound at agent or project scope. Bind one (Step 3) and the run dispatches. If the task genuinely doesn't need to push, check whether its title or description reads like a PR deliverable — that's the second trigger. + **`gh: command not found`.** The agent host doesn't have the GitHub CLI installed. `brew install gh` or pull it from the [official releases](https://github.com/cli/cli/releases). The Claude Code adapter does not bundle `gh` — it has to be on `PATH` for the heartbeat process. diff --git a/docs/how-to/connect-aws-secrets-vault.md b/docs/how-to/connect-aws-secrets-vault.md index 1ca195e..5df49ed 100644 --- a/docs/how-to/connect-aws-secrets-vault.md +++ b/docs/how-to/connect-aws-secrets-vault.md @@ -14,7 +14,7 @@ If you only need the conceptual model — strict mode, the local encrypted provi You need three things on the host running the Paperclip server: -- AWS credentials the process can read (the usual chain: env vars, instance role, or `~/.aws/credentials`). The credentials need `secretsmanager:ListSecrets` — that's the only call discovery makes. Reading and writing secret values at runtime uses the usual `GetSecretValue` / `PutSecretValue` permissions on whatever ARNs the secrets resolve to. +- AWS credentials the process can read (the usual chain: env vars, instance role, or `~/.aws/credentials`). The credentials need `secretsmanager:ListSecrets` — that's the only call discovery makes. Reading and writing secret values at runtime uses the usual `GetSecretValue` / `PutSecretValue` permissions on whatever ARNs the secrets resolve to, plus `secretsmanager:UpdateSecretVersionStage` if you want to [write values through to linked secrets](#change-the-value-of-a-linked-aws-secret) — that one backs the rollback on a failed write. - A region you intend to read from, for example `us-east-1`. - An owner role on the company in Paperclip — only owners can write `secret_provider_config` rows. @@ -77,13 +77,35 @@ The server runs a health check on save (the same one bound to the per-vault heal Now that the vault exists, any secret on this company can use it. 1. Switch back to the **Secrets** tab. -2. On any existing AWS-backed secret, open the update dialog and click **Update value** (or **Update reference** if the secret is an external reference). The dialog has a **Provider vault** dropdown — pick the vault you just created and Paperclip writes the new version to AWS through it. The dropdown's default option is labeled **Deployment default**, and any vault with a `blockReason` (Disabled, failed health) is listed but not selectable. +2. On any existing AWS-backed secret, open the update dialog and click **Update value** (or **Update reference** if the secret is an external reference that can't take a value write — see below). The dialog has a **Provider vault** dropdown — pick the vault you just created and Paperclip writes the new version to AWS through it. The dropdown's default option is labeled **Deployment default**, and any vault with a `blockReason` (Disabled, failed health) is listed but not selectable. 3. For new secrets, the same vault picker appears in the create dialog. Existing secrets that were created before this vault stay on whichever vault they already pointed at. Re-point them via **Update value** the next time you turn the key. --- +## Change the value of a linked AWS secret + +Linking a secret used to be a one-way street: Paperclip could read the AWS value and repoint the link, but if the credential itself changed you had to go to the AWS console and put the new value there yourself. You don't anymore. AWS Secrets Manager supports writing values through to linked secrets, so you can rotate the credential from the Secrets page. + +Open the update dialog on a linked AWS secret and you get two tabs: + +- **Write new value** — paste the new credential. Paperclip writes it into the AWS secret your reference already points at. The field shows the exact reference it will write to underneath, so you can double-check the target before you commit. +- **Change reference** — the original behavior. Point the secret at a different AWS secret without writing anything to AWS. + +The detail pane tells you which of these the secret can do. A linked secret that supports value writes reads *"Paperclip resolves this provider reference and can write new values to it via Update value."* One that doesn't reads *"Paperclip resolves this provider reference but does not rotate the provider value."* — and its menu action stays labeled **Update reference** rather than **Update value**. + +Two things to know before you use it: + +- **The new value becomes current for everyone.** Paperclip writes a new AWS version and makes it `AWSCURRENT`, exactly as a rotation in the AWS console would. Anything else reading that AWS secret — Lambdas, ECS tasks, another team's service — picks up the new value too. That is usually the point, but it means this is not a Paperclip-only change. +- **You can't do both at once.** Writing a value and repointing the reference are separate operations. Pick one tab, save, and run the second change as its own update if you need it. + +Writing a value uses `PutSecretValue` on the target ARN, on top of the `GetSecretValue` that resolution already needed. Grant `secretsmanager:UpdateSecretVersionStage` as well: if the write fails partway, Paperclip moves `AWSCURRENT` back to the version that was current before, and without that permission the rollback can't run — leaving the new version live even though the update reported a failure. + +> **Tip:** Paperclip keeps tracking `AWSCURRENT` rather than pinning the version it just wrote. Rotate the same secret directly in AWS later and Paperclip still resolves the newest value — the write-through path doesn't lock the link to one version. + +--- + ## Remove a vault you no longer need When a vault is retired in AWS, or you registered it by mistake, remove it from Paperclip without touching AWS itself. @@ -108,9 +130,16 @@ Secrets that referenced the removed vault lose the association and fall back to **You want to test against AWS without writing to it** — Save the vault with status **Disabled**. The metadata is stored, discovery results remain visible on edit, and no rotation can target it until you flip the status back. +**"… does not support writing values to external reference secrets"** — The message names the provider that refused the write. AWS Secrets Manager implements write-through, so seeing this means the secret is backed by a provider that doesn't — GCP Secret Manager and HashiCorp Vault are placeholders in the current build. Move the credential to an AWS-backed secret, or change the value in that provider's own console and leave the Paperclip secret as a plain reference. + +**"Provide either a new value or a new external reference, not both"** — The update carried a new value *and* pointed at a different reference. Do them one at a time: write the value, save, then change the reference in a second update (or the other way round). + +**The linked secret only offers "Update reference"** — Either the secret has no external reference stored yet, or its provider doesn't advertise value writes. Only providers that support it get the **Write new value** tab. + --- ## Related - [Secrets](../reference/deploy/secrets.md) — the secret-store model, strict mode, and how secret refs resolve at runtime. +- [Secrets API](../reference/api/secrets.md#rotating-an-external-reference-secret) — the same value write from the API, including every validation rule. - [Update or rotate a provider API key](rotate-provider-api-key.md) — Path B uses provider vaults like the one created above. diff --git a/docs/how-to/create-a-daily-routine.md b/docs/how-to/create-a-daily-routine.md index 234bc76..82164db 100644 --- a/docs/how-to/create-a-daily-routine.md +++ b/docs/how-to/create-a-daily-routine.md @@ -126,6 +126,60 @@ In the UI, any `env` value with `type: "secret_ref"` is rendered through a secre --- +## 5. (Optional) Don't run on a dead week + +A daily routine on a quiet project is mostly noise — thirty standups in a row that all say "nothing changed". The activity gate turns that off: a scheduled tick only fires if something actually happened since the routine last ran. + +### Turn it on from the routine page + +Open the routine and go to its **Delivery** section — the same place you set concurrency and catch-up. Under those, you'll find **Advanced run policy** with two cards: + +- **Run on every scheduled tick** — "Fire on the schedule no matter what — the default behavior." +- **Skip when there's been no activity since the last run** — "On a scheduled tick, only run if something happened since the last run that finished. Lets a watcher-style routine stay asleep while the system is settled instead of burning tokens." + +Pick the second card and an **Activity scope** picker appears underneath it, with two more cards: + +- **Company-wide** — "Any activity across the company counts as a reason to run." +- **This project** — "Only activity in the routine's project counts as a reason to run." + +Save the routine and the gate is live from the next tick. + +If the routine doesn't have a schedule trigger yet, you'll see the cards greyed out rather than missing, with the reason spelled out: "Add a schedule trigger to gate runs on activity. Webhook, manual, and API fires always run." Add a schedule trigger (step 2) and the control wakes up. + +### Or set it from the API + +The two fields behind those cards are `activityGatePolicy` and `activityGateScope`, so you can script the whole thing: + +```bash +curl -X PATCH "$PAPERCLIP_API_URL/api/routines/$ROUTINE_ID" \ + -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "activityGatePolicy": "require_external_activity", + "activityGateScope": "project" + }' +``` + +Both fields also work at create time, alongside `concurrencyPolicy` and `catchUpPolicy` in step 1. The defaults are `activityGatePolicy: "always"` — fire on every tick, which is what you get if you never touch this — and `activityGateScope: "company"`. + +### What counts as activity + +"Something happened" means an entry in the company's activity log recorded after the routine's last dispatched run: someone commented, an issue moved, an agent finished a piece of work. Three kinds of entries deliberately don't count: + +- Read and inbox bookkeeping — `issue.read_marked`, `issue.read_unmarked`, `issue.inbox_archived`, `issue.inbox_unarchived`. Opening your inbox isn't news. +- Anything the scheduler logged about this routine, including its own skip records. +- Anything an agent did while working on an execution issue this routine created. + +Those last two matter more than they look. Without them a routine would keep re-arming itself on its own output, and the gate would never close. + +`activityGateScope` decides how wide to look. `company` counts activity anywhere in the company; `project` counts only activity tied to the routine's project — its issues, its routines and their runs, and agent runs working on its issues. Reach for `project` on the standup and triage patterns below, where a busy neighbouring project shouldn't wake up a quiet one. One catch: a routine with `activityGateScope: "project"` and no `projectId` can never match anything, so it will never fire on a schedule. + +A gated-off tick is not an error and not a backlog. It shows up in the run history as a `skipped` run with `failureReason: "no_external_activity"` — labelled **Skipped — no activity since last run** on the run row — the schedule advances to the next tick, and nothing is backfilled when the project wakes up again. + +The gate only applies to schedule triggers. Webhook firings and `POST /api/routines/{routineId}/run` always dispatch — if an external system took the trouble to call you, that call *is* the activity. + +--- + ## Pattern 1 — Daily standup The agent reads what changed since the previous standup and comments on a parent project issue with a short summary. Use `coalesce_if_active`: if the agent is still writing yesterday's standup when today's tick lands, merge the work — there's no value in two standup issues for the same morning. @@ -307,10 +361,20 @@ The runs you'll see: | `received` | The tick was accepted; dispatch is in flight. | | `issue_created` | A fresh execution issue was created and assigned. | | `coalesced` | An active run already existed; this tick linked to it. | -| `skipped` | An active run already existed; concurrency policy dropped this tick. | +| `skipped` | The tick didn't create work — an active run already existed and the concurrency policy dropped it, the project was paused, or the activity gate found nothing new. `failureReason` says which. | | `completed` | The execution issue reached `done`. | | `failed` | The execution issue failed, was cancelled, or dispatch errored. The `failureReason` field tells you which. | +In the UI you don't have to read `failureReason` yourself — the routine's **Runs** list writes the reason straight onto a skipped row as a one-line subtitle: + +| Row subtitle | `failureReason` | +|---|---| +| Skipped — no activity since last run | `no_external_activity` | +| Skipped — routine paused | `paused` | +| Skipped — worktree execution cutoff | `worktree_execution_cutoff` | + +Those three are the reasons with a label. A skipped run whose reason isn't one of them — a `skip_if_active` routine dropping a tick because the previous execution issue is still open, for instance — falls back to the normal row subtitle, the run's resolved variable values. That case isn't a mystery either: the row links to the execution issue that caused the skip. + For a deeper look at what the agent actually did, follow `linkedIssueId` to the execution issue and read its comments. To force one tick now without waiting for the cron: diff --git a/docs/how-to/debug-stuck-heartbeat.md b/docs/how-to/debug-stuck-heartbeat.md index 0dadc97..5a84c1b 100644 --- a/docs/how-to/debug-stuck-heartbeat.md +++ b/docs/how-to/debug-stuck-heartbeat.md @@ -4,7 +4,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. +A handful of 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. --- @@ -70,6 +70,30 @@ Five symptoms that cover almost every "this agent isn't working right" report. O **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. +--- + +## 7. Run succeeds, task stays in progress, then the agent wakes itself again + +**Symptom.** A run finishes as `succeeded`, but the task never leaves `in_progress`. Paperclip posts a **Missing issue disposition** notice — *"Paperclip needs a disposition before this issue can continue."* — and the same agent wakes up one more time on that task, sometimes doing real work before it finally sets a status. + +**Cause.** The run ended without telling Paperclip what happened to the task. Every successful run has to leave one of four outcomes behind: + +- `done` or `cancelled` +- `in_review` with a real reviewer path — `executionState.currentParticipant`, a human owner via `assigneeUserId`, a pending interaction, or a linked pending approval +- `blocked`, with blockers (`blockedByIssueIds`) or a clearly named unblock owner +- a delegated follow-up issue, or an explicit continuation recorded with `resumeIntent` and `resumeFromRunId` + +When none of those is present and nothing else already owns the next action — no queued wake, no pending approval or interaction, no monitor, no open recovery issue, no pause hold — Paperclip queues one corrective wake back to the same agent, with the wake reason `finish_successful_run_handoff`. + +**Fix.** Usually nothing: let it run. That corrective wake goes out on the agent's **normal** model and adapter settings, not the cheap, status-only recovery profile it used to get, and the wake carries the task's own description plus the agent's final report and recorded next action back to it. The agent re-reads its own evidence, does the smallest verification still missing, and only then picks an outcome — so this wake can look like a full work run in **Run history**, and it costs accordingly. + +Two things to keep in mind while you watch it: + +- **It only tries once.** The corrective wake is bounded to a single attempt per run, so you will never see this turn into a loop. If that one attempt still lands no disposition, Paperclip posts a **Missing disposition recovery blocked** notice — *"Paperclip could not resolve this issue's missing disposition automatically. The issue is blocked on a recovery owner."* — and hands the task to a recovery owner instead of trying again. +- **A stopped agent gets no wake at all.** If the assignee is paused, terminated, or awaiting approval, or is hard-stopped on budget, the corrective wake is skipped and the task simply sits in `in_progress` until you pick it up. + +If you would rather not pay for these wakes at all, the cure is upstream: have your agents record a status before they finish. A comment describing progress is not a disposition — Paperclip is looking at the task's status and path, not its prose. + ## Where to look first - **Run logs.** Agent detail → **Run history** → click any run for the full transcript and exit code. diff --git a/docs/how-to/develop-a-plugin-locally.md b/docs/how-to/develop-a-plugin-locally.md index 7818cd6..b68466a 100644 --- a/docs/how-to/develop-a-plugin-locally.md +++ b/docs/how-to/develop-a-plugin-locally.md @@ -21,6 +21,10 @@ When you run `paperclipai plugin install `, the CLI does two things differ You can force the local path interpretation with `--local` if you'd rather be explicit. The CLI rejects `--version` for local-path installs — version pinning only applies to npm packages. +On the server side, Paperclip canonicalizes the path before it loads anything from it: the value is resolved to an absolute path, then to its real path — `..` segments collapsed, symlinks followed — and it has to be a directory the server can read. A normal `paperclipai plugin install ~/dev/my-plugin` is unaffected by this, and there's no restriction on *where* on your filesystem the folder lives. Two things are worth knowing anyway. A symlinked plugin folder installs as its real target, so that's the path Paperclip records and watches. And a path that isn't a readable directory fails immediately with `Invalid localPath: …` rather than turning into a confusing manifest error further down. + +The one place local-path installs don't work at all is an instance managed by a Paperclip control plane rather than run by you — those only accept plugins bundled with the application. Develop against an instance you run yourself, which is what the rest of this guide assumes. + Local-path plugins run as trusted local code, under the same trust boundary as the rest of the running Paperclip instance. There is no sandboxing of worker code and no signature check beyond "Paperclip can read the path you gave it." Don't local-install a plugin you didn't write or audit. --- @@ -170,6 +174,8 @@ When you're done iterating, publish the package and reinstall from the npm form **Install returns `error` status.** Run `paperclipai plugin inspect ` for the last error. The three usual suspects: (1) the plugin hasn't been built yet — run `pnpm dev` or `pnpm build` first; (2) the `paperclipPlugin` entries in `package.json` point at files that don't exist on disk; (3) the manifest failed validation. The Paperclip server log has the full validation error. +**Install fails with `Invalid localPath: …`.** The server couldn't turn your argument into a readable directory. `path does not exist` means the resolved path is wrong — check the spelling. (The CLI always sends an absolute path. If you're calling `POST /api/plugins/install` directly instead, a relative path is resolved against the server process's own working directory.) `path is not a directory` means you pointed at a file instead of the package folder. `path is not readable` means the folder is there but the Paperclip server process doesn't have permission to read it. + **Edits don't seem to reload.** Confirm `pnpm dev` is still running and writing to `dist/`. If you renamed entry files, update `paperclipPlugin.manifest`, `paperclipPlugin.worker`, or `paperclipPlugin.ui` in `package.json` so the watcher targets them. **Worker restarts but the UI is stale.** Hard-reload the page. For HMR during UI iteration, run `pnpm dev:ui` and set `devUiUrl` in your manifest to `http://127.0.0.1:4177`. diff --git a/docs/how-to/sync-to-cloud-upstream.md b/docs/how-to/sync-to-cloud-upstream.md index 2b113e3..fd35f81 100644 --- a/docs/how-to/sync-to-cloud-upstream.md +++ b/docs/how-to/sync-to-cloud-upstream.md @@ -4,6 +4,8 @@ paperclip_version: v2026.609.0 # Sync a local company to a Paperclip Cloud upstream +> **Deprecated — being removed.** Host-to-host Cloud Sync has been retired upstream. To move a company to another instance (including a hosted one), export it to a portable package and import it there instead — the replacement is full-fidelity and works local-to-local, over a URL, or from GitHub. See [Back up and restore a company](./back-up-and-restore-a-company.md). This page is kept temporarily for anyone on an older build and will be removed in a future release. + Cloud Upstream sync moves a local Paperclip company — its agents, projects, issues, skills, and portability files — into a connected Paperclip Cloud stack. You preview what would change, resolve any conflicts, push the work, and then decide which entity types (agents, routines, monitors) should activate on the cloud side. The flow is opt-in, experimental, and gated behind an instance setting. Nothing leaves your local instance until you flip the flag and authorize a connection. diff --git a/docs/how-to/update-paperclip.md b/docs/how-to/update-paperclip.md index 720f172..57003da 100644 --- a/docs/how-to/update-paperclip.md +++ b/docs/how-to/update-paperclip.md @@ -1,10 +1,14 @@ ---- -paperclip_version: v2026.525.0 ---- - # Update Paperclip to the latest version -A new Paperclip release has dropped — maybe you saw it on the [releases page](https://github.com/paperclipai/paperclip/releases), maybe an agent flagged a bug that's already fixed upstream, maybe you just want the newest UI. This guide walks you through updating an existing install. The steps depend on how you originally installed Paperclip, so pick the path that matches. +A new Paperclip release has dropped — maybe you saw it on the [releases page](https://github.com/paperclipai/paperclip/releases), maybe an agent flagged a bug that's already fixed upstream, maybe you just want the newest UI. This guide walks you through updating an existing install. + +The short version, if you installed Paperclip with `paperclipai install`: + +```bash +paperclipai update +``` + +That checks npm, takes a database backup, swaps in the new version, restarts your background service if you have one, and leaves the previous version sitting there ready for an instant rollback. The rest of this page unpacks what that means, and covers the other ways you might have installed Paperclip. If you're installing for the first time instead, follow [Installation](../guides/getting-started/installation.md). @@ -16,116 +20,153 @@ Paperclip uses **calendar versioning**: `YYYY.MDD.P`. The pieces are the year, t Every stable release has notes at `releases/vYYYY.MDD.P.md` in the parent repo and on the [GitHub releases page](https://github.com/paperclipai/paperclip/releases). Skim those before updating — they call out breaking changes, migrations, and new env vars. ---- - -## Which install do you have? - -| Install path | How to tell | Which section below | -|---|---|---| -| Terminal (`npx paperclipai`) | You run `npx paperclipai run` (or it's wrapped by systemd). | [Path A](#path-a--terminal--npx) | -| Git clone | You ran `git clone`, then `pnpm install && pnpm dev`. | [Path B](#path-b--git-clone--self-hosted) | - -> **Note:** Using the unofficial, community-maintained desktop app? It updates itself — see [Community Desktop App](./community-desktop-app.md#updating). +Paperclip also nudges you. `paperclipai run` and `paperclipai doctor` do a cached, once-a-day check against npm and print a one-line notice when a newer version exists on your channel. Set `PAPERCLIP_UPDATE_CHECK=0`, or `updates.checkEnabled` to `false` in your config, to turn that off. --- -## Path A — Terminal / `npx` +## Check before you commit -The `paperclipai` CLI is a regular npm package. Updating means refreshing what `npx` (or your global install) resolves. - -### Step 1 — Check what you're on +`--check` looks up your channel on npm, compares it against what you're running, and tells you what it found — without touching anything. ```bash -npx paperclipai --version +paperclipai update --check +paperclipai update --check --json ``` -This prints the version of the resolved binary. Compare it against [npm](https://www.npmjs.com/package/paperclipai) or the [releases page](https://github.com/paperclipai/paperclip/releases). +It exits with code `10` when an update is available and `0` when you're already current, which makes it easy to script: -### Step 2 — Pull the latest +```bash +paperclipai update --check || paperclipai update --yes +``` -The fix depends on how you invoke Paperclip day to day: +`--dry-run` goes one step further and describes the exact action it *would* take — including whether a backup would be taken — without doing any of it. -**If you run `npx paperclipai …`:** +```bash +paperclipai update --dry-run +``` + +--- + +## Apply the update + +```bash +paperclipai update +``` + +`upgrade` is an alias, if that's the word your fingers reach for. + +Here's the sequence, so nothing is a surprise: + +1. **It works out how you installed Paperclip.** Managed install, global npm, `npx`, or a source checkout — each gets different treatment, covered below. +2. **It takes a database backup.** This happens before anything is swapped, using the same machinery as [`paperclipai db:backup`](../reference/cli/setup-commands.md#paperclipai-dbbackup). If the instance was never onboarded and has no data, the backup is skipped with a note. +3. **It downloads and verifies the new version**, then flips the active payload over to it atomically. Your previous version stays on disk. +4. **It restarts your background service**, if one is active for this instance, using the [hot restart](../reference/cli/service.md#restart-without-losing-work) that hands active agent runs over to the new process rather than dropping them. +5. **It prunes old payloads** it no longer needs to keep. + +If the restarted service fails to come back healthy at the new version, the update rolls itself back to the previous payload and restarts that instead, then reports the failure. You should not end up on a broken build. + +| Flag | Use | +|---|---| +| `--check` | Check for an available update without applying it. | +| `--dry-run` | Print the action without changing anything. | +| `--latest` | Switch to the latest stable channel. | +| `--canary` | Switch to the canary channel. | +| `--version ` | Install an exact published version. | +| `--rollback` | Flip back to the retained previous managed payload. | +| `--no-backup` | Skip the pre-update database backup. | +| `-y`, `--yes` | Confirm an explicit downgrade without a prompt. | +| `--json` | Print machine-readable output. | + +`--latest`, `--canary`, and `--version` are mutually exclusive — pick at most one. + +--- + +## Switch channels + +Without a channel flag, `update` stays wherever you already are: stable stays stable, canary stays canary, and a pinned version stays pinned. Pass a flag to move. + +```bash +paperclipai update --canary # try the canary channel +paperclipai update --latest # come back to stable +paperclipai update --version 2026.609.0 # pin to one exact version +``` + +Canary builds get new features earlier but can be rougher, and switching back to `--latest` is always available. + +Moving to an older version is a downgrade, and the CLI asks you to confirm it before proceeding. Pass `--yes` to confirm up front in a script. Bear in mind that going back in code does not go back in database schema — see the warning below. + +--- + +## Roll back + +If an update goes badly, you do not have to re-download anything. The managed install keeps your previous payload on disk, and `--rollback` flips straight back to it. + +```bash +paperclipai update --rollback +paperclipai update --rollback --dry-run # see which version you'd land on +``` + +It restarts an active service as part of the rollback, so you're back on the old version and serving within seconds. + +> **Warning:** Rollback reverses the *code*, not your *data*. Database migrations that ran during the update are not undone. If the new version migrated your database and you need to go all the way back, restore the pre-update backup — that's exactly why `update` takes one by default. + +`--rollback` is only available for managed installs. Other install types have no retained payload to flip back to. + +--- + +## About the pre-update backup + +Every managed update starts with a database snapshot, using your instance's configured backup directory and retention settings. You don't have to remember to do it. + +Two things are worth knowing: + +- **If the database isn't reachable, the update stops.** Paperclip won't quietly update without the safety net. The error tells you to start the service with `paperclipai service start` and retry, or to skip the backup deliberately with `--no-backup`. +- **`--no-backup` is a real option, not a trap.** On a throwaway dev instance, or when you've just taken a snapshot yourself, skipping it is reasonable. On anything you care about, let it run. + +To take a snapshot outside of an update, use [`paperclipai db:backup`](../reference/cli/setup-commands.md#paperclipai-dbbackup). + +--- + +## If you didn't use the managed install + +`paperclipai update` recognizes how you installed Paperclip and does the right thing — which sometimes means telling you it isn't going to touch your setup. + +### Global npm install + +`update` runs the `npm install -g` for you against the version you asked for, and prompts before an explicit downgrade. `--dry-run` prints the exact npm command without running it. + +```bash +paperclipai update +paperclipai update --version 2026.609.0 +``` + +### `npx` + +An `npx` run is ephemeral by definition — there's nothing installed to update. `update` says so and points you at the durable path: + +```bash +npx paperclipai install # move to a managed install +paperclipai update # from then on, this works +``` + +If you'd rather keep using `npx`, just add `@latest` to force a fresh resolve instead of the cached copy: ```bash npx paperclipai@latest run ``` -The `@latest` suffix forces npx to fetch the newest published version instead of reusing a cached older one. Once you've run it once, plain `npx paperclipai run` will pick up the cached new version on subsequent runs — until the next release. +### Source checkout -**If you have it installed globally:** +Paperclip will not mutate a git repository you're working in. `update` detects the checkout and hands it back to you: ```bash -npm install -g paperclipai@latest +git pull +pnpm install +pnpm db:migrate +pnpm dev ``` -Then `paperclipai --version` should show the new version on the next invocation. - -**If you're on the canary channel:** - -```bash -npx paperclipai@canary run -# or -npm install -g paperclipai@canary -``` - -Canary builds get new features earlier but can be rougher. Switch back to stable any time with `@latest`. - -### Step 3 — Restart Paperclip - -A running Paperclip process keeps using the version it booted with. Stop the process (Ctrl+C in the terminal it's running in) and start it again: - -```bash -npx paperclipai run -``` - -### Step 4 — If you run under systemd on a VPS - -The systemd unit from [Installation](../guides/getting-started/installation.md) uses `ExecStart=/usr/bin/npx paperclipai run`, which means a restart is enough to pick up a newer npx cache — but only if you also refreshed that cache. Do both: - -```bash -# As the paperclip user -npx paperclipai@latest --version # refresh the cache -sudo systemctl restart paperclip # restart the service -``` - -Check the service came back cleanly: - -```bash -sudo systemctl status paperclip -sudo journalctl -u paperclip -n 50 --no-pager -``` - -> **Warning:** If your service unit pins a specific version (`npx paperclipai@2026.318.0 run`), changing it requires editing the unit file and running `sudo systemctl daemon-reload` before the restart. - ---- - -## Path B — Git clone / self-hosted - -This is the developer path — you ran `git clone https://github.com/paperclipai/paperclip` and run `pnpm dev` directly. - -1. **Stop Paperclip.** Ctrl+C the dev process. Any agents currently mid-heartbeat will pick up where they left off after the restart. -2. **Pull the new code.** - ```bash - git pull - ``` - If you've made local commits or have a dirty tree, resolve those first (`git status`). -3. **Reinstall dependencies.** Updates frequently bump package versions and pnpm needs to refresh `node_modules`. - ```bash - pnpm install - ``` -4. **Run database migrations.** New releases sometimes add columns or tables. Run migrations before starting the server. - ```bash - pnpm db:migrate - ``` -5. **Restart.** - ```bash - pnpm dev - ``` - -> **Tip:** Before pulling, glance at the [releases notes](https://github.com/paperclipai/paperclip/releases) for the versions between yours and the latest. Anything labelled **breaking** or **migration** is worth reading first. - -### If you want to pin to a tagged release rather than `master` +Stop the dev process before pulling, and resolve a dirty tree first. To pin to a tagged release instead of tracking `master`: ```bash git fetch --tags @@ -135,27 +176,33 @@ pnpm db:migrate pnpm dev ``` -Switching back to tracking `master`: +### Managed install built from a git ref -```bash -git checkout master -git pull -``` +If you installed with `paperclipai install --ref `, `update` re-resolves that ref on GitHub and rebuilds if the commit moved. It warns you first, because building from source executes that repository's build scripts, and requires confirmation (or `--yes`) before it does. If you pinned to an exact commit SHA, there is nothing to move to and `update` tells you the install is pinned. --- ## Verify the update worked -Regardless of path: - -1. **Check the running version.** In the UI, hover the small **`v`** badge at the bottom of the left sidebar (next to the Documentation link and the settings/theme icons) — the tooltip shows the full server version, e.g. `v2026.525.0`. CLI users can run `paperclipai --version`. -2. **Open the dashboard.** Confirm the UI loads, your companies and agents are present, and nothing renders as an error state. -3. **Trigger one heartbeat.** Assign a small task to an existing agent or wait for the next scheduled heartbeat. Watch the run log for a successful turn. This confirms adapters still launch under the new binary. +1. **Check the running version.** In the UI, hover the small **`v`** badge at the bottom of the left sidebar (next to the Documentation link and the settings/theme icons) — the tooltip shows the full server version. From the terminal, `paperclipai --version` reports the CLI and `paperclipai service status` reports what the running server is actually serving. +2. **Run `paperclipai doctor`.** It checks the managed install and the background service, including whether the running server's version matches the version you just installed. +3. **Open the dashboard.** Confirm the UI loads, your companies and agents are present, and nothing renders as an error state. +4. **Trigger one heartbeat.** Assign a small task to an existing agent or wait for the next scheduled heartbeat. Watch the run log for a successful turn. This confirms adapters still launch under the new build. --- ## Troubleshooting +**`doctor` reports a version mismatch between the server and the managed install** — the service is still running the old build. Restart it and require the new version: + +```bash +paperclipai service restart --expected-version +``` + +**The update stopped with "the pre-update backup cannot be taken"** — the database isn't running or isn't reachable. Start it (`paperclipai service start`, or `paperclipai run` in a terminal), then retry. Use `--no-backup` only if you're deliberately skipping the snapshot. + +**"Another restart for instance … is still running"** — a previous restart didn't finish and left its lock behind. The message includes the lock file path; once you've confirmed nothing else is restarting, remove it and retry. + **`npx paperclipai` still reports the old version after `@latest`** — npx caches by name and falls back to the cache if the registry lookup is rate-limited or offline. Clear it and retry: ```bash @@ -163,18 +210,20 @@ npx clear-npx-cache # or: rm -rf ~/.npm/_npx npx paperclipai@latest --version ``` -**Database migration fails on a git-clone install** — Don't roll forward against a half-migrated database. Restore your last `pnpm db:backup` snapshot, file an issue with the migration error, and stay on the previous tag until it's resolved. See [Back Up and Restore a Company](./back-up-and-restore-a-company.md). +**Database migration fails on a source checkout** — Don't roll forward against a half-migrated database. Restore your last snapshot, file an issue with the migration error, and stay on the previous tag until it's resolved. See [Back Up and Restore a Company](./back-up-and-restore-a-company.md). -**The dev server boots but the UI is blank or shows old assets** — Hard-refresh the browser (Cmd+Shift+R) to bypass cached UI bundles. If you're behind a reverse proxy, also flush its cache. +**The server boots but the UI is blank or shows old assets** — Hard-refresh the browser (Cmd+Shift+R) to bypass cached UI bundles. If you're behind a reverse proxy, also flush its cache. **Agents stop running after the update** — Check the run log for adapter errors. New releases occasionally tighten env-var validation or require a newer adapter binary (Claude Code, Codex, etc.). Update those binaries on the host and re-test. -**You updated but want to roll back** — For npm: `npm install -g paperclipai@` or `npx paperclipai@ run`. For git clone: `git checkout `, then `pnpm install` and restart. Rollback is safe for code, but a forward-only migration may have already rewritten your database — restore the pre-update DB backup if so. +> **Note:** Using the unofficial, community-maintained desktop app? It updates itself — see [Community Desktop App](./community-desktop-app.md#updating). --- ## Related -- [Installation](../guides/getting-started/installation.md) — fresh install for each channel. +- [Installing the CLI](../reference/cli/installation.md) — the managed install store, channels, and `paperclipai uninstall`. +- [Service](../reference/cli/service.md) — running Paperclip in the background, and hot restarts. +- [Installation](../guides/getting-started/installation.md) — fresh install for each path. - [Back Up and Restore a Company](./back-up-and-restore-a-company.md) — take a snapshot before updating a production install. - [Deploy to a VPS or Fly.io](./deploy-to-vps-or-fly.md) — production deploy patterns that influence how you restart. diff --git a/docs/how-to/write-a-company-skill.md b/docs/how-to/write-a-company-skill.md index 755c66a..e70335f 100644 --- a/docs/how-to/write-a-company-skill.md +++ b/docs/how-to/write-a-company-skill.md @@ -153,7 +153,7 @@ You can also browse the result in the UI under **Skills** — the new row shows ## 4. Assign the skill to specific agents -Skills live company-wide; *which* agents see them is controlled by each agent's `desiredSkills`. To attach the new skill to two coder agents, sync each one with a list that includes the new key: +Skills live company-wide; *which* agents see them is controlled by each agent's `desiredSkills`. Sync takes a required `mode` that says how the skills you name should meet the agent's current set. To simply add the new skill to a coder agent without touching anything else it already has, use `mode: "add"`: ```bash curl -X POST "$PAPERCLIP_API_URL/api/agents/$CODER_AGENT_ID/skills/sync" \ @@ -161,13 +161,13 @@ curl -X POST "$PAPERCLIP_API_URL/api/agents/$CODER_AGENT_ID/skills/sync" \ -H "Content-Type: application/json" \ -d '{ "desiredSkills": [ - "paperclip", "release-note-writer" - ] + ], + "mode": "add" }' ``` -The route is **reconciling**, not additive: anything in `desiredSkills` is attached, anything *not* in the list is detached. Always send the full set, not just the new entry. If you don't know the agent's current set, read it first: +`mode` picks the behaviour: `add` keeps the agent's other skills and unions in the ones you name, `remove` drops only the named skills, and `replace` overwrites the complete desired set with exactly what you send (that's the reconciling behaviour — anything not in the list is detached). Because `add` and `remove` are now available, you no longer have to send the full set just to change one entry — reach for `replace` only when you truly want to overwrite everything. If you want to `replace` and don't know the agent's current set, read it first: ```bash curl "$PAPERCLIP_API_URL/api/agents/$CODER_AGENT_ID/skills" \ @@ -182,7 +182,7 @@ Each `desiredSkills` entry can be: - a skill **UUID** (`id` from the company skills list), - or a **slug** (`release-note-writer`) — fine for one-off calls but rejected with `Invalid company skill selection (ambiguous references: …)` if the slug matches more than one skill. -> **Bundled skills are always attached.** The server unions any `paperclipai/paperclip/*` bundled skills (`paperclip`, etc.) into every agent's resolved set, regardless of what you send. That's why you keep `paperclip` on the list above — leaving it off doesn't remove it; it just makes the response confusing. +> **Bundled skills are always attached.** The server unions any `paperclipai/paperclip/*` bundled skills (`paperclip`, etc.) into every agent's resolved set, regardless of what you send. So you never have to list `paperclip` to keep it — and even a `mode: "replace"` that omits it won't drop it from the resolved set. ### Adapter sync mode matters diff --git a/docs/reference/adapters/claude-code.md b/docs/reference/adapters/claude-code.md index 1392cbf..58eb6bf 100644 --- a/docs/reference/adapters/claude-code.md +++ b/docs/reference/adapters/claude-code.md @@ -1,5 +1,5 @@ --- -paperclip_version: v2026.720.0 +paperclip_version: v2026.817.0 --- # Claude Code diff --git a/docs/reference/adapters/codex.md b/docs/reference/adapters/codex.md index a648297..1b5a332 100644 --- a/docs/reference/adapters/codex.md +++ b/docs/reference/adapters/codex.md @@ -1,5 +1,5 @@ --- -paperclip_version: v2026.722.0 +paperclip_version: v2026.817.0 --- # Codex @@ -88,7 +88,6 @@ Pick any of the known Codex model ids in the `model` field. The current options - `gpt-5.6-luna` - `gpt-5.4` - `gpt-5.4-mini` -- `gpt-5.3-codex-spark` - `gpt-5` - `o3` - `o4-mini` @@ -103,6 +102,22 @@ You can also type a model id that is not in this list. Anything Paperclip does n --- +## Model Profiles + +A model profile is a named alternative to the agent's main model settings, so you can run the same agent on a different lane without rewriting its adapter config. Codex ships one profile, and it comes from the adapter itself (`source: "adapter_default"`), so every `codex_local` agent has it available: + +| Key | Label | Description | +|---|---|---| +| `cheap` | Cheap | Use an explicitly configured lower-cost Codex model without changing the primary model. | + +The important part is *explicitly configured*: the `cheap` profile ships with an empty `adapterConfig`, so it does not choose a model for you. Enabling it changes nothing until you fill in what the cheap lane should use — normally a `model`, and optionally an override such as `modelReasoningEffort`. You decide which model counts as cheap for this agent, and the primary `model` in your adapter config stays exactly as it is. + +> **Heads-up:** `cheap` used to hard-code a specific Codex model and reasoning effort for you. It no longer does. If you were relying on that, set the model you want on the profile yourself. + +New Codex agents start with this profile switched off. When an agent is created and its runtime config does not already mention `cheap`, Paperclip records `modelProfiles.cheap = { enabled: false }` — the profile is present but dormant, waiting for you to configure it and turn it on. + +--- + ## Session Persistence Codex preserves the `previous_response_id` chain so heartbeats can continue the same conversation instead of starting fresh each time. diff --git a/docs/reference/adapters/opencode.md b/docs/reference/adapters/opencode.md index 028f325..79383d6 100644 --- a/docs/reference/adapters/opencode.md +++ b/docs/reference/adapters/opencode.md @@ -1,5 +1,5 @@ --- -paperclip_version: v2026.618.0 +paperclip_version: v2026.817.0 --- # OpenCode @@ -115,6 +115,12 @@ OpenCode supports multiple providers. Common ids: Run `opencode models` for the authoritative list on your machine. +You are not limited to that list, though. OpenCode only resolves a `--model provider/model` when the model id exists in that provider's `models` map, so an id the bundled catalog does not carry — a model released after the catalog shipped, or a routing variant such as `openai/gpt-oss-120b:nitro` — would otherwise be rejected with `Model not found` even though the provider serves it. + +To spare you that, Paperclip registers whatever you put in `model` into the temporary `opencode.json` described above, adding it to that provider's `models` map when it is not already there. Models the catalog already knows keep all of their metadata, and an explicit definition — from your own OpenCode config or from `PAPERCLIP_OPENCODE_PROVIDERS` — always wins, so Paperclip never overwrites one. When it does add an entry, you will see a run note saying it registered the model. Like the rest of the runtime config on this page, this applies when `dangerouslySkipPermissions` is enabled and the target runs locally. + +Before a run, Paperclip pre-flights your configured model against `opencode models`. If the model is present in the returned list, the run proceeds. If it is missing, the run is rejected with `Configured OpenCode model is unavailable`, listing a sample of the ids that are available so you can correct the `model` value. The pre-flight is strict on purpose: if the probe returns no models at all, the run stops with `OpenCode returned no models. Run \`opencode models\` and verify provider auth.`, and if the `opencode models` call times out or errors the run stops there too — Paperclip would rather block a run it can't verify than start one that fails partway through. + --- ## Example diff --git a/docs/reference/adapters/overview.md b/docs/reference/adapters/overview.md index 23770ac..6c6f844 100644 --- a/docs/reference/adapters/overview.md +++ b/docs/reference/adapters/overview.md @@ -1,5 +1,5 @@ --- -paperclip_version: v2026.720.0 +paperclip_version: v2026.817.0 --- # Adapters Overview diff --git a/docs/reference/adapters/sandbox-providers.md b/docs/reference/adapters/sandbox-providers.md index 5f90d7a..c767562 100644 --- a/docs/reference/adapters/sandbox-providers.md +++ b/docs/reference/adapters/sandbox-providers.md @@ -238,9 +238,92 @@ NetworkPolicy paperclip-egress-allow (DNS + paperclip-server callback + Each run then gets its own short-lived resources, named `pc-{ulid}`, that cascade-delete when the lease is released (a `Sandbox` CR + pod + `pc-{ulid}-env` secret under `sandbox-cr`, or a `batch/v1` Job + pod + secret under `job`). +### Task-scoped egress grants + +The `egressAllowFqdns` and `egressAllowCidrs` you set in the `Configure` table apply to every run in the environment, which quietly pushes you toward making them wide enough for your most demanding task. Task-scoped grants let you avoid that trade. Keep the environment's allow-list as tight as you like, then open the extra destinations one individual task needs, for the length of that task's run only. + +You set the grant on the task itself, inside its `executionWorkspaceSettings` (one of the execution fields described on the [Issues](../api/issues.md) API page): + +```json +{ + "executionWorkspaceSettings": { + "networkEgress": { + "allowFqdns": ["registry.npmjs.org", "files.pythonhosted.org"], + "allowCidrs": ["203.0.113.0/24"] + } + } +} +``` + +Both lists are optional. Paperclip trims each entry, drops blanks and duplicates, and lowercases the FQDNs; CIDRs are kept exactly as you wrote them. If both lists come out empty, nothing extra is created and the run simply uses the namespace's normal egress allow-list. + +When there is something to grant, the provider creates one more policy alongside the run's other resources, named after the run's workload as `pc-{ulid}-egress`. (If a workload name would push that past Kubernetes' 253-character name limit, the middle is shortened to fit.) Two details make it safe to hand out per task: + +- It selects only the pod carrying that run's `paperclip.io/run-id` label, so the extra destinations never become reachable from other agent pods sharing the tenant namespace. +- It carries an owner reference to the run's `Sandbox` CR or `Job`, so it's cleaned up along with the run. + +The per-run policy carries only the granted destinations. DNS and the callback to paperclip-server keep coming from the namespace-level policy, so a grant adds to the baseline rather than replacing it. The grant is read once, when the sandbox is claimed for the run — editing the task afterwards won't rewrite a policy that's already live. And if the policy can't be created, Paperclip releases the workload it just claimed and lease acquisition fails, so a run never starts with a grant that didn't actually get applied. + +#### What each egress mode really enforces + +This is where the `egressMode` you picked matters, because the two modes are not equivalent and the difference is worth understanding before you rely on one. + +With `egressMode: "cilium"`, the grant becomes a `CiliumNetworkPolicy`. Your `allowFqdns` are enforced as names: each one becomes an exact-name `toFQDNs` match, allowed on TCP 443. Your `allowCidrs` become a `toCIDRSet` entry with no port restriction. + +With `egressMode: "standard"` (the default), the grant becomes a plain `networking.k8s.io/v1` NetworkPolicy — and a Kubernetes NetworkPolicy has no way to express a hostname at all. It can only match IP blocks. So: + +- `allowCidrs` behave exactly as you'd expect: one `ipBlock` rule per CIDR, with no port restriction. +- `allowFqdns` are **not** enforced. If you grant FQDNs and leave `allowCidrs` empty, the policy falls back to allowing all public IPv4 on TCP 80 and 443 so the hosts you named are at least reachable, with `0.0.0.0/8`, `10.0.0.0/8`, `100.64.0.0/10`, `127.0.0.0/8`, `169.254.0.0/16`, `172.16.0.0/12`, `192.168.0.0/16`, and `224.0.0.0/4` carved out. Cluster internals, loopback, and the link-local metadata endpoint stay blocked — but every *other* public host on 80/443 becomes reachable too, not just the ones you listed. +- If you grant FQDNs **and** CIDRs together in standard mode, that fallback does not fire. Only your CIDRs are allowed, and the FQDNs you named are reachable only if they happen to resolve inside those CIDRs. + +The short version: under `standard`, treat `allowFqdns` as a statement of intent that buys the run public HTTP/HTTPS, and treat `allowCidrs` as the part that's genuinely enforced. If you need a grant to mean "this hostname and nothing else", run Cilium and set `egressMode: "cilium"`. + +#### When a request gets blocked + +Two things make a denied request diagnosable from inside the run instead of leaving the agent guessing. + +First, every sandbox is handed the effective grant through its environment: + +| Variable | Value | +|---|---| +| `PAPERCLIP_NETWORK_EGRESS_POLICY` | `kubernetes-default-deny` | +| `PAPERCLIP_NETWORK_EGRESS_GRANT_PATH` | `executionWorkspaceSettings.networkEgress` | +| `PAPERCLIP_NETWORK_EGRESS_ALLOW_FQDNS` | The granted FQDNs, comma-separated (empty when there are none) | +| `PAPERCLIP_NETWORK_EGRESS_ALLOW_CIDRS` | The granted CIDRs, comma-separated (empty when there are none) | + +Second, when a command's stderr looks like a blocked network call — `could not resolve host`, `network is unreachable`, `connection timed out`, `failed to connect`, or `temporary failure in name resolution` — Paperclip appends a note to it saying the network policy denied or couldn't route the request, listing what the task currently has granted (or saying that nothing is granted), and pointing at `executionWorkspaceSettings.networkEgress` as the place to request access. That's usually enough for an agent to tell a flaky endpoint apart from a destination nobody opened for it. + +### Workspace file sync + +Every agent run moves files both ways: Paperclip pushes the workspace and the run's asset files into the sandbox before the agent starts, then pulls changed files and outputs back to the host afterwards. On the default `sandbox-cr` backend the provider now does that transfer itself, natively over the pod exec channel, and there's nothing for you to switch on. + +The provider implements the two file-sync lifecycle hooks, `onEnvironmentSyncIn` (host → pod) and `onEnvironmentSyncOut` (pod → host). Defining them makes the plugin worker advertise the `environmentSyncIn` and `environmentSyncOut` verbs, and Paperclip then routes each sync operation through **one** pod exec that streams a tar archive — instead of the generic base64 fallback, which pays a fresh exec (a fresh WebSocket to the API server) per chunk. Inbound, the host builds a tarball on disk and streams its raw bytes into the pod's stdin; outbound, the pod tars straight to stdout and the host streams that into a file. Nothing buffers the whole payload, so the fallback's 100 MB in-memory ceiling no longer applies — but the Paperclip host does need ephemeral disk for the archive, which it stages in a `paperclip-k8s-sync-*` directory under the system temp dir. + +Two limits worth knowing about, because the sandbox is untrusted and both fail closed rather than letting a hostile pod exhaust the host: + +- An outbound transfer aborts the moment the pod has streamed more than 8 GiB to host disk. +- The pod's stderr for a sync exec is capped at 1 MiB. + +Neither is exposed as an environment config field, and there are no new fields in the `Configure` table for this — the one setting that does affect sync is the existing `podActivityDeadlineSec`, which each sync exec inherits (in milliseconds) as its timeout. If you tightened that deadline, remember that large transfers now draw on the same budget as the run itself. + +Sync needs a workspace remote dir on the lease (`remoteCwd`, `/workspace` unless you set a `remotePath`), and that directory is the confinement root: every sandbox-side path must resolve inside it. Paperclip checks each path on the host first, then re-checks it in the pod through `realpath` and a `/proc/self/fd` pin so a symlink swapped in mid-transfer can't redirect a write outside the root. Outbound archives are sandbox-authored, so the host inspects every tar member (and every symlink and hardlink target) and refuses the archive before extracting if any of them would land outside the extraction directory. + +The `job` backend has no exec path at all, so it can't do native sync. Those leases are marked `nativeFileSyncUnsupported`, and Paperclip's per-lease capability gate keeps them on the byte-identical base64 fallback — you don't lose file transfer by choosing `job`, it's just slower. + +#### When a transfer fails + +Failures are loud. A non-zero exit from the in-pod script fails the whole transfer with the captured stderr attached, so you never get a silent partial success: + +- **Single files land atomically.** Each file is staged in a `0700` directory directly under the workspace root, gets its requested mode applied *before* the rename, then is moved onto its target with `mv -f`. An interrupted transfer can't leave a truncated file behind, and a secret file is never briefly world-readable. +- **Directory mappings extract in place** and are not atomic, matching what the base64 fallback's tar did. +- **Scratch is always swept.** Staging directories (named with the reserved `.paperclip-upload` prefix) are removed by a shell `trap` on any exit, including a failed one. +- **A confinement violation rejects before any byte moves,** as does a source file that gets replaced between validation and copy. + +One image prerequisite comes with this: the in-pod scripts need a path canonicalizer, either `realpath` or `readlink -f`. If your image has neither, sync fails closed rather than proceeding with the host check as its only defense. They also use `sh`, `tar`, `head`, `mkdir`, `chmod`, `mv`, `cp`, `dirname`, and `dd`, and read `/proc/self/fd`. The scripts are plain POSIX `sh` and deliberately avoid GNU-only flags, so BusyBox-based images are fine — but an image with no shell or no `tar` can't be a sync target. The first-party `ghcr.io/paperclipai/agent-runtime-*` images build on `ubuntu:22.04` and already carry all of this, so they need no changes. + ### Security baseline -Every agent pod runs non-root (`runAsUser: 1000`, `runAsNonRoot: true`), drops all Linux capabilities with `allowPrivilegeEscalation: false`, uses `readOnlyRootFilesystem: true` with explicit `emptyDir` mounts for the writable paths it needs, and applies `seccompProfile: RuntimeDefault`. Each tenant namespace enforces `pod-security.kubernetes.io/enforce: restricted` and starts from a deny-all NetworkPolicy, so the only egress that works is what the adapter defaults and your `egressAllowFqdns` / `egressAllowCidrs` open up. +Every agent pod runs non-root (`runAsUser: 1000`, `runAsNonRoot: true`), drops all Linux capabilities with `allowPrivilegeEscalation: false`, uses `readOnlyRootFilesystem: true` with explicit `emptyDir` mounts for the writable paths it needs, and applies `seccompProfile: RuntimeDefault`. Each tenant namespace enforces `pod-security.kubernetes.io/enforce: restricted` and starts from a deny-all NetworkPolicy, so the only egress that works is what the adapter defaults and your `egressAllowFqdns` / `egressAllowCidrs` open up — plus whatever an individual task adds through a [task-scoped egress grant](#task-scoped-egress-grants) for the length of its own run. For stronger isolation, install [Kata Containers](https://github.com/kata-containers/kata-containers) with the Firecracker hypervisor and set `runtimeClassName: kata-fc`. Each agent pod then runs inside a Firecracker microVM. This requires nodes capable of nested virtualization. diff --git a/docs/reference/api/activity.md b/docs/reference/api/activity.md index 0a3912e..ede3ffd 100644 --- a/docs/reference/api/activity.md +++ b/docs/reference/api/activity.md @@ -2,7 +2,7 @@ Activity is Paperclip's audit trail. Use it when you want to answer: what changed, who changed it, which object changed, and when did it happen? -The company-wide feed is newest-first and is meant for quick review. If you need the history for one issue or one heartbeat run, use the issue and run-specific endpoints below. +There are two reads of the same underlying log. The plain company feed (`/activity`) is a lightweight, newest-first list meant for quick review. The richer audit feed (`/audit/agent-actions`) is the one the UI's unified Activity page is built on — it adds cursor pagination, a scope toggle, attribution (which agent, which run, on whose behalf), and CSV export. If you need the history for one issue or one heartbeat run, use the issue and run-specific endpoints below. --- @@ -78,6 +78,141 @@ activity = response.json() +## Audit feed (agent actions) + +``` +GET /api/companies/{companyId}/audit/agent-actions +``` + +Returns the rich, cursor-paginated audit feed that powers the unified Activity page. Rows come newest first and carry attribution the plain feed does not: the acting agent, the heartbeat run, and the responsible user (resolved from the run when the row itself doesn't name one). Issue, comment, and document rows are enriched with the issue identifier, title, and a short comment excerpt. + +Query parameters: + +| Param | Type | Description | +|---|---|---| +| `actorScope` | `agents` \| `all` | Which actors to include. `agents` (the default) returns only rows with an agent attached; `all` returns every actor kind. | +| `agentId` | string (UUID) | Filter to one acting agent. | +| `responsibleUserId` | string | Everything done on one person's behalf. | +| `runId` | string (UUID) | Filter to one heartbeat run. | +| `entityType` | string | Exact entity type, such as `issue`. | +| `entityId` | string | Exact entity ID. | +| `action` | string | Action-name prefix match, so `issue` matches `issue.updated`, `issue.comment_added`, and so on. | +| `actorType` | `agent` \| `user` \| `system` \| `plugin` | Exact actor kind. | +| `from` | date | Earliest `createdAt` to include. | +| `to` | date | Latest `createdAt` to include. | +| `cursor` | string | Opaque cursor from a previous response's `nextCursor`. | +| `limit` | integer | Page size, 1–200. Defaults to `50`. | + +Each response is `{ items, nextCursor, accessTier }`. Pass `nextCursor` back as `cursor` to page; it is `null` on the last page. + +### Two-tier access + +This endpoint has two levels of access, and the level is reported back as `accessTier`: + +- **`actorScope=all` is open to any company member.** They get every actor kind (`agent`, `user`, `system`, `plugin`), but the sensitive attribution fields — `agentId`, `runId`, `responsibleUserId`, and `details` — are stripped to `null`, and the response comes back with `accessTier: "basic"`. Attribution filters (`agentId`, `responsibleUserId`, `runId`, `entityType`, `entityId`, `action`, `actorType`, `from`, `to`) are rejected with `403` for these callers. +- **Members holding the `audit:view_agent_actions` permission** get complete rows, may use every filter, and get `accessTier: "full"`. The default scope, `actorScope=agents`, always requires this permission regardless of tier. + + + + + +```bash +curl "http://localhost:3100/api/companies/company-1/audit/agent-actions?actorScope=all&limit=50" \ + -H "Authorization: Bearer " +``` + + + +```javascript +const url = new URL("/api/companies/company-1/audit/agent-actions", "http://localhost:3100"); +url.searchParams.set("actorScope", "all"); +url.searchParams.set("limit", "50"); + +const res = await fetch(url, { + headers: { + Authorization: `Bearer ${token}`, + }, +}); + +const { items, nextCursor, accessTier } = await res.json(); +``` + + + +```python +import requests + +url = "http://localhost:3100/api/companies/company-1/audit/agent-actions" +params = { + "actorScope": "all", + "limit": 50, +} +headers = { + "Authorization": f"Bearer {token}", +} + +response = requests.get(url, params=params, headers=headers) +feed = response.json() +``` + + + +## Export the audit feed as CSV + +``` +GET /api/companies/{companyId}/audit/agent-actions.csv +``` + +Streams the audit feed as a CSV attachment. It takes the same filter parameters as the feed above (`cursor` and `limit` are ignored — the export drives its own pagination). A single export tops out at 10,000 rows, so narrow the date range for long histories. + +This endpoint always requires the `audit:view_agent_actions` permission — there is no basic tier for the export. + +The export is itself an auditable act: Paperclip records an `audit.exported` activity event capturing who exported, the filter set they used, and how many rows left the system. + +The columns are, in order: `createdAt`, `action`, `actorType`, `actorId`, `agentId`, `runId`, `responsibleUserId`, `entityType`, `entityId`, `issueIdentifier`, `issueTitle`, `commentExcerpt`, `documentKey`. + + + + + +```bash +curl "http://localhost:3100/api/companies/company-1/audit/agent-actions.csv?from=2026-01-01" \ + -H "Authorization: Bearer " \ + -o agent-audit.csv +``` + + + +```javascript +const url = new URL("/api/companies/company-1/audit/agent-actions.csv", "http://localhost:3100"); +url.searchParams.set("from", "2026-01-01"); + +const res = await fetch(url, { + headers: { + Authorization: `Bearer ${token}`, + }, +}); + +const csv = await res.text(); +``` + + + +```python +import requests + +response = requests.get( + "http://localhost:3100/api/companies/company-1/audit/agent-actions.csv", + params={"from": "2026-01-01"}, + headers={"Authorization": f"Bearer {token}"}, +) + +with open("agent-audit.csv", "wb") as f: + f.write(response.content) +``` + + + ## Create activity event ``` @@ -92,7 +227,7 @@ Request body: | Field | Type | Required | Notes | |---|---|---:|---| -| `actorType` | `agent` \| `user` \| `system` | no | Defaults to `system` | +| `actorType` | `agent` \| `user` \| `system` \| `plugin` | no | Defaults to `system` | | `actorId` | string | yes | Free-form actor label or ID | | `action` | string | yes | Event name, such as `issue.updated` | | `entityType` | string | yes | What changed | @@ -284,7 +419,7 @@ Each activity row stores: | Field | Meaning | |---|---| | `companyId` | Which company the event belongs to | -| `actorType` | `agent`, `user`, or `system` | +| `actorType` | `agent`, `user`, `system`, or `plugin` | | `actorId` | Text label or ID for the actor | | `action` | Event name | | `entityType` | What was changed | diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index 8b50843..02c4330 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -904,13 +904,22 @@ Use them when the agent's adapter supports skill discovery and sync. ### Sync request -Send the desired skill set as the request body. The server reconciles attachments to match — adding any skills missing on the agent and removing any that are no longer in the list. +Send the desired skill set plus a `mode` that tells the server how to apply it, relative to the agent's current desired skills. The `mode` field is required. | Field | Required | Notes | |---|---|---| | `desiredSkills` | yes | Array of references. Each entry is a company-skill UUID, canonical `key`, or unique slug. Mix and match is fine. | +| `mode` | yes | One of `"add"`, `"remove"`, or `"replace"`. Chooses how `desiredSkills` merges with the current set. | -The same field is accepted at hire time on `POST /api/companies/{companyId}/agents` and `POST /api/companies/{companyId}/agent-hires`, so the agent comes online with skills already assigned. +`mode` decides the merge, keyed by skill key: + +- `"add"` — union: keeps the agent's other skills and adds the ones named in `desiredSkills` (a named skill already present is updated in place). +- `"remove"` — removes only the named skills from the current set; everything else stays. +- `"replace"` — destructively overwrites the complete desired set with exactly `desiredSkills`. This is the reconciling "send the full set" behaviour: anything missing is added and anything not in the list is removed. + +If `mode` is missing or not one of those three values, the route returns `422` with the message: `Skill sync requires mode: "add", "remove", or "replace". Use "replace" only to overwrite the complete desired skill set.` + +`desiredSkills` is also accepted at hire time on `POST /api/companies/{companyId}/agents` and `POST /api/companies/{companyId}/agent-hires`, so the agent comes online with skills already assigned. Those hire/create routes do **not** take `mode` — it applies only to `/skills/sync`. @@ -920,7 +929,7 @@ curl -s -X POST \ "http://localhost:3100/api/agents/{agentId}/skills/sync" \ -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ -H "Content-Type: application/json" \ - -d '{ "desiredSkills": ["paperclip", "improve-skill"] }' + -d '{ "desiredSkills": ["paperclip", "improve-skill"], "mode": "add" }' ``` ```js @@ -930,7 +939,7 @@ await fetch(`http://localhost:3100/api/agents/${agentId}/skills/sync`, { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, - body: JSON.stringify({ desiredSkills: ["paperclip", "improve-skill"] }), + body: JSON.stringify({ desiredSkills: ["paperclip", "improve-skill"], mode: "add" }), }); ``` @@ -940,7 +949,7 @@ requests.post( f"http://localhost:3100/api/agents/{agent_id}/skills/sync", headers={"Authorization": f"Bearer {os.environ['PAPERCLIP_API_KEY']}", "Content-Type": "application/json"}, - json={"desiredSkills": ["paperclip", "improve-skill"]}, + json={"desiredSkills": ["paperclip", "improve-skill"], "mode": "add"}, ) ``` diff --git a/docs/reference/api/attention.md b/docs/reference/api/attention.md index 6f1460d..f750fcb 100644 --- a/docs/reference/api/attention.md +++ b/docs/reference/api/attention.md @@ -28,23 +28,54 @@ curl -s \ -H "Authorization: Bearer " ``` +### Query Parameters + +| Param | Description | +|---|---| +| `includeDismissed` | `true` to include rows the current user has dismissed or snoozed. | +| `activitySince` / `activityUntil` | ISO timestamps bounding each item's `activityAt`. | +| `queue` | Only items belonging to the [decision queue](./decisions.md) with this key. | +| `sort` | `activity` (default) or `decide`. | +| `cursor` | Continue from a previous page's `nextCursor`. | +| `limit` | Integer between 1 and 100. Defaults to 50. | + +A bad boundary returns `400 Bad Request` with `"activitySince must be an ISO timestamp"` (or the same for `activityUntil`), and a reversed pair returns `"activitySince must be before or equal to activityUntil"`. Other validation messages are `"sort must be 'activity' or 'decide'"`, `"limit must be an integer between 1 and 100"`, and — for a cursor that is malformed or was minted under a different sort — `"Invalid attention cursor"`. + 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. | +| `totalCount` | The number of items in the whole filtered feed, before paging. | +| `decideNowCount` | How many of those are due today, by their decide-by deadline. This is the number behind the sidebar badge. | +| `nextCursor` | Pass this back as `cursor` for the next page, or `null` when you have reached the end. | | `countsBySourceKind` | Counts grouped by each attention source. | -| `items` | The ranked `AttentionItem` records. | +| `items` | The ranked `AttentionItem` records for this page. | 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. +Items also carry their sidecar state and a little provenance: + +| Field | What you get | +|---|---| +| `rank` | The item's 1-based position in the full ranked feed. | +| `queues` | The decision queues this item belongs to, each `{ key, title }`. | +| `decideBy` | `today`, `this_week`, `whenever`, a `YYYY-MM-DD` date, or `null`. | +| `decideByAttribution` | Who set that deadline, or `null`. | +| `snoozedUntil` | When a triage snooze lifts, or `null`. | +| `expiresAt` | When the underlying work ages out, or `null`. | +| `ruleKey` | The rule a proposed decision was raised under, or `null`. | +| `originAgentName` | The agent behind the item, when there is one. | +| `inlineResolvable` | Whether the row can be resolved in place rather than by opening the subject. | +| `trainingExampleId` | Set when you have already kept this item as a [decision training](./decision-training.md) example. | + ### Sources and severity `sourceKind` is one of: - `approval` +- `decision` - `issue_thread_interaction` - `join_request` - `recovery_action` @@ -57,7 +88,69 @@ Each item tells you what needs attention (`whyNow`), the subject to open (`subje `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. +Paperclip deduplicates the underlying signals, then ranks the feed. With the default `sort=activity` that means recent activity first, then severity, then source priority, then a stable deduplication key. With `sort=decide` the feed leads with items that have a real deadline — soonest first — then items marked `whenever`, then everything untriaged, breaking ties on `expiresAt`, severity, and the activity order above. + +Treat it as a decision queue, not as a complete history of every event in your company. + +### Decisions in the feed + +A `decision` item is an agent's open proposal: a question with a short list of options and the exact changes each one would make. Those rows are resolvable in place, and `expiresAt` tells you when the proposal lapses on its own. The full record, and the routes to answer it, live in the [Decisions API](./decisions.md). + +--- + +## Agent-addressed issue-thread interactions + +An `issue_thread_interaction` item is a card an agent raised on an issue thread — a question, a confirmation, a set of verdicts. Historically every such card waited for the board. Cards can now be **addressed to a specific agent** and resolved by an **eligible agent under company governance**, which changes both who can answer and whether the card shows up in this feed at all. + +The full interaction lifecycle (create, respond, withdraw, expiry) lives in the [Issues API](./issues.md#interactions); this section covers the addressee and resolver-policy surface that decides how a card is routed. + +### Resolver policy + +Two fields set on create, both exposed on every interaction record, decide who may resolve a card: + +| Field | Values | Meaning | +|---|---|---| +| `resolverPolicy` | `board_only`, `board_or_agents` | The policy requested when the card was created. Optional on create. | +| `addresseeAgentId` | agent UUID or `null` | Optional. Addresses the card to one specific agent, which is woken to resolve it. Must reference an invokable agent in the same company. | + +The server resolves the requested policy against **company governance** and stores the result as three fields on the interaction: + +| Field | Meaning | +|---|---| +| `resolverPolicy` | Mirrors `requestedResolverPolicy` for compatibility. | +| `requestedResolverPolicy` | The policy asked for: the create request's `resolverPolicy`, else the company's per-kind `defaultPolicy`, else the built-in default for that kind (`ask_user_questions` defaults to `board_or_agents`; all other kinds default to `board_only`). | +| `effectiveResolverPolicy` | What is actually enforced. Forced to `board_only` when the card carries a tool action, or when the company's per-kind governance `cap` is `board_only`; otherwise equal to `requestedResolverPolicy`. | + +Company governance lives on the company setting `interactionResolverGovernance` — a per-kind map of `{ defaultPolicy?, cap? }`, each value being `board_only` or `board_or_agents`. `defaultPolicy` sets the fallback when a create request omits `resolverPolicy`; `cap` is a ceiling that can only tighten a card down to `board_only`. + +### Who may resolve + +A board user may always resolve a card. An **agent** may resolve one only when every check passes: + +- `effectiveResolverPolicy` is `board_or_agents` — otherwise `403` `This issue-thread interaction is board-only`. +- If `addresseeAgentId` is set, the calling agent must be that addressee — otherwise `403` `Only the addressed agent or a board user may resolve this issue-thread interaction`. +- The calling agent is not the card's creator (`createdByAgentId`) — otherwise `403` `Agents cannot resolve interactions they created`. +- The call is not from the same run that created the card (`sourceRunId`) — otherwise `403` `Agents cannot resolve interactions created by the same run`. +- The call carries an active run id — otherwise `401` `Agent run id required`. + +Tool-action confirmations are always board-only, and task-watchdog runs can never resolve interactions. + +### Attention-feed filtering + +The board feed is deliberately quiet about cards that a governed agent is expected to handle. An `issue_thread_interaction` item is **dropped from this feed** when its `addresseeAgentId` points at an agent that is currently **invokable** — that card is the addressed agent's to resolve, not the board's. A card stays in the feed when: + +- `addresseeAgentId` is `null` (it was raised for the board), or +- the addressed agent is no longer invokable — the card falls back to the board so it never gets stranded. + +### Withdrawal and terminal expiry + +Addressed cards settle through the same administrative endings as any other interaction, plus one that is specific to addressees: + +- **Withdrawal** — the creator, the current issue assignee, or a board user can withdraw a pending card; its `result.outcome` becomes `withdrawn`. See [withdraw vs. cancel](./issues.md#withdraw-vs-cancel). +- **Issue closed** — when the issue reaches `done` or `cancelled`, every still-pending card on it expires with `result.outcome` `issue_closed`. +- **Addressee deleted** — when the addressed agent is deleted, its pending cards are cancelled with `result.outcome` `addressee_deleted` (`Cancelled because the addressed agent was deleted`). + +Treat `withdrawn`, `issue_closed`, and `addressee_deleted` as administrative endings, not answers — no decision was made. --- @@ -83,10 +176,13 @@ For an attention item, pass its `dismissalKey`, which begins with `attention:`. 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"`. +There is a second, different snooze. An inbox dismissal is yours alone and shows up on the item as `dismissal`. A **triage** snooze is set on the source itself, applies to everyone reading the feed, and shows up as `snoozedUntil`. Reach for triage when the whole company should stop seeing something until a date; reach for a dismissal when you personally want your own queue tidier. Triage lives on the [Decisions API](./decisions.md). + --- ## Where to go next - [Decisions](../../guides/day-to-day/decisions.md) — use the built-in operator workflow. +- [Decisions API](./decisions.md) — decision records, queues, and triage. - [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. diff --git a/docs/reference/api/built-in-agents.md b/docs/reference/api/built-in-agents.md index 78d4b33..b1c50a3 100644 --- a/docs/reference/api/built-in-agents.md +++ b/docs/reference/api/built-in-agents.md @@ -44,7 +44,7 @@ Every built-in has a state, computed per company and per key from the marked age |---|---| | `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. | +| `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, which you do by provisioning it again with the adapter details. See [Finishing first-time setup](#finishing-first-time-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. | @@ -58,6 +58,25 @@ Provisioning a built-in does **not** go through the full agent hire flow. There 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`. +### Finishing First-Time Setup + +Some built-ins arrive before you have configured them. An approved hire can leave you with an agent row that exists but whose `adapterConfig` is still empty — that is exactly the `needs_setup` state. The natural next step is to open the agent's setup dialog, pick an adapter type, fill in a model or command, and save. + +If you tried that while "require board approval for new agents" was on, you used to hit a wall: the request came back `409 Conflict` with `code: "built_in_agent_reconfiguration_requires_approval"` and the message "Built-in agent adapter changes require board approval before they can be applied." Since you were the board, there was nobody left to grant an approval you already held, and setup could never be completed. + +That now works. Sending adapter details for a built-in that has never finished its adapter setup is treated as the first-time configuration it is, and applies straight away — the same path you get when board approval is off. Call the usual endpoint: + +`POST /api/companies/{companyId}/built-in-agents/{key}/provision` + +You get `200 OK`, no approval is created, and the built-in moves from `needs_setup` to `ready`. + +Two nearby cases behave differently, and both are deliberate: + +- **Changing the adapter of a built-in that is already configured** — one in `ready` or `paused` — is a genuine reconfiguration, and still returns `409 Conflict` with `code: "built_in_agent_reconfiguration_requires_approval"`. +- **Sending adapter details while the built-in is still in `pending_approval`** returns `409 Conflict` with `code: "built_in_agent_pending_approval"` and the message "Built-in agent setup is already pending board approval." Wait for the board decision, then configure the agent. + +What counts as "finished" depends on the adapter type. For the `process` and `command` adapters, Paperclip looks for a `command` or a `script`. For `http`, it looks for a `url`, `endpoint`, or `webhookUrl`. For the `openclaw_gateway` and `hermes_gateway` adapters, it looks for a `baseUrl` or `url`. For everything else — including the local adapters the shipped built-ins default to — it looks for a `model`. + > **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. @@ -127,6 +146,8 @@ The body is optional — provision with an empty body to accept the registry def 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. +This is also the endpoint you use to complete the adapter setup of a built-in that is sitting in `needs_setup` — even under board approval, that first configuration applies directly and returns `200 OK`. See [Finishing first-time setup](#finishing-first-time-setup) for the details and the two cases that still require an approval. + diff --git a/docs/reference/api/companies.md b/docs/reference/api/companies.md index 4f33cbf..3f5fc90 100644 --- a/docs/reference/api/companies.md +++ b/docs/reference/api/companies.md @@ -1,5 +1,5 @@ --- -paperclip_version: v2026.609.0 +paperclip_version: v2026.817.0 --- # Companies @@ -72,9 +72,21 @@ Returns a company-wide projection of the outputs agents produced while working. | `q` | string (max 160 chars) | Free-text search over artifact title/summary and originating issue identifier/title. | | `groupBy` | `none`, `task`, `parent_task` | Grouping mode. `none` (default) returns a flat artifact list; `task` groups by the originating issue; `parent_task` rolls sub-issue artifacts up under their root issue. | | `groupIssueId` | UUID | When grouping, expand a single group (stack) into its own artifacts. | +| `starred` | `true`, `false` | Return only the documents the calling user has starred. Defaults to `false`. | | `limit` | integer 1–100 | Page size. Defaults to `30`. | | `cursor` | string | Opaque pagination cursor from a prior response's `nextCursor`. | +### Your starred documents + +Set `starred=true` when you want the shortlist instead of everything — the documents you personally starred, newest star first. A few things follow from that: + +- Only documents come back. Work products and attachments are left out, because stars only exist for documents. Pairing `starred=true` with a `kind` other than `all` or `document` therefore returns nothing. +- The usual document filters are relaxed. A starred document is included even if no agent authored or edited it, and even if it is one of the system issue documents that the unfiltered list hides. +- Stars are per-user, so this only works for a signed-in board caller. An agent caller asking for `starred=true` gets an empty list rather than someone else's stars. +- `cursor` paging still works, and pages by when you starred each document. + +To add or remove a star, use the document route on the [Resource Memberships](./resource-memberships.md) page. + ### Response The response is an envelope. In the ungrouped case it returns the flat `artifacts` list; when `groupBy` is `task` or `parent_task` it returns `groups` instead (or, when `groupIssueId` is supplied, the expanded group's `artifacts` plus `selectedGroup`). `nextCursor` is `null` when there are no further pages. diff --git a/docs/reference/api/costs.md b/docs/reference/api/costs.md index 2ac9bd9..c40cbb5 100644 --- a/docs/reference/api/costs.md +++ b/docs/reference/api/costs.md @@ -133,6 +133,30 @@ requests.post( > **Tip:** If you already know the issue, project, or run that caused the spend, send those IDs. They make the breakdown views much more useful later. +### Cache-adjusted cost + +Prompt caching means the amount a provider actually bills for a run is often well below what its raw token counts imply. Paperclip records the billed amount rather than the nominal one, so the dollars in the ledger match the dollars on your invoice. + +Adapters express this through their execution result: + +- `costUsd` — the cost the adapter reports for the run, in US dollars. +- `cacheAdjustedCostUsd` — the provider-billed cost after prompt-cache discounts, in US dollars. Adapters set this when they expose it separately from `costUsd`. + +Paperclip resolves the two into a single billed figure: + +- If `cacheAdjustedCostUsd` is a finite number that is zero or greater, that value is the billed cost. +- Otherwise, if `costUsd` is a finite number that is zero or greater, that value is the billed cost — a provider-reported `costUsd` is treated as already cache-adjusted. +- Otherwise there is no billed cost for the run. + +That resolved figure is what Paperclip converts into the `costCents` of the cost event it records for the run, so it is the number that flows into every summary, breakdown, and budget policy described below. It is also the figure used to decide `costStatus`: a run with token usage but no resolvable cost is recorded as `unpriced` rather than as a real zero. + +The run keeps both numbers. The heartbeat run's `usageJson` carries `costUsd` when the adapter reported one and `cacheAdjustedCostUsd` when a cache-adjusted amount was resolved, alongside the token counts and the `provider`, `biller`, `model`, `costStatus`, and `billingType` fields. Read them from either of: + +- `GET /api/companies/{companyId}/heartbeat-runs` +- `GET /api/heartbeat-runs/{runId}` + +> **Note:** `cacheAdjustedCostUsd` and `costUsd` are in dollars, while `costCents` on cost events is in cents. Don't mix the two units when you reconcile a run against its ledger entry. + --- ## Reading Spend diff --git a/docs/reference/api/decisions.md b/docs/reference/api/decisions.md new file mode 100644 index 0000000..d646e60 --- /dev/null +++ b/docs/reference/api/decisions.md @@ -0,0 +1,468 @@ +# Decisions + +Sometimes an agent gets far enough to know what should happen next, but not far enough to be allowed to do it on its own. A **decision** is how it asks. The agent writes down the question, offers you a short list of options, and attaches the exact changes each option would make. You pick one, and Paperclip runs those changes for you. + +Everything about that exchange is a record. The decision is stored, the options are signed so they cannot drift, the effects are executed one at a time with an audit entry each, and the outcome is available afterwards. Open decisions also show up in the [Attention](./attention.md) feed as a `decision` source, so you meet them in the same place as everything else that needs you. + +This page also covers **decision queues** and **triage** — the two sidecars that let you organise that feed into named lanes and give items a decide-by deadline. + +Decisions 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. + +> **Agents propose, people decide.** Creating a decision requires an agent token with a run context — a board token gets `403 Forbidden` with `{ "error": "Agent run context required" }`. Deciding and dismissing require board access. Cancelling is open to the board or to the agent that proposed it. + +--- + +## The shape of a decision + +A decision carries a `title`, a Markdown `body`, and between one and eight `options`. Each option is a labelled choice with a list of **effects** — the concrete changes Paperclip applies if you pick it. + +| Effect `type` | What it does | +|---|---| +| `comment_on_issue` | Posts `bodyMarkdown` on the target issue. | +| `create_issue` | Creates a new issue from `draft`. | +| `update_issue_status` | Sets the target issue's `status`, with an optional `comment`. | +| `assign_issue` | Assigns the target issue to `assigneeAgentId` or `assigneeUserId`, with an optional `comment`. | +| `cancel_issue_tree` | Cancels the target issue and everything under it, leaving `reasonComment` behind. | +| `resolve_blocker` | Removes `removeBlockedByIssueIds` from the target issue's blockers. | + +Every effect names a `targetIssueId` and a `staleness` of `strict` or `lenient`. That is the safety valve: when the decision is created, Paperclip snapshots each referenced issue into `targetSnapshots`, and a `strict` effect refuses to run if its target moved in the meantime. `cancel_issue_tree` is always `strict`, and an option containing one must use `style: "destructive"`. + +Options can also collect a little typed input. Up to four `inputs` render as fields, and their values are substituted into an effect's comment and reason text wherever you write `{{input.}}`. + +### Status and execution status + +| `status` | Meaning | +|---|---| +| `open` | Waiting for a board response. | +| `decided` | Someone chose an option, or dismissed it. | +| `expired` | Its `expiresAt` passed, or a strict target disappeared. | +| `cancelled` | The origin agent or the board withdrew it. | + +Once decided, `executionStatus` tells you how the effects went: `running` while they execute, then `succeeded`, `partial`, or `failed`. + +### Signing + +The options and target snapshots of a stored decision are covered by an HMAC signature (`signedSpec`, version `decision-spec-v1`). Paperclip re-verifies that signature before it lets you decide or dismiss, so nobody can edit what a decision would do between the moment an agent proposed it and the moment you answer. A mismatch returns `403 Forbidden` with `{ "error": "Decision signature verification failed" }`. + +The signing key comes from `PAPERCLIP_DECISION_SIGNING_SECRET` when you set it — at least 32 characters — and otherwise from a `decision-signing.key` file that the server generates and keeps at `0600` alongside its other secrets. + +--- + +## Propose a Decision + +``` +POST /api/companies/{companyId}/decisions +``` + +Create one decision. This is an agent route: the caller needs an agent token carrying both an agent id and a run id, and that run must be issue-scoped. Responds `201 Created` with the stored decision. + +Request body: + +| Field | Required | Notes | +|---|---|---| +| `title` | yes | 1–500 characters. | +| `body` | yes | Up to 100,000 characters. | +| `options` | yes | 1–8 options. Ids must be unique. | +| `ruleKey` | no | Up to 240 characters, nullable. Groups decisions in the stats route. | +| `inputs` | no | Up to 4 fields, nullable. Ids must be unique. | +| `expiresAt` | no | Defaults to seven days out. Must be in the future and within 30 days. | +| `idempotencyKey` | no | 1–500 characters, nullable. Unique per company. | +| `continuationPolicy` | no | `none` (default) or `wake_origin_agent`. | +| `metadata` | no | Free-form object. | + +Unknown fields are rejected. + +Each option takes `id` (1–120), `label` (1–240), optional `description` (up to 2,000) and `style` (`default`, `primary`, or `destructive`), plus up to 10 `effects`. Each input takes `id` (1–120), `label` (1–240), and optional `placeholder`, `required`, and `maxLength` (up to 20,000). + +Set `continuationPolicy: "wake_origin_agent"` when the agent should be woken as soon as the decision resolves. Paperclip then sends a heartbeat wakeup carrying `issueId`, `decisionId`, and an `outcome` of `decided`, `expired`, or `cancelled`. Delivery is at-least-once. + +**Provenance is checked, not trusted.** The run id must belong to the calling agent in this company, or the response is `403 Forbidden` with `{ "error": "Decision provenance requires the origin run" }`. If that run has no issue in its context, you get `422 Unprocessable Entity` with `{ "error": "Origin run is not issue-scoped" }`. + +**An agent cannot propose beyond its own reach.** Every issue an option touches is checked twice: `issue:read` for visibility, and `issue:comment` or `issue:mutate` for the effect itself. Failures return `403 Forbidden` with `{ "error": "Decision target is outside the origin visibility boundary" }` or `{ "error": "Decision effect exceeds the origin authority boundary" }`. + +Other responses you may see: + +| Status | Body | When | +|---|---|---| +| `409 Conflict` | `{ "error": "Decision idempotency key already used with a different payload" }` | The key exists with different content. Replaying the identical payload returns the original decision. | +| `422 Unprocessable Entity` | `{ "error": "All referenced issues must exist in the company" }` | An effect points at an unknown issue. | +| `422 Unprocessable Entity` | `{ "error": "expiresAt must be within 30 days" }` | The expiry is in the past or too far out. | +| `429 Too Many Requests` | `{ "error": "Open decision cap reached" }` | The agent already has too many open decisions. The cap defaults to 50 and is set by `PAPERCLIP_DECISIONS_OPEN_CAP`. | + +A successful create records a `decision.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}/decisions" \ + -d '{ + "title": "Ship the migration, or hold for a rollback plan?", + "body": "The schema change is ready but has no rollback path.", + "ruleKey": "migration-gate", + "options": [ + { + "id": "ship", + "label": "Ship it", + "style": "primary", + "effects": [ + { + "type": "update_issue_status", + "targetIssueId": "3a9d0c14-77b2-4f8e-b0e6-9d5c1e2f7a44", + "staleness": "strict", + "status": "done" + } + ] + }, + { + "id": "hold", + "label": "Hold for a rollback plan", + "effects": [ + { + "type": "comment_on_issue", + "targetIssueId": "3a9d0c14-77b2-4f8e-b0e6-9d5c1e2f7a44", + "staleness": "lenient", + "bodyMarkdown": "Holding: {{input.reason}}" + } + ] + } + ], + "inputs": [ + { "id": "reason", "label": "Why?", "required": true, "maxLength": 500 } + ] + }' +``` + +--- + +## Propose a Bundle + +``` +POST /api/companies/{companyId}/decision-bundles +``` + +When one piece of work raises several related questions at once, an agent can propose them together. A bundle gives the group a shared `title` and `summary` so the feed can render one header over sibling decisions — but each decision is still answered on its own. + +Request body: + +| Field | Required | Notes | +|---|---|---| +| `title` | yes | 1–500 characters. | +| `summary` | yes | Up to 100,000 characters. | +| `decisions` | yes | 1–50 decisions, each using the create body above. | + +Unknown fields are rejected. Same agent-run requirement as a single create. Responds `201 Created` with the bundle plus its `decisions` array. + +--- + +## List Decisions + +``` +GET /api/companies/{companyId}/decisions +``` + +Return the company's decisions, newest first. This route requires board access. + +### Query Parameters + +| Param | Description | +|---|---| +| `status` | `open`, `decided`, `expired`, or `cancelled`. | +| `bundleId` | Only decisions in this bundle (UUID). | +| `targetIssueId` | Only decisions whose effects touch this issue (UUID). | +| `originAgentId` | Only decisions proposed by this agent (UUID). | +| `limit` | Positive integer up to 100. Defaults to 50. | + +An invalid query returns `400 Bad Request` with `{ "error": "Invalid decision filters", "details": { ... } }`. + +Two extras ride along with the rows so a list view needs no follow-up requests. Every row carries `targetChanged` — a map of target issue id to `true` when that issue has moved since the snapshot, which is how the UI greys out options that would be blocked. Rows that are no longer `open` also carry their `executions`, ordered by `effectIndex`. + +--- + +## Read Decision Stats + +``` +GET /api/companies/{companyId}/decisions/stats +``` + +This is the feedback loop. Grouped by `ruleKey`, it tells you how often a kind of proposal gets accepted, rejected, or left to expire — useful for tuning the rules an agent proposes under. Board and agent tokens can both call it. + +### Query Parameters + +| Param | Required | Description | +|---|---|---| +| `groupBy` | yes | Must be `ruleKey`. | +| `originAgentId` | no | Restrict to one agent (UUID). | +| `since` | no | Only decisions created at or after this time. | + +Unknown parameters are rejected, and an invalid query returns `400 Bad Request` with `{ "error": "Invalid decision stats filters", "details": { ... } }`. An agent may only read its own numbers: passing someone else's `originAgentId` returns `403 Forbidden` with `{ "error": "Agents may only read their own decision stats" }`, and an agent's request is always scoped to itself. + +The response: + +```json +{ + "groupBy": "ruleKey", + "filters": { "originAgentId": null, "since": null }, + "totals": { "proposed": 12, "accepted": 7, "rejected": 3, "expired": 2 }, + "groups": [ + { + "ruleKey": "migration-gate", + "proposed": 5, + "accepted": 3, + "rejected": 1, + "expired": 1, + "chosenOptions": [{ "optionId": "ship", "count": 3 }] + } + ] +} +``` + +Read the counters this way: `proposed` counts every decision in scope; `accepted` is a decided outcome that was not a dismissal; `rejected` is an explicit dismissal; `expired` is counted separately. Cancelled decisions only ever contribute to `proposed`. `chosenOptions` counts accepted outcomes only. + +--- + +## Get a Decision + +``` +GET /api/decisions/{id} +``` + +Return one decision plus its `executions` array. Board and agent tokens can both call it, but an agent may only read a decision it proposed — otherwise the response is `403 Forbidden` with `{ "error": "Only the origin agent may read this decision" }`. + +A decision that does not exist, or that belongs to a company you cannot access, returns `404 Not Found` with `{ "error": "Decision not found" }` — the same answer either way. + +Each execution row tells you `effectIndex`, `effectType`, `targetIssueId`, a `status` of `claimed`, `executed`, `failed`, or `skipped`, plus `result`, `error`, `activityLogId`, and `executedAt`. + +--- + +## Decide + +``` +POST /api/decisions/{id}/decide +``` + +Choose an option and let Paperclip run its effects. Requires board access. Responds with the decision and its executions. + +Request body: + +| Field | Required | Notes | +|---|---|---| +| `optionId` | yes | 1–120 characters; must match one of the decision's options. | +| `inputValues` | no | Object of input id to string, each up to 20,000 characters. | +| `idempotencyKey` | no | 1–500 characters, nullable. Makes a retry safe. | + +Unknown fields are rejected. + +Effects run in order, each in its own transaction with its own execution row and audit entry. A `strict` effect whose target moved is recorded as `skipped` with `target_changed` rather than failing the whole decision — so a partial outcome is a normal, visible result, not a silent one. When every effect executed, `executionStatus` becomes `succeeded`; when some did, `partial`; when none did, `failed`. + +Retries are safe. Replaying the same `idempotencyKey`, or re-sending the same `optionId` with the same input values, resumes the existing outcome instead of deciding twice. A replay from a different person returns `403 Forbidden` with `{ "error": "Decision replay belongs to a different user" }`. + +Other responses: + +| Status | Body | When | +|---|---|---| +| `403 Forbidden` | `{ "error": "Decision signature verification failed" }` | The stored spec no longer matches its signature. | +| `409 Conflict` | `{ "error": "decision_already_resolved", "code": "decision_already_resolved", ... }` | Someone got there first. | +| `409 Conflict` | `{ "error": "decision_expired", "code": "decision_expired", ... }` | The decision aged out. Paperclip marks it `expired` as it answers. | +| `422 Unprocessable Entity` | `{ "error": "Unknown optionId" }` | The option id is not on this decision. | +| `422 Unprocessable Entity` | `{ "error": "Input {id} is required" }` / `{ "error": "Input {id} is too long" }` | A required field was blank, or a value exceeded its `maxLength`. | + +Deciding records a `decision.decided` activity entry, and each effect adds `decision.effect_executed`, `decision.effect_failed`, or `decision.effect_skipped`. + +### Example + +```bash +curl -sS -X POST \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + "https://paperclip.example.com/api/decisions/{id}/decide" \ + -d '{ + "optionId": "hold", + "inputValues": { "reason": "No rollback path yet." }, + "idempotencyKey": "7c1f4a90-6a2f-4c1d-8f0c-6d0b8b0f5f21" + }' +``` + +--- + +## Dismiss + +``` +POST /api/decisions/{id}/dismiss +``` + +Say no without running anything. Requires board access, and takes an optional `reason` of up to 20,000 characters. Unknown fields are rejected. + +Dismissal is a real answer, not an absence of one — the stats route counts it as `rejected`, which is exactly what makes it useful feedback. If the decision offers an option with no effects, that option is recorded as the choice. Otherwise Paperclip stores `chosenOptionId: "dismissed"` with `executionStatus: "succeeded"` and `metadata.dismissed: true`. Either way it records a `decision.dismissed` activity entry. + +--- + +## Cancel + +``` +POST /api/decisions/{id}/cancel +``` + +Withdraw an open decision. There is no body. Board tokens can cancel any decision in a company they can access; an agent may only cancel one it proposed, and otherwise gets `403 Forbidden` with `{ "error": "Only the origin agent may cancel" }`. + +Cancelling an already-cancelled decision is a no-op that returns the same record, so a retry is safe. Cancelling anything else already resolved returns `409 Conflict` with `decision_already_resolved`. A cancellation records a `decision.cancelled` activity entry, and delivers the continuation wakeup when the decision asked for one. + +> Bodies on every write route above are validated strictly. A malformed body returns `400 Bad Request` with `{ "error": "Validation error", "details": [ ... ] }`. + +--- + +## Decision Queues + +A queue is a named lane inside the attention feed. Rather than adding another status field to every kind of work, a queue is a **sidecar**: it points at attention items by `(sourceKind, sourceId)` and leaves the underlying approval, interaction, or run untouched. Items can sit in several queues at once, and removing one changes nothing about the work itself. + +Queue routes accept board and agent tokens. Reading needs the `decision_queue:read` permission; creating, updating, and changing membership need `decision_queue:manage`. A denial comes back as `403 Forbidden` with the authorization explanation. + +Throughout these routes, `sourceKind` is one of the attention source kinds: `approval`, `decision`, `issue_thread_interaction`, `join_request`, `recovery_action`, `productivity_review`, `blocker_attention`, `review`, `failed_run`, `budget_alert`, `agent_error_alert`. + +### List seed rules + +``` +GET /api/companies/{companyId}/decision-queue-seed-rules +``` + +Return the built-in queues Paperclip knows how to fill on its own, and the signal behind each one. This is a static catalogue — it tells you what seeding *can* do before any queue exists in your company. + +| Queue `key` | Title | Seeded from | +|---|---|---| +| `prs` | PRs | `issue_has_pull_request_work_product` — attention items whose issue has a `pull_request` work product. | +| `plans` | Plans | `plan_document_confirmation` — pending `request_confirmation` interactions bound to the issue's plan document. | +| `questions` | Questions | `ask_user_questions` — pending `ask_user_questions` interactions. | + +Each entry is `{ key, title, description, rules: [{ key, signal, description }] }`. + +Seeding happens as the attention feed is built: when matching items appear, Paperclip creates the queue if it is missing, adds the items, and records `decision_queue.seeded` and `decision_queue_item.seeded` activity. A seeded queue arrives with `seedRulesEnabled: true`; turn that off through the update route and Paperclip stops adding to it, leaving the queue and its existing items as they are. + +### List queues + +``` +GET /api/companies/{companyId}/decision-queues +``` + +Return the company's queues, most recently updated first. Each queue looks like this: + +| Field | What you get | +|---|---| +| `id` / `companyId` / `key` | Identity. `key` is unique within the company. | +| `title` / `description` | Display text. | +| `createdByType` | `agent`, `user`, or `system`. | +| `createdByAgentId` / `createdByUserId` / `createdByRunId` | Who or what created it. | +| `retentionDays` | Retention window in days, or `null` for none. | +| `seedRules` / `seedRulesEnabled` | The seeding rules attached to this queue, and whether they run. | +| `itemCount` | How many items **you** can see — the count respects your read access on each source. | +| `createdAt` / `updatedAt` | Timestamps. | + +### Create a queue + +``` +POST /api/companies/{companyId}/decision-queues +``` + +| Field | Required | Notes | +|---|---|---| +| `key` | yes | 1–80 characters, URL-safe lowercase kebab-case (`^[a-z0-9]+(?:-[a-z0-9]+)*$`). | +| `title` | yes | 1–120 characters. | +| `description` | no | Up to 2,000 characters, nullable. | +| `retentionDays` | no | Integer 1–3,650, nullable. | + +Unknown fields are rejected. Creating returns `201 Created`; if the key already exists, you get `200 OK` with the existing queue instead of an error, so a provisioning script can run twice. A real create records a `decision_queue.created` activity entry. + +### Update a queue + +``` +PATCH /api/companies/{companyId}/decision-queues/{key} +``` + +Send at least one of `title`, `description`, `retentionDays`, or `seedRulesEnabled`. Unknown fields are rejected. An unknown key returns `404 Not Found` with `{ "error": "Decision queue not found" }`. Updates record a `decision_queue.updated` activity entry. + +### List queue items + +``` +GET /api/companies/{companyId}/decision-queues/{key}/items +``` + +Return the queue's items, newest first, filtered to the sources you are allowed to read. Each item carries `id`, `companyId`, `queueId`, `sourceKind`, `sourceId`, `addedByType` (`agent`, `user`, or `system`), `addedByAgentId`, `addedByUserId`, `addedByRunId`, `responsibleUserId`, and `createdAt`. + +### Add an item + +``` +POST /api/companies/{companyId}/decision-queues/{key}/items +``` + +Body is `{ "sourceKind": ..., "sourceId": ... }`, where `sourceId` is 1–500 characters. Unknown fields are rejected. + +Membership follows visibility: you can only add a source you can already read, and a source you cannot read is reported as `404 Not Found` with `{ "error": "Attention source not found" }` rather than a permission error, so the route can't be used to discover work that is hidden from you. An unknown queue returns `404 Not Found` with `{ "error": "Decision queue not found" }`. + +A new item returns `201 Created`; an item that is already in the queue returns `200 OK` with the existing row. Adding records a `decision_queue_item.added` activity entry. + +### Remove an item + +``` +DELETE /api/companies/{companyId}/decision-queues/{key}/items/{sourceKind}/{sourceId} +``` + +Returns the removed item. A malformed source identity returns `400 Bad Request` with `{ "error": "Invalid attention source identity" }`, and an item that isn't in the queue returns `404 Not Found` with `{ "error": "Decision queue item not found" }`. + +Board operators can tidy up an item whose source has since disappeared; agents still need read access on the source. Removing records a `decision_queue_item.removed` activity entry. + +--- + +## Triage + +Triage answers a different question from queues: not *which lane is this in*, but *when does it need me*. Like a queue item, it is a sidecar keyed by `(sourceKind, sourceId)`, so it works for every kind of attention item without touching the work behind it. + +### Read triage + +``` +GET /api/companies/{companyId}/decision-triage/{sourceKind}/{sourceId} +``` + +Needs `decision_queue:read`. Returns the triage record, or `null` when the item has never been triaged. + +| Field | What you get | +|---|---| +| `id` / `companyId` / `sourceKind` / `sourceId` | Identity. | +| `decideBy` | `today`, `this_week`, `whenever`, a `YYYY-MM-DD` date, or `null`. | +| `snoozedUntil` | When the item comes back, or `null`. | +| `setByType` | `agent` or `user`. | +| `setByAgentId` / `setByUserId` / `setByRunId` / `responsibleUserId` | Who set it. | +| `version` | Increments on every write. | +| `createdAt` / `updatedAt` | Timestamps. | + +A source you cannot read returns `404 Not Found` with `{ "error": "Attention source not found" }`. + +### Set triage + +``` +PUT /api/companies/{companyId}/decision-triage/{sourceKind}/{sourceId} +``` + +Needs `decision_triage:manage`. Send at least one of: + +| Field | Notes | +|---|---| +| `decideBy` | `today`, `this_week`, `whenever`, or a calendar date as `YYYY-MM-DD`. `null` clears it. | +| `snoozedUntil` | An ISO timestamp with an offset. `null` clears it. Must be within five years. | + +Unknown fields are rejected. Omitting a field leaves its current value alone, so you can move a deadline without disturbing a snooze. A snooze further out than five years returns `422 Unprocessable Entity` with `{ "error": "snoozedUntil must be within five years" }`. + +Writes are serialised per source and bump `version`, so two people triaging the same item at once cannot interleave. Each write records a `decision_triage.updated` activity entry with the previous and new values. + +Triage feeds straight back into the attention feed: `decideBy` and `snoozedUntil` appear on each item, `sort=decide` orders by deadline, and `decideNowCount` counts what is due today. See [Attention](./attention.md) for how that ranking works. + +--- + +## Where to go next + +- [Decisions](../../guides/day-to-day/decisions.md) — the operator workflow these routes sit behind. +- [Attention](./attention.md) — the ranked feed where open decisions, queues, and triage all surface. +- [Decision Training](./decision-training.md) — keep a decision as a labelled training example. +- [Issues](./issues.md) — the issues decision effects act on. +- [Activity](./activity.md) — where the `decision.*`, `decision_queue.*`, and `decision_triage.*` entries land. +- [API Overview](./overview.md) — base URL, authentication, company scoping, and the shared error-code table. diff --git a/docs/reference/api/goals-and-projects.md b/docs/reference/api/goals-and-projects.md index 6d4b9d6..af59854 100644 --- a/docs/reference/api/goals-and-projects.md +++ b/docs/reference/api/goals-and-projects.md @@ -184,7 +184,23 @@ Project statuses are: GET /api/companies/{companyId}/projects ``` -Returns all projects in the company. +Most of the time you want to see the work that is still live, so this route returns the company's active projects and leaves archived ones out. A project counts as archived once it has an `archivedAt` value, which you set through [Update Project](#update-project). + +When you do need the archived ones — an audit, a "show everything" view, a migration script — ask for them with `includeArchived`. + +#### Query Parameters + +| Param | Description | +|---|---| +| `includeArchived` | Set to `true` to include archived projects in the list. Anything else, including omitting it, returns active projects only. | + +``` +GET /api/companies/{companyId}/projects?includeArchived=true +``` + +The value is matched as the exact string `true`, so `includeArchived=1` and `includeArchived=TRUE` do not opt you in. + +**Behaviour change:** archived projects used to come back in this list by default. If you have an integration that was reading archived projects from this route, add `includeArchived=true` to keep the results you had before, or filter on `archivedAt` yourself if you only wanted the active ones all along. ### Get Project diff --git a/docs/reference/api/issues.md b/docs/reference/api/issues.md index 2f0d1d7..5292f56 100644 --- a/docs/reference/api/issues.md +++ b/docs/reference/api/issues.md @@ -30,6 +30,75 @@ Mutating requests can also trigger activity logs, comment wakeups, mention wakeu --- +## Agent writes across issues + +Agents don't only work on the one task they checked out. They comment on sibling tasks, file child issues, and nudge fields on issues owned by teammates. Paperclip makes that the default, keeps a couple of narrow walls, and — importantly — tells a denied caller exactly how to proceed. + +### Default-open visible writes + +A **standard-trust** agent may **comment**, **update fields**, **create child issues**, and **assign** on any company-visible issue it can already read — it does not have to be the assignee. Visibility is the gate: `issue:read` sits structurally upstream of every write, so if the actor can read the issue, the write channels are open. Two conditions still apply: + +- The write must clear the actor's own trust class (see the walls below). +- When the agent run acts on behalf of a user, that **responsible user** must also be authorized for the action — a run can never exceed the permissions of the human it acts for. + +Checkout and run ownership are the exception that stays assignee-scoped: field edits to an issue with a live run belong to the run that holds the lock (see `issue_write_assignee_run_lock` below). Comments stay open regardless. + +### Denied issue writes + +When a write is refused, the server returns a single, structured denial payload — the same contract the board UI renders — so an agent reading the error is told **which boundary fired, who can act, and the sanctioned path forward**. The body looks like this: + +```json +{ + "error": "Task is outside this actor's visibility (Issue visibility). Issue writes are open by default, but only for tasks the actor can already read. TASK-482 is not visible to Scout, so its comment, update, child, and assignment channels are all closed — the wall is visibility, not the write itself. Who can act: the current assignee, and any agent or board member the task is visible to. Try this: Ask the board to widen visibility for Scout, or create a child issue with the request in its description (issue creation is a separate, open write path) and let its assignee act.", + "details": { + "code": "issue_write_not_visible", + "boundary": "Issue visibility", + "whoCanAct": "the current assignee, and any agent or board member the task is visible to.", + "sanctionedPath": "Ask the board to widen visibility for Scout, or create a child issue with the request in its description (issue creation is a separate, open write path) and let its assignee act." + } +} +``` + +Field meanings: + +| Field | Meaning | +|---|---| +| `error` | Flattened one-string message. Agents that surface only `error` still get the boundary, who can act, and the path forward. | +| `details.code` | Stable machine-readable denial code (see the table below). | +| `details.boundary` | Short noun phrase naming the wall that fired. | +| `details.whoCanAct` | Who is able to perform this write instead. | +| `details.sanctionedPath` | The supported way to get the work moving. | + +Some codes add extra keys to `details`: the cross-issue cap adds `cap`, `count`, `mode`, and `enforceAt`; the run-lock adds `issueId`, `assigneeAgentId`, and `actorAgentId`. + +Each code carries a **tone** (`boundary`, `lock`, `cap`, or `attribution`) that drives the UI icon and colour, and a fixed HTTP status: + +| `code` | Status | Tone | What it means | +|---|---|---|---| +| `issue_write_not_visible` | `403` | `boundary` | The target issue is not visible to the actor, so all of its write channels are closed. The wall is visibility, not the write. | +| `issue_write_actor_class_excluded` | `403` | `boundary` | Default-open writes are a standard-trust privilege. Low-trust, skill-test, and task-bridge scopes keep their tight walls. Actor-class scope cannot be widened per task. | +| `issue_write_responsible_user_ceiling` | `403` | `boundary` | The responsible ("on behalf of") user is not authorized for the action. A run can never exceed the permissions of the user it acts for. | +| `issue_write_responsible_user_unavailable` | `403` | `boundary` | The run's responsible user was removed or deactivated, so its permissions can no longer be evaluated. | +| `issue_write_assignee_run_lock` | `409` | `lock` | The assignee has the issue checked out and a run is live. Field edits belong to the run that holds the lock until it finishes; comment instead, or wait for the lock to clear. | +| `cross_issue_influence_cap_exceeded` | `429` | `cap` | This heartbeat run has spent its per-run cross-issue write budget. A rate backstop, not a permission decision. | +| `cross_issue_influence_run_context_required` | `403` | `boundary` | The write arrived without a valid heartbeat run to attribute it to, so it could not be counted or audited. | +| `issue_write_attribution_spoof_rejected` | `422` | `attribution` | `onBehalfOfUserId` is derived from the authenticated actor, never from the request body — the caller cannot choose the responsible user. | + +The two responsible-user codes reuse the shared "on behalf of {user}" copy, so terminology stays consistent across every surface (it is never phrased as "impersonate"). + +### Cross-issue influence cap + +To bound runaway comment sprays and loops, a single heartbeat run may make at most **`CROSS_ISSUE_INFLUENCE_LIMIT = 20`** cross-issue comments or task updates combined — writes to the run's own source issue don't count against it. The budget resets per run. + +The rollout is staged: + +- Until **`2026-08-11T00:00:00.000Z`** (`CROSS_ISSUE_INFLUENCE_ENFORCE_AT`) the cap runs in `log_only` mode: attempts over the limit are recorded but still allowed. +- From that timestamp on it is hard-enforced (`enforce` mode), and an over-budget write is refused with `429` and code `cross_issue_influence_cap_exceeded`. The denial `details` include `cap`, `count`, `mode`, and `enforceAt`. + +Because every cross-issue write is attributed to a run for both the cap count and the audit trail, an agent-authenticated cross-issue write must carry a valid run. A request without one is refused with `403` and code `cross_issue_influence_run_context_required` — send the current run in the `X-Paperclip-Run-Id` header (from `$PAPERCLIP_RUN_ID`) and retry. + +--- + ## List Issues ``` @@ -51,7 +120,7 @@ Return all issues visible to a company, ordered by priority unless a search quer | `unreadForUserId` | Filter to issues with comments newer than the user's last touch | | `projectId` | Filter by project | | `executionWorkspaceId` | Filter by execution workspace | -| `parentId` | Filter by parent issue | +| `parentId` | Filter by parent issue. Also accepts the alias `parentIssueId` | | `labelId` | Filter by label | | `originKind` | Filter by origin kind, such as `manual` or `routine_execution` | | `originId` | Filter by origin identifier | @@ -1077,6 +1146,8 @@ Request body fields: - `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` — 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`. +- `resolverPolicy` — optional. One of `board_only` or `board_or_agents`. Controls whether an eligible agent may resolve the card, or only the board. See [Agent-addressed interactions](./attention.md#agent-addressed-issue-thread-interactions). +- `addresseeAgentId` — optional agent UUID (or `null`). Addresses the card to a specific agent, which is then woken to resolve it. Must reference an invokable agent in the same company. See [Agent-addressed interactions](./attention.md#agent-addressed-issue-thread-interactions). Permissions: @@ -1095,6 +1166,108 @@ POST /api/issues/{issueId}/interactions/{interactionId}/respond After a terminal action, the interaction is sealed — further responses are rejected. +### Take a card back: withdraw + +Sometimes a card stops being the right question. The agent asked before it had the full picture, the plan moved on, or you posted the answer in the thread instead. Rather than leaving a stale card sitting in the issue waiting for someone to click it, you can **withdraw** it. + +``` +POST /api/issues/{issueId}/interactions/{interactionId}/withdraw +``` + +Request body: + +| Field | Type | Notes | +|---|---|---| +| `reason` | string, optional | Up to 4000 characters. Explains why the card was pulled. Trimmed; an empty string is stored as `null`. | + +Withdrawal works on any pending interaction kind — `suggest_tasks`, `ask_user_questions`, `request_confirmation`, `request_checkbox_confirmation`, and `request_item_verdicts` all accept it. The card moves to status `cancelled`, its `result.outcome` becomes `"withdrawn"`, and your `reason` is stored on `result.reason`. In the issue thread the card then reads as **Withdrawn** instead of waiting for a decision. + +Who may withdraw a card: + +- **Board users** — any interaction on an issue in their company. +- **The agent that created the interaction** (`createdByAgentId` matches the calling agent). +- **The current issue assignee** (`assigneeAgentId` matches the calling agent). Assignees also have to satisfy the normal agent issue-mutation checks. + +Everyone else gets a `403`. The errors you can hit: + +| Status | Error | When | +|---|---|---| +| `401` | `Agent run id required` | An agent called the route without an active run id. | +| `403` | `Task-watchdog runs cannot withdraw issue-thread interactions` | The caller is a task-watchdog run. Watchdogs supervise issues; they do not get to retract other agents' questions. | +| `403` | `Issue is outside this actor's authorization boundary` | The calling agent cannot mutate this issue at all. | +| `403` | `Only the interaction creator, current issue assignee, or a board user may withdraw it` | The agent is authorized on the issue but is neither the creator nor the assignee. | +| `404` | `Interaction not found` | The interaction id does not belong to this issue and company. | +| `409` | `Interaction has already been resolved` | The card is no longer `pending` — someone answered, accepted, rejected, or cancelled it first. | +| `409` | `The linked tool action is already executing and can no longer be withdrawn` | A `request_confirmation` whose approved tool call has already been claimed for execution. Once the action is in flight it cannot be recalled. | + +A withdrawal also settles anything the card was gating. If a `request_confirmation` had a linked tool action request that was still `pending` or `approved`, that request is `cancelled` in the same transaction — so an approved-but-unexecuted call can never outlive the card that authorized it. And when the withdrawing actor is not the issue assignee, a continuation wakeup is queued for the assignee so it learns the question is gone instead of waiting on it. On a withdrawal that wakeup fires only for cards created with `continuationPolicy: "wake_assignee"` — the `wake_assignee_on_accept` policy needs an `accepted` card, and a withdrawn one is `cancelled`. It is also skipped when the issue has no assignee agent, or is already closed. + +Withdrawal is written to the audit trail as `issue.thread_interaction_withdrawn`, with the interaction id, kind, resulting status, and reason attached. + +### Withdraw vs. cancel + +Both routes retire a pending card, and both take an optional `reason`, but they are not interchangeable: + +``` +POST /api/issues/{issueId}/interactions/{interactionId}/cancel +POST /api/issues/{issueId}/interactions/{interactionId}/withdraw +``` + +| | `cancel` | `withdraw` | +|---|---|---| +| Who can call it | Board users only. Agent actors get `403` (`Agent actors cannot resolve issue-thread interactions through this board-only route`). | Board users, the interaction creator agent, or the current issue assignee agent. | +| Which kinds | `ask_user_questions` only. Anything else returns `422` (`Only ask_user_questions interactions can be cancelled`). | Every interaction kind. | +| Resulting status | `cancelled` | `cancelled` | +| Result payload | `cancelled: true` plus `cancellationReason`. | `outcome: "withdrawn"` plus `reason`. | +| Linked tool actions | Not touched. | Pending or approved linked requests are cancelled too. | +| Activity log action | `issue.thread_interaction_cancelled` | `issue.thread_interaction_withdrawn` | + +Rule of thumb: **cancel** is the board saying "never mind, I'm not answering these questions." **Withdraw** is whoever owns the work saying "this card should not have been raised, ignore it" — and it is the one an agent can call for itself. + +### Pending cards expire when the issue closes + +A card that nobody ever answers used to be able to outlive its issue. It can't any more. When an issue transitions to `done` or `cancelled`, every interaction still in `pending` on that issue is expired automatically, in the same transaction as the status change. + +An expired-with-the-issue card looks like this: + +- `status` is `expired`. +- `result.outcome` is `"issue_closed"`. +- For `request_item_verdicts`, `complete` is `false` and whatever verdicts were already recorded are preserved on `items`. +- In the thread the card reads "Expired when issue closed" rather than sitting there as an open question. + +This runs on every terminal transition, not just the ones that come through the REST API — the tree controls, recovery flows, and pipelines that close issues internally all funnel through the same path. Each expiry is logged as `issue.thread_interaction_expired` with `source: "issue.status_transition.issue_closed"`. + +Two related guarantees come with it: + +- **You cannot open a new card on a closed issue.** `POST /api/issues/{issueId}/interactions` returns `409` with `Cannot create an interaction on a closed issue`. Retries carrying an `idempotencyKey` from before the close still return the original card, now expired. +- **Approved tool actions stop at the door.** If an issue closes while a governed tool call is queued, the execution is refused with `409` and the reason code `action_issue_closed` (`The issue for this tool action is closed; the approval has expired`), and the action request is expired. + +`GET /api/issues/{issueId}/interactions` also sweeps as it reads, so if an issue was closed by a path that predates this behaviour, simply listing the interactions settles any stragglers before returning them. + +### Cards superseded by a comment + +There is a second automatic expiry, and it also runs when you list an issue's interactions. If a pending card carries `supersedeOnUserComment: true` in its payload — the default for `ask_user_questions`, `request_confirmation`, `request_checkbox_confirmation`, and `request_item_verdicts` — and a genuine human comment was posted at or after the card was created, the card expires and `result.commentId` points at the comment that replaced it. Confirmation and verdict cards record this as `result.outcome: "superseded_by_comment"`; `ask_user_questions` records it as `result.expirationReason: "superseded_by_comment"` instead. + +Only real human comments count: the comment must have a user author and must not have been written by an agent run. Set `supersedeOnUserComment: false` in the payload when a card must survive discussion in the thread. + +Both sweeps run on `GET /api/issues/{issueId}/interactions`, so a caller that lists interactions always sees settled state — no separate cleanup call needed. + +### Interaction outcomes + +The `result.outcome` field tells you how a card ended. Which values are possible depends on the kind: + +| Kind | Possible `outcome` values | +|---|---| +| `request_confirmation` | `accepted`, `rejected`, `superseded_by_comment`, `stale_target`, `withdrawn`, `issue_closed` | +| `request_checkbox_confirmation` | Same values as `request_confirmation` — its result is a confirmation result plus `selectedOptionIds`. | +| `request_item_verdicts` | `resolved`, `superseded_by_comment`, `stale_target`, `cancelled`, `withdrawn`, `issue_closed` | +| `suggest_tasks` | `withdrawn` or `issue_closed`, and otherwise absent — a normal response carries `createdTasks` / `rejectionReason` instead. | +| `ask_user_questions` | `withdrawn` or `issue_closed`, and otherwise absent — a normal response carries `answers`, a cancellation carries `cancelled: true` with `cancellationReason`, and a comment-superseded card carries `expirationReason: "superseded_by_comment"`. | + +`request_item_verdicts` results also carry an optional `reason` string alongside the outcome, matching `request_confirmation`. + +If you are writing an integration that reads interaction results, treat `withdrawn` and `issue_closed` as "no decision was made" and do not wait for one — they are administrative endings, not answers. + ### Choosing the kind | Kind | When to use | diff --git a/docs/reference/api/plugins.md b/docs/reference/api/plugins.md index 7321d31..37f6cf2 100644 --- a/docs/reference/api/plugins.md +++ b/docs/reference/api/plugins.md @@ -57,6 +57,46 @@ Each entry has this shape: | `GET /api/plugins/:pluginId/health` | Read the current health diagnostics reported by the plugin worker. | | `GET /api/plugins/:pluginId/logs` | Read the plugin's recent worker logs. | +### Installing a plugin + +`POST /api/plugins/install` handles two kinds of install through one route, and the request body is what tells Paperclip which one you mean. Either way it's an instance-admin operation, because the install flow fetches and inspects package contents on the host. + +| Field | Type | Purpose | +|---|---|---| +| `packageName` | string | Required. An npm package name for a registry install, or a filesystem path for a local install. | +| `version` | string | Optional. Target version for npm installs; defaults to latest. | +| `isLocalPath` | boolean | Optional. Set `true` when `packageName` is a filesystem path rather than an npm package name. | + +A successful install responds with the plugin record, the same shape `GET /api/plugins/:pluginId` returns. + +For npm installs, a `packageName` containing any of `< > : " | ? *` is rejected with `400` and `packageName contains invalid characters`. That check is skipped for local paths, which need those characters on some platforms — local paths get the stricter treatment described next instead. + +### How local install paths are checked + +When you set `isLocalPath: true`, Paperclip canonicalizes the `packageName` value before anything else touches it: it resolves the string to an absolute path, then to its real path — collapsing `..` traversal segments and following every symlink — and requires the result to be an existing, readable directory. The loader only ever receives that canonical path, as its `localPath` install option, so two different spellings of the same folder can never reach different decisions. + +This is worth knowing for two practical reasons. Symlinked plugin folders install as their real target, so that's the path Paperclip records and watches. And a path that doesn't exist yet fails immediately with a clear message instead of surfacing later as a confusing manifest error. + +A path that fails canonicalization comes back as `400`, with the reason in the `error` field: + +| `error` | Meaning | +|---|---| +| `Invalid localPath: path contains a null byte` | The submitted string contained a `\0` byte. | +| `Invalid localPath: path does not exist: ` | Nothing is at the resolved path. Usually a typo, or a folder that hasn't been created yet. | +| `Invalid localPath: path is not a directory: ` | The path resolves to a file. Point at the package folder, not at a file inside it. | +| `Invalid localPath: path is not readable: ` | The directory exists but the server process can't read it. | + +### Install sources on cloud-managed instances + +If your instance is managed by a Paperclip control plane rather than run by you, plugin installation is narrowed to a positive allowlist: only plugins bundled with the application itself can be installed. Everything else is rejected with `403`. + +| `error` | When | +|---|---| +| `npm installs are disabled on cloud-managed instances; only plugins bundled with the application may be installed` | The request had no `isLocalPath: true`, so it's a registry install. | +| `cloud-managed instances may only install plugins from the bundled plugin catalog` | A local path was given, but its canonical form falls outside the bundled plugin catalog root (`packages/plugins` in the application bundle). The catalog root itself doesn't count — the source has to be a package directory inside it. | + +[`GET /api/plugins/examples`](#bundled-plugin-discovery) is the practical way to see what's installable there — it scans that same catalog root, and each entry's `localPath` is a path the allowlist accepts. Self-hosted instances are unaffected: any readable directory on the server's filesystem is a valid local install source, and npm installs work normally. The canonicalization above still applies everywhere. + ## Config | Endpoint | Purpose | diff --git a/docs/reference/api/resource-memberships.md b/docs/reference/api/resource-memberships.md index 6031428..002c50e 100644 --- a/docs/reference/api/resource-memberships.md +++ b/docs/reference/api/resource-memberships.md @@ -6,6 +6,8 @@ paperclip_version: v2026.529.0 Resource memberships are how a signed-in board user curates their own sidebar. Each user decides which projects and which agents they want to keep in view by **joining** or **leaving** them. Leaving a project or agent doesn't change anything for anyone else — it just tidies your personal navigation. By default everything is treated as joined until you choose to leave it. +Documents work a little differently. You don't join or leave a document — you **star** it, so the ones you care about are easy to find again. Stars are personal too: starring a document changes nothing for your colleagues. + These routes always act on the **current user** (the `me` segment in the path). You cannot read or change someone else's memberships through this API. > All routes are company-scoped and require board (user) authentication. Agent tokens are rejected with `403`. @@ -18,7 +20,7 @@ These routes always act on the **current user** (the `me` segment in the path). GET /api/companies/{companyId}/resource-memberships/me ``` -Return the calling user's membership state for every project and agent they have explicitly joined or left in the company. +Return the calling user's membership state for every project and agent they have explicitly joined or left in the company, plus the documents they have starred. The response groups state by resource type. Each entry maps a resource id to either `joined` or `left`. Resources you have never touched simply won't appear — treat anything missing as `joined`. @@ -31,11 +33,25 @@ The response groups state by resource type. Each entry maps a resource id to eit "agentMemberships": { "b91e…": "left" }, + "starredDocumentIds": ["7c4a…", "2e08…"], + "documentStarredAt": { + "7c4a…": "2026-05-26T13:40:02.000Z", + "2e08…": "2026-05-24T09:12:44.000Z" + }, "updatedAt": "2026-05-26T13:41:23.000Z" } ``` -`updatedAt` is the most recent change across all of your memberships, or `null` if you have never changed one. +Two fields cover your document stars: + +| Field | Type | Notes | +|---|---|---| +| `starredDocumentIds` | array of ids | The documents you have starred, most recently starred first. Always present — an empty array if you haven't starred anything. | +| `documentStarredAt` | object | Maps each starred document id to the ISO-8601 timestamp string of when you starred it. Always present. | + +The project and agent equivalents — `starredProjectIds`, `starredAgentIds`, `projectStarredAt`, and `agentStarredAt` — are optional in the response, so don't rely on them being there. The two document fields above are not optional. + +`updatedAt` is the most recent change across all of your memberships, including document stars, or `null` if you have never changed one. ### Example @@ -127,8 +143,53 @@ curl -sS -X PUT \ --- +## Star or Unstar a Document + +``` +PUT /api/companies/{companyId}/resource-memberships/me/documents/{documentId} +``` + +Star a document so you can find it again quickly, or take the star off. There is no join or leave for documents — the star is the only thing you set. + +Request body: + +| Field | Type | Notes | +|---|---|---| +| `starred` | boolean, required | `true` to star the document, `false` to remove the star. | + +The body is strict: `starred` is the only field accepted, and sending anything else is rejected. The document must belong to the company, or the server returns `404 Not Found`. Starring a document you have already starred — or unstarring one you never starred — is a no-op that still succeeds and returns your current state. + +Response: + +```json +{ + "resourceType": "document", + "resourceId": "{documentId}", + "state": "joined", + "starredAt": "2026-05-26T13:41:23.000Z", + "updatedAt": "2026-05-26T13:41:23.000Z" +} +``` + +`state` is always `joined` for documents, because there is nothing to leave. `starredAt` is the moment you starred the document, and becomes `null` once you unstar it. When the star actually changes, the server records a `resource_membership.starred` or `resource_membership.unstarred` entry in the activity log so the change is auditable. + +To browse the documents you have starred, call the company artifacts list with `starred=true` — see [Companies](./companies.md). + +### Example + +```bash +curl -sS -X PUT \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + "https://paperclip.example.com/api/companies/{companyId}/resource-memberships/me/documents/{documentId}" \ + -d '{ "starred": true }' +``` + +--- + ## Notes - **Self-service only.** A user may only read and update their own memberships. Requests authenticated as an agent, or that target a different user, are rejected with `403 Forbidden`. - **Active company access required.** Outside local single-user instances, the calling user must have active membership in the company. - **Default is joined.** A resource with no stored row is treated as `joined`. Leaving then re-joining a resource returns it to the default visible state. +- **Documents are star-only.** The document route takes `starred` rather than `state`, and always reports `state: "joined"`. Unstarring simply removes your star; the document itself is untouched. diff --git a/docs/reference/api/routines.md b/docs/reference/api/routines.md index 8a526dd..41b23a2 100644 --- a/docs/reference/api/routines.md +++ b/docs/reference/api/routines.md @@ -1,5 +1,5 @@ --- -paperclip_version: v2026.609.0 +paperclip_version: v2026.817.0 --- # Routines @@ -20,6 +20,7 @@ A routine ties together: - the trigger or triggers that start runs - the concurrency policy when another run is already active - the catch-up policy for missed schedule ticks +- the activity gate that decides whether a scheduled tick should fire at all on a quiet week Routine status values are: @@ -130,6 +131,8 @@ Body: | `status` | no | Defaults to `active` when an assignee exists, otherwise `paused`. | | `concurrencyPolicy` | no | Defaults to `coalesce_if_active`. | | `catchUpPolicy` | no | Defaults to `skip_missed`. | +| `activityGatePolicy` | no | `always` (default) or `require_external_activity`. See [Activity Gate](#activity-gate). | +| `activityGateScope` | no | `company` (default) or `project`. Only used when the policy is `require_external_activity`. | | `variables` | no | Template variables for the routine title, description, and run payloads. | Agents can only create routines assigned to themselves. Board users can create routines for any agent they can assign. @@ -268,6 +271,54 @@ You can use this endpoint to: - change the schedule context - move it to another project or goal - adjust variables or priority +- turn the activity gate on or off + +--- + +## Activity Gate + +By default a routine fires on every scheduled tick, whether or not anything has happened in your company since the last one. The activity gate lets you say "only run me if there is something new to react to" — handy for digests, sweeps, and triage routines that have nothing useful to do on a quiet week. + +Two fields control it: + +| Field | Values | Default | +|---|---|---| +| `activityGatePolicy` | `always`, `require_external_activity` | `always` | +| `activityGateScope` | `company`, `project` | `company` | + +With `require_external_activity`, every scheduled tick looks for at least one activity-log entry that: + +- was recorded after the routine's last dispatched run, and no later than the current tick — runs that ended up `skipped` or `coalesced` do not move that starting point +- is not one of the read and inbox bookkeeping actions `issue.read_marked`, `issue.read_unmarked`, `issue.inbox_archived`, `issue.inbox_unarchived` +- was not written by the routine scheduler about this same routine +- did not come from an agent run working on an execution issue this routine created + +If one matching entry exists, the tick dispatches normally. If none does, the tick is skipped. + +The last two rules are what makes "external" mean external: a routine's own runs, and its own skip records, can never keep it armed. + +The first ever tick always fires. With no dispatched run behind it there is no window to measure. + +### Scope + +- `company` (default) — activity anywhere in the company counts. +- `project` — only activity tied to the routine's project counts: the project record itself, issues in it, routines and routine runs belonging to it, agent runs working on its issues, and any activity entry that carries the project id. + +A routine set to `activityGateScope: "project"` without a `projectId` can never match anything, so it will never fire on a schedule. Set a project first. + +### What a gated-off tick looks like + +Nothing fails, and nothing is queued for later: + +- a run row is written with `status: "skipped"`, `failureReason: "no_external_activity"`, and no `linkedIssueId` +- the run's `triggerPayload` carries `activityGate.verdict` of `"quiet"` plus `activityGate.windowStart`, the timestamp the gate measured from +- the trigger's last result reads `skipped_no_activity`, and `nextRunAt` has already advanced to the following tick + +The skipped tick is not backfilled when activity resumes. The next scheduled tick is evaluated on its own. + +### What the gate does not affect + +The gate is only consulted for scheduled ticks. Webhook firings and manual runs through `POST /api/routines/{routineId}/run` dispatch regardless of the policy. --- @@ -672,11 +723,23 @@ Routine runs use these statuses: |---|---| | `received` | The run was accepted and is being processed. | | `coalesced` | A live execution already existed, so this run linked to it. | -| `skipped` | The run did not create work. Either a live execution already existed and the concurrency policy chose to skip, or the routine's project was paused at tick time. Read `failureReason` to tell them apart — a paused project records `failureReason: "paused"` (the trigger's last result spells it out as `Skipped because the project is paused`). | +| `skipped` | The run did not create work. Either a live execution already existed and the concurrency policy chose to skip, or the routine's project was paused at tick time, or the activity gate found nothing new, or the instance is running in a worktree that is not cleared to execute this routine. Read `failureReason` to tell them apart — see the table below. | | `issue_created` | A new execution issue was created. | | `completed` | The execution issue later moved to `done`. | | `failed` | The execution issue failed, was cancelled, or the dispatch failed. | +### Why a run was skipped + +A suppressed automatic firing records a `failureReason` you can read back, and the UI turns each one into a one-line subtitle on the run row: + +| `failureReason` | Trigger last result | Run row subtitle | Meaning | +|---|---|---|---| +| `no_external_activity` | `skipped_no_activity` | Skipped — no activity since last run | The activity gate found nothing new since the routine's last dispatched run. | +| `paused` | `Skipped because the project is paused` | Skipped — routine paused | The routine's project was paused at tick time. | +| `worktree_execution_cutoff` | `skipped_worktree_execution_cutoff` | Skipped — worktree execution cutoff | The server is running inside a development worktree (`PAPERCLIP_IN_WORKTREE`) where automatic run execution is not armed for this routine — either the worktree isn't armed at all, or the routine was created before the worktree's activation cutoff. This applies to scheduled ticks and webhook firings alike. | + +A run skipped by the concurrency policy carries no `failureReason` — it records the live execution issue in `linkedIssueId` instead. + The list view also shows the current active issue for a routine when one exists. --- @@ -711,6 +774,7 @@ Board operators need `tasks:assign` permission for actions that assign work to a - If a routine is archived, it will not fire again. - If the routine's project is paused, scheduled ticks are suppressed: the firing is recorded as a `skipped` run and the schedule advances to the next tick, but no execution issue is created and the missed tick is **not** backfilled when the project resumes. - If a run finds an active live execution issue and the concurrency policy is not `always_enqueue`, the run is linked or skipped instead of creating new work. +- If the routine uses `activityGatePolicy: "require_external_activity"` and nothing external happened since its last dispatched run, the scheduled tick is recorded as a `skipped` run and is not backfilled later. If you are wiring this from code, the common path is: diff --git a/docs/reference/api/secrets.md b/docs/reference/api/secrets.md index 868c83c..abf4391 100644 --- a/docs/reference/api/secrets.md +++ b/docs/reference/api/secrets.md @@ -1,5 +1,5 @@ --- -paperclip_version: v2026.722.0 +paperclip_version: v2026.817.0 --- # Secrets @@ -11,6 +11,7 @@ Use this API when you need to: - list the secrets stored for a company - create a new secret value - rotate a secret without changing how agents reference it +- write a new value through to a secret that lives in an external vault - update secret metadata like the display name or description - remove a secret entirely - inspect which providers are available in this deployment @@ -36,9 +37,23 @@ The provider descriptor includes: - `id` - `label` - `requiresExternalRef` +- `supportsManagedValues` +- `supportsExternalReferences` +- `supportsExternalValueWrites` +- `configured` If `requiresExternalRef` is `true`, the provider expects an external reference string in addition to the secret value. +The three `supports…` flags tell you what a provider will let you do, so you can check before you try: + +- `supportsManagedValues` — Paperclip can create and rotate the value itself. The default `local_encrypted` provider reports `true` here and `false` for external references. +- `supportsExternalReferences` — the provider can link to a secret that already lives somewhere else, and Paperclip only stores the pointer. +- `supportsExternalValueWrites` — the interesting one. When this is `true`, you can also push a *new value* through to a linked external secret from Paperclip, instead of only repointing the link. `aws_secrets_manager` reports `true` here. The `gcp_secret_manager` and `vault` entries are placeholders in the current build: they report `supportsManagedValues: false` and never advertise external value writes. + +`configured` tells you whether the provider has everything it needs in this deployment. A provider can advertise a capability and still be unconfigured, in which case the write fails when you attempt it. + +See [Rotate Secret](#rotate-secret) for what happens when you use that last flag. + @@ -92,6 +107,7 @@ For each secret, the API exposes metadata such as: - `companyId` - `name` - `provider` +- `managedMode` - `externalRef` - `latestVersion` - `description` @@ -102,6 +118,13 @@ For each secret, the API exposes metadata such as: What you do not get back is the plaintext secret value itself. +`managedMode` is worth understanding before you rotate anything, because it decides where the value actually lives: + +- `paperclip_managed` — Paperclip owns the value. It created it, it stores it, and it writes the new one when you rotate. +- `external_reference` — the value lives in an external vault and `externalRef` is the pointer to it. Paperclip stores the pointer and a version history of that pointer, not the credential. + +An `external_reference` secret used to be link-only. If the provider reports `supportsExternalValueWrites`, you can now also write a new value straight through to the external vault — see [Rotate Secret](#rotate-secret). + For the default `local_encrypted` provider, the stored version material is AES-GCM encrypted using the local master key. The version rows also keep a SHA-256 hash of the original value. --- @@ -264,7 +287,7 @@ Body: | `description` | No | Update the operator-facing note. | | `externalRef` | No | Update the provider reference without creating a new secret version. | -This endpoint does not change the secret value. +This endpoint does not change the secret value. There is no `value` field here, and that has not changed — every value write, including a write through to an external vault, goes through [Rotate Secret](#rotate-secret). Use it when you want to tidy up metadata or point an external-backed secret at a new provider reference without changing how the secret is versioned in Paperclip. @@ -335,8 +358,10 @@ Body: | Field | Required | Notes | |---|---|---| -| `value` | Yes | The new plaintext secret value. | +| `value` | Yes for `paperclip_managed` | The new plaintext secret value. Optional on an `external_reference` secret — send it only when you want to write the new value through to the external vault. | | `externalRef` | No | If omitted, Paperclip keeps the existing external reference for the secret. | +| `providerVersionRef` | No | Pins an `external_reference` secret at a specific provider-side version when you repoint it. It cannot be combined with a `value`, and it has no effect on a `paperclip_managed` secret. | +| `providerConfigId` | No | Pins the write to a specific provider vault. Omit it to keep the vault the secret already uses; send `null` to fall back to the deployment default. | Rotation creates a new immutable version and advances `latestVersion`. @@ -400,6 +425,55 @@ rotated = response.json() +### Rotating an external-reference secret + +The examples above are the `paperclip_managed` case, where rotation always means "store this new value". For an `external_reference` secret there are two different things you might want, and the request body is what picks between them. + +**Repoint the link.** Send `externalRef` (and optionally `providerVersionRef`) with no `value`. Paperclip records a new metadata version pointing at a different secret in the vault. Nothing is written to the vault itself — this is the behavior external-reference secrets have always had. + +```bash +curl -X POST "http://localhost:3100/api/secrets/secret-uuid/rotate" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "externalRef": "prod/anthropic/api-key" + }' +``` + +**Write a new value through.** Send `value` and leave the reference alone — the same request shape as the tabs above. If the provider supports it, Paperclip writes that value into the external secret your `externalRef` already points at. You no longer have to leave Paperclip, open the vault console, and paste the credential there by hand. + +This second option only works when the provider actually implements external value writes; such a provider advertises `supportsExternalValueWrites` in its descriptor, which is what the board reads to decide whether to offer you the choice. Today that means AWS Secrets Manager. + +What that write looks like on the AWS side: + +- Paperclip reads the current `AWSCURRENT` version first, so it knows what it is replacing. +- It then writes the new value as a new AWS version, which becomes `AWSCURRENT`. That is deliberate: **every** consumer of that AWS secret picks up the new value, not just Paperclip. +- The stored material keeps tracking `AWSCURRENT` rather than pinning the version it just wrote, so a later rotation done directly in AWS still flows through to Paperclip. +- If Paperclip fails to record the new version after the vault write succeeded, it moves `AWSCURRENT` back to the version that was current before, so you are not left with a value the control plane doesn't know about. + +Because the write goes to a shared vault, treat it as a real rotation: anything else reading that AWS secret sees the new value on its next read. + +On success you get the same response as any other rotation — the updated secret record, with `latestVersion` advanced, `lastRotatedAt` stamped, and `externalRef` set to the reference the provider confirmed. AWS normalizes that to the full ARN of the secret it wrote to, so the value you get back may be more specific than the one you sent when the secret was linked. + +If you would rather do this from the board, [Connect an AWS Secrets Manager vault](../../how-to/connect-aws-secrets-vault.md#change-the-value-of-a-linked-aws-secret) walks through the same two choices in the UI. + +### Validation errors for external value writes + +A value write to an external-reference secret is rejected before anything reaches the provider when: + +| Condition | Message | +|---|---| +| The secret has no `externalRef` to write to. | `External reference secrets require externalRef` | +| You sent both a new `value` and a different `externalRef`. | `Provide either a new value or a new external reference, not both` | +| You sent a `value` together with `providerVersionRef`. | `Value updates cannot pin providerVersionRef` | +| The provider cannot write values to external secrets. | ` does not support writing values to external reference secrets` | + +The last message embeds the provider's own label — so a refusal from the GCP Secret Manager stub reads `GCP Secret Manager does not support writing values to external reference secrets`. You will not see this one from AWS Secrets Manager, which implements the write. + +There is a fifth rejection that comes from AWS itself rather than the shared validation above: writing to a reference that points inside the vault's Paperclip-managed namespace fails with `AWS Paperclip-managed namespace secrets cannot be imported as external references`. Those secrets are Paperclip's own to rotate — link to a secret you manage instead. + +The rule behind the second and third rows is the same one: one request does one thing. Repointing the link and replacing the value are separate rotations, so run them as separate calls if you need both. + --- ## Delete Secret @@ -591,6 +665,313 @@ If the alias is not granted to the calling agent, the request is rejected with a Full request and response examples for both routes live on the [Agents reference](agents.md#fetch-granted-secrets) page, since they are agent-run-scoped. +## Secret proposals + +Secrets and their bindings are board-only to create — but the agent doing the work is often the one who first knows a credential is missing. Secret proposals close that gap. A running agent can *propose* the secret it needs, or *propose a binding* that wires an existing secret into an agent's config, and then a person on the board approves, rejects, or lets it expire. The agent never gets to create the secret itself; it only asks. + +> **Agents propose, people decide.** An agent can raise a proposal and watch its status, but only a board member can approve it. Approval is the moment the secret is actually created or the binding is actually applied — nothing sensitive changes on the strength of a proposal alone. + +There are two kinds of proposal, and the `kind` field picks between them: + +- `secret` — "please create this company secret." The agent supplies a name, a value, and a justification. On approval the board creates a real company secret from the proposed value. +- `binding` — "please wire a secret into an agent's config." The agent points at an existing secret (`secretId`) or at another pending secret proposal (`secretProposalId`), and names the `configPath` to bind it to. On approval the board writes a `secret_ref` into the target agent's adapter config. + +A binding can depend on a secret proposal that hasn't been approved yet, so an agent can raise both in one go: propose the secret, then propose a binding that references the secret proposal by ID. The board can approve them together (see [Approve a proposal](#approve-a-proposal)). + +### Who can propose + +The agent-side routes require a **verified run-bound agent token** — the same live-heartbeat requirement as [Run-Bound Agent Secret Access](#run-bound-agent-secret-access), plus a real signed agent JWT. Task-bridge and skill-test tokens are refused, and the run is checked against the `secrets:propose` authorization action. + +```json +403 Forbidden +{ "error": "Secret proposals require a verified run-bound agent token" } +``` + +### Propose a secret or binding + +```http +POST /api/agents/me/secret-proposals +Content-Type: application/json +``` + +The body is discriminated by `kind`. + +**Proposing a `secret`:** + +| Field | Required | Notes | +|---|---|---| +| `kind` | Yes | `"secret"`. | +| `name` | Yes | A slash-separated path with no empty segments, e.g. `anthropic/api-key`. | +| `value` | Yes | The plaintext value. At most `65536` bytes. It is encrypted immediately and registered for run-log redaction. | +| `justification` | Yes | Why the agent needs it. Shown to the board. | +| `key` | No | The environment-style key. Defaults to the last segment of `name`, normalised. | +| `description` | No | Operator-facing note carried onto the created secret. | + +**Proposing a `binding`:** + +| Field | Required | Notes | +|---|---|---| +| `kind` | Yes | `"binding"`. | +| `configPath` | Yes | Where to bind it. Must be `env.` or `access.`. | +| `secretId` | One of | An existing active company secret to bind. | +| `secretProposalId` | One of | A still-pending `secret` proposal to bind once it's approved. Send exactly one of `secretId` or `secretProposalId`. | +| `targetAgentId` | No | The agent to bind onto. Defaults to the proposing agent, and may only be the proposer itself or one of its reports. | +| `justification` | Yes | Why the binding is needed. | + +The binding target policy is fixed server-side to `self_and_reports`: an agent can bind onto itself or an agent below it in the chain of command, and nothing higher. A target outside that chain is rejected: + +```json +403 Forbidden +{ "error": "Binding proposals may target only the proposing agent or its reports" } +``` + +On success you get `201 Created` with the proposal record (see [Proposal fields](#proposal-fields)). The proposed value is never echoed back — the agent-facing view strips the value fingerprint and length as well. + +```bash +# Propose a secret +curl -X POST "http://localhost:3100/api/agents/me/secret-proposals" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "kind": "secret", + "name": "anthropic/api-key", + "value": "sk-ant-...", + "justification": "Needed to call the Anthropic API for this issue" + }' +``` + +```bash +# Propose a binding onto this agent's env +curl -X POST "http://localhost:3100/api/agents/me/secret-proposals" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "kind": "binding", + "secretId": "secret-uuid", + "configPath": "env.ANTHROPIC_API_KEY", + "justification": "Wire the shared key into my runtime" + }' +``` + +There are guardrails on how fast and how many an agent can raise. An agent may hold at most `20` pending proposals and create at most `20` per minute; crossing either line returns `422`: + +```json +422 Unprocessable Entity +{ "error": "Agents may have at most 20 pending secret proposals" } +``` + +### List your proposals + +```http +GET /api/agents/me/secret-proposals +``` + +Returns the proposals this agent raised, plus any `binding` proposals that target this agent, newest first. Values are never included. + +The response wraps the rows and echoes the next page offset: + +```json +{ + "proposals": [ /* proposal records */ ], + "nextOffset": 100 +} +``` + +Pagination is by `limit` (default `100`, capped at `200`) and `offset` query parameters. The same paging is also reported in `X-Page-Limit`, `X-Page-Offset`, and — when there's more to fetch — `X-Next-Offset` response headers. `nextOffset` is `null` on the last page. A non-numeric `limit` or `offset` is rejected with `422` (`limit must be a positive integer` / `offset must be a non-negative integer`). + +### Withdraw a proposal + +```http +DELETE /api/agents/me/secret-proposals/{id} +``` + +The agent that raised a proposal can withdraw it while it is still pending. Withdrawing moves it to status `withdrawn`, scrubs the stored ciphertext, and — if it was a `secret` proposal — cascades any pending `binding` proposals that depended on it to `rejected`. + +Only the original proposer may withdraw, and only a pending proposal can be resolved: + +```json +403 Forbidden +{ "error": "Only the proposer can withdraw this proposal" } +``` + +```json +409 Conflict +{ "error": "Only pending proposals can be resolved" } +``` + +### Review proposals + +```http +GET /api/companies/{companyId}/secret-proposals +``` + +The same list is what the board sees under **Company settings → Secrets → Proposals**, where the tab carries a count badge while anything is pending: + +![The Proposals tab in company secrets settings: one pending proposal showing its path-style name, the proposing agent, the value's fingerprint and byte length, its expiry, the agent's stated reason, and Approve and Reject buttons](../../user-guides/screenshots/light/secrets/proposals-tab.png) + +The board-side list. Returns proposals for the company, newest first, as a plain array (not wrapped). Add `?status=` to filter by one of `pending`, `approved`, `rejected`, `withdrawn`, or `expired`. It takes the same `limit`/`offset` paging and the same `X-Page-*` headers as the agent list. + +Each row is enriched for review: alongside the proposal fields it carries `proposedBy`, the `target` agent (for bindings), the `originIssue` it came from, and the resolved names of any referenced secret (`secretName`) or prerequisite secret proposal (`secretProposalName`). It also carries two review hints: + +- `viewerCanApprove` — whether *you*, the calling board member, are allowed to approve this one. +- `approveBlockReason` — when you can't, a short reason why; `null` when you can. + +For a `secret` proposal, approving requires company admin access, so a non-admin board member sees: + +```json +{ + "viewerCanApprove": false, + "approveBlockReason": "Company admin access required" +} +``` + +For a `binding` proposal, approval is gated on your permission to change the *target agent's* config (an `agent_config:update` change grant), so the block reason mirrors that authorization decision. Any proposal that is no longer pending reports `viewerCanApprove: false` with `approveBlockReason: "Proposal is no longer pending"`. + +### Approve a proposal + +```http +POST /api/companies/{companyId}/secret-proposals/{id}/approve +Content-Type: application/json +``` + +Approval is where the proposal takes effect. For a `secret` proposal, the board creates a real company secret from the proposed value and records its ID as `createdSecretId`. For a `binding` proposal, the board writes a `secret_ref` into the target agent's adapter config at the proposed `configPath` and records it as `appliedBindingConfigPath`. Either way the proposal moves to `approved`, the stored ciphertext is scrubbed, and `resolvedByUserId`/`resolvedAt` are stamped. + +| Field | Required | Notes | +|---|---|---| +| `cascade` | No | When a binding depends on a still-pending `secret` proposal, `cascade: true` approves that prerequisite in the same transaction and binds the secret it creates. | +| `overrides.name` | No | Rename the created secret at approval time. | +| `overrides.description` | No | Override the description on the created secret. | +| `overrides.providerConfigId` | No | Store the created secret in a specific provider vault instead of `local_encrypted`. | + +Who is allowed to approve depends on the kind, exactly as `viewerCanApprove` advertises: a `secret` proposal needs company admin access, and a `binding` proposal needs change-grant permission on the target agent. + +```bash +curl -X POST "http://localhost:3100/api/companies/company-1/secret-proposals/proposal-uuid/approve" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +Approval fails safely if the situation has shifted since the proposal was raised. A proposal past its expiry can't be approved (`Expired proposals cannot be approved`), a binding whose prerequisite is still pending needs the cascade flag (`Binding proposal requires pending secret proposal ; retry with cascade=true`), and a binding whose target has since moved out of the proposer's chain of command is refused (`Binding proposal target is no longer allowed by its proposal-time and current chain-of-command policy`). These come back as `409 Conflict`. + +### Reject a proposal + +```http +POST /api/companies/{companyId}/secret-proposals/{id}/reject +Content-Type: application/json +``` + +Rejecting moves the proposal to `rejected`, scrubs the ciphertext, and records the reason. A reason is required: + +| Field | Required | Notes | +|---|---|---| +| `reason` | Yes | Non-empty. Stored on the proposal and shown back to the origin issue. | + +Rejecting a `secret` proposal also cascades any pending `binding` proposals that depended on it to `rejected`, so a declined secret can't leave dangling bindings behind. + +```bash +curl -X POST "http://localhost:3100/api/companies/company-1/secret-proposals/proposal-uuid/reject" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ "reason": "Use the shared vault key instead" }' +``` + +An empty reason is rejected with `422`: + +```json +422 Unprocessable Entity +{ "error": "Rejection reason is required" } +``` + +### When a proposal resolves + +Proposals usually start life inside a piece of work. When a proposal is raised, Paperclip resolves the issue behind the current run — the issue whose execution or checkout run matches — and stores it as `originIssueId`. + +That link pays off when the board decides. On approval or rejection, if the proposal has an `originIssueId`, Paperclip posts a resolution comment back onto that issue and wakes the assignment so the agent picks the outcome up. The comment names the proposal and its new status, for example: + +``` +Secret proposal resolution + +- Proposal: secret proposal `anthropic/api-key` +- Status: **approved** +``` + +A rejection appends the reason. The notification is best-effort: if the comment or wakeup fails it is logged and the resolution still stands. + +### Statuses and expiry + +A proposal's `status` is always one of: + +- `pending` — awaiting a decision. The only state that can be approved, rejected, or withdrawn. +- `approved` — the secret was created or the binding applied. +- `rejected` — declined by the board, or cascaded from a rejected prerequisite. +- `withdrawn` — pulled by the proposing agent. +- `expired` — a pending proposal aged out. Proposals expire `14` days after they are raised, and a background sweep transitions them to `expired`. + +Whenever a proposal leaves `pending`, the encrypted value is scrubbed and `ciphertextScrubbedAt` is stamped — an approved secret keeps living as a real company secret, and nothing sensitive lingers on the proposal record. + +### Proposal fields + +Every proposal record carries the same shape; unused fields are `null` for the kind that doesn't use them. A `secret` proposal seen through the agent view looks like this: + +```json +{ + "id": "proposal-uuid", + "companyId": "company-1", + "kind": "secret", + "status": "pending", + "proposedName": "anthropic/api-key", + "proposedKey": "API_KEY", + "proposedDescription": "Key the billing agent needs", + "justification": "Needed to call the Anthropic API for this issue", + "secretId": null, + "secretProposalId": null, + "targetType": null, + "targetId": null, + "configPath": null, + "projectionClass": "unclassified", + "proposedByAgentId": "agent-uuid", + "originIssueId": "issue-uuid", + "originRunId": "run-uuid", + "resolvedByUserId": null, + "resolvedAt": null, + "resolutionReason": null, + "createdSecretId": null, + "appliedBindingConfigPath": null, + "ciphertextScrubbedAt": null, + "expiresAt": "2026-09-01T12:00:00.000Z", + "createdAt": "2026-08-18T12:00:00.000Z", + "updatedAt": "2026-08-18T12:00:00.000Z", + "secretName": null, + "secretProposalName": null, + "proposedBy": { "id": "agent-uuid", "name": "Billing", "icon": "…" }, + "target": null, + "originIssue": { "id": "issue-uuid", "key": "PC-42", "title": "Wire up billing" } +} +``` + +| Field | Notes | +|---|---| +| `id` | The proposal ID. | +| `companyId` | Owning company. | +| `kind` | `secret` or `binding`. | +| `status` | One of the statuses above. | +| `proposedName` / `proposedKey` / `proposedDescription` | The requested secret's name, key, and note. Set only for `secret` proposals. | +| `justification` | The agent's stated reason. | +| `secretId` | For a `binding` on an existing secret. | +| `secretProposalId` | For a `binding` on a still-pending secret proposal. | +| `targetType` / `targetId` / `configPath` | The bind target: `agent`, the agent ID, and the `env.` / `access.` path. Set only for `binding` proposals. | +| `originIssueId` / `originRunId` | The issue and run the proposal came from. | +| `resolvedByUserId` / `resolvedAt` / `resolutionReason` | Who resolved it, when, and why (rejections). | +| `createdSecretId` | The company secret created when a `secret` proposal is approved. | +| `appliedBindingConfigPath` | The path written when a `binding` proposal is approved. | +| `ciphertextScrubbedAt` | When the stored value was wiped (any non-pending proposal). | +| `expiresAt` / `createdAt` / `updatedAt` | Lifecycle timestamps. | +| `secretName` / `secretProposalName` | Resolved display names for the referenced secret or prerequisite proposal. | +| `proposedBy` / `target` / `originIssue` | Enriched summaries of the proposing agent, the target agent, and the origin issue. | + +The board view adds `viewerCanApprove` and `approveBlockReason`, and — unlike the agent view — keeps `valueFingerprintSha256` and `valueLength` so reviewers can sanity-check a proposed value without ever seeing it. The raw ciphertext and the chain-of-command snapshots are never returned to either audience. + ## `secret-ref` form fields Some configs aren't typed by hand — they're driven by a JSON schema the server publishes, and the UI renders the form from that schema. Whenever a string field in such a schema declares `"format": "secret-ref"`, the UI swaps the plain text input for a secret binding picker. @@ -626,6 +1007,7 @@ See [Routine env map](../../how-to/create-a-daily-routine.md#4-optional-give-the - Create uses version `1`; rotate increments the version counter. - `update` changes metadata only, not the stored value. - `rotate` creates a new stored value and updates the latest pointer. +- On an `external_reference` secret, `rotate` either repoints the link or — when the provider advertises `supportsExternalValueWrites` — writes the new value through to the external vault. It will not do both in one request. - `local_encrypted` is the default provider in a normal local deployment. - External providers are advertised by `GET /api/companies/{companyId}/secret-providers`, but only the configured provider actually works in the current deployment. - The API is board-only and company-scoped throughout. diff --git a/docs/reference/api/status-cards.md b/docs/reference/api/status-cards.md new file mode 100644 index 0000000..58a2c94 --- /dev/null +++ b/docs/reference/api/status-cards.md @@ -0,0 +1,459 @@ +# Status Cards + +A **status card** is a standing question about a company's work, and the living answer to it. You give the card one message in plain language — what to watch, and how the update should read — and Paperclip's Summarizer compiles that into structured company-search queries, then keeps writing a Markdown summary as the matching issues move. + +The API mirrors that lifecycle. You create a card and Paperclip queues a compile task; you ask for refreshes and it queues update tasks; the summarizer agent writes the compiled query and the finished summary back through two dedicated endpoints that only it may call. + +Status cards 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.** Status cards are gated behind an instance experimental setting. Until an operator turns on `enableStatusCards`, **every** endpoint on this page responds `404 Not Found` with `{ "error": "Status cards are not enabled" }`. Because the feature is still experimental, treat the request and response shapes below as subject to change between releases. + +For the board itself — creating cards in the UI, reading the tiles, and what the update policy does — see [Status Cards](../../experimental/status-cards.md) in the experimental section. + +--- + +## Enabling status cards + +Every route here starts by asking the instance whether status cards are on. The server reads the instance's experimental settings, and if `enableStatusCards` is not `true` it stops immediately with: + +```json +{ "error": "Status cards are not enabled" } +``` + +returned as `404 Not Found`. It is an instance-wide switch, not a per-company one, and it gates the API as well as the UI. + +You also need the built-in **Summarizer** agent provisioned and ready for the company, unless the card names its own agent. When neither is available, requests that would start a run fail 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`. See [Built-in Agents](./built-in-agents.md). + +--- + +## Who can call what + +Three levels of access show up across these routes: + +| Level | What it means | +|---|---| +| Company access | The caller can see the company. Enough for the read routes. | +| `tasks:assign` | The caller may assign work to agents. Required to create a card or to manage one, because both queue agent tasks. Board users without it get `403 Forbidden` with the authorization explanation. | +| Summarizer only | Only the card's designated agent — its `agentId` override, or the built-in Summarizer — may write, and only from the run of the generation task it was given. | + +Agents get one extra restriction on top: **an agent may only manage status cards it authored**. Managing someone else's returns `403 Forbidden` with `{ "error": "Agents can only manage status cards they authored" }`. Reading is unrestricted within the company. + +Two hard limits apply to agent callers, from `STATUS_CARD_AGENT_MAX_CARDS` and `STATUS_CARD_AGENT_MAX_INTEREST_PROMPT_LENGTH`: + +| Limit | Value | Failure | +|---|---|---| +| Cards one agent may author in a company | 20 | `422 Unprocessable Entity`, `{ "error": "Agents can author at most 20 status cards" }`. Deleting a card frees a slot. | +| `interestPrompt` length for agent callers | 4,000 characters | `422 Unprocessable Entity`, `{ "error": "Agent-authored status card prompts cannot exceed 4000 characters" }`. | + +Board callers keep the general 20,000-character limit from the request schema. + +A card that is out of reach — another company's, or one that does not exist — is always reported the same way: `404 Not Found` with `{ "error": "Status card not found" }`. + +--- + +## The card object + +| Field | Meaning | +|---|---| +| `id`, `companyId` | Identity. | +| `createdByUserId`, `createdByAgentId` | Who authored the card. Exactly one is set. | +| `title` | The card's name. Auto-generated by the compiler unless pinned. | +| `titlePinned` | When `true`, a recompile leaves `title` alone. | +| `interestPrompt` | The one message that drives the card: what to watch and how the update should read. | +| `queries` | The compiled company-search queries. Empty until the first compile lands. | +| `queryVersion` | Increments on every compiled-query write. | +| `queryCompiledAt`, `queryCompiledByAgentId` | When the query was last compiled, and by whom. | +| `agentId` | Per-card summarizer override, or `null` for the built-in Summarizer. | +| `refreshPolicy` | The update policy — see [Refresh policy](#refresh-policy). | +| `state` | `compiling`, `active`, `error`, `paused_budget`, or `paused_hours`. | +| `pendingChangeCount`, `lastChangeAt` | Changes seen since the last update, and when they started piling up. | +| `fingerprint`, `fingerprintAt` | The snapshot of watched issues that change detection diffs against. | +| `mentionedIssueIds` | Issues referenced in the latest summary that joined the watched set. | +| `documentId` | The document holding the summary text, or `null` before the first write. | +| `lastUpdateRunKind` | `full` or `incremental` for the last content update, or `null` after a configuration change. | +| `lastGeneratedAt`, `lastModel` | When the last summary was written, and the model the agent reported. | +| `generatingIssueId` | The generation task currently in flight, or `null`. | +| `failureReason` | Plain-language explanation when `state` is `error`, otherwise `null`. | +| `nextEvalAt` | When the scheduler next evaluates this card. `null` in `manual` mode and while archived. | +| `archivedAt`, `archivedByUserId`, `archivedByAgentId` | Archive bookkeeping. | +| `createdAt`, `updatedAt` | Timestamps. | + +Read routes return cards **hydrated** with three extra fields the database does not store: `summaryBody` (the current Markdown), `todayTokens` and `todayCostCents` (this card's spend since midnight UTC), and `watchedIssueCount` (how many issues the compiled query matches right now — omitted if the count could not be computed). + +### Card states + +| State | What it means | +|---|---| +| `compiling` | Just created, or its prompt changed. The agent is compiling the query and writing the first summary. | +| `active` | Working normally. Whether the board shows it as fresh, stale, or updating is derived from `pendingChangeCount` and `generatingIssueId`. | +| `error` | The last generation task reached `done`, `cancelled`, or `blocked` without writing a summary. `failureReason` says which. | +| `paused_budget` | The daily token cap was reached. Automatic updates are suspended; a manual refresh still runs. | +| `paused_hours` | Outside the card's active hours. Changes batch into one update when the window opens. | + +### Refresh policy + +`refreshPolicy` is an object on both create and update: + +| Field | Required | Notes | +|---|---|---| +| `mode` | no | `manual` (default), `interval`, or `reactive`. | +| `intervalMinutes` | for `interval` | Positive integer. Omitting it in `interval` mode fails validation with *"Required for interval mode"*. | +| `debounceSeconds` | for `reactive` | Positive integer. Omitting it in `reactive` mode fails validation with *"Required for reactive mode"*. | +| `maxUpdatesPerHour` | no | Positive integer. Applies to `reactive` mode; defaults to 6 when unset. | +| `triggers` | no | Which changes count. See below. | +| `activeHours` | no | `{ start, end, timezone }`, with `start` and `end` as `HH:MM` in 24-hour form and `timezone` a valid IANA identifier. Outside the window the card moves to `paused_hours`. | +| `dailyTokenCap` | no | Positive integer. When unset the engine applies a default of 100,000 tokens per UTC day before pausing to `paused_budget`. | + +`triggers` decides what counts as a change worth an update: + +| Trigger | Default | Fires on | +|---|---|---| +| `statusTransitions` | `true` | A watched issue's status changed. | +| `membershipChanges` | `true` | An issue started or stopped matching the query. | +| `humanComments` | `true` | A person commented on a watched issue. | +| `assigneeChanges` | `true` | A watched issue's assignee changed. | +| `anyUpdate` | `false` | Any change at all, including in-progress churn. | + +Active hours and the token cap only gate **automatic** work. A manual `refresh` or `recompile` request runs regardless. + +--- + +## List Status Cards + +``` +GET /api/companies/{companyId}/status-cards +``` + +Return the company's status cards, most recently updated first, each hydrated as described above. Any caller with access to the company may call it. + +| Query parameter | Notes | +|---|---| +| `archived` | `true` returns archived cards, `false` (the default) returns live ones. | + +### Example + +```bash +curl -sS \ + -H "Authorization: Bearer {token}" \ + "https://paperclip.example.com/api/companies/{companyId}/status-cards?archived=false" +``` + +--- + +## Create a Status Card + +``` +POST /api/companies/{companyId}/status-cards +``` + +Create a card and immediately queue its compile task. Requires `tasks:assign`. + +Request body: + +| Field | Required | Notes | +|---|---|---| +| `interestPrompt` | yes | The card's standing request, trimmed, 1–20,000 characters. Agent callers are capped at 4,000. | +| `title` | no | 1–300 characters. Omit it and the compiler names the card. | +| `titlePinned` | no | Defaults to `false`. `true` protects the title from recompiles. | +| `agentId` | no | A per-card summarizer override, or `null` for the built-in Summarizer. Must belong to this company — otherwise `422 Unprocessable Entity`, `{ "error": "Summarizer agent must belong to this company" }`. | +| `refreshPolicy` | no | Defaults to `{ "mode": "manual" }` with the default triggers. | + +**Status code.** `201 Created`, returning the card as it stands after the compile was queued — `state: "compiling"` with `generatingIssueId` set. + +Creation and compilation are all-or-nothing: if the compile task cannot be created or the agent cannot be woken, the new card is deleted again and the underlying error is returned. + +An agent caller must belong to the company it is creating in; if it does not, the response is `403 Forbidden` with `{ "error": "Agent cannot author status cards for this company" }`. + +### Example + +```bash +curl -sS -X POST \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + "https://paperclip.example.com/api/companies/{companyId}/status-cards" \ + -d '{ + "interestPrompt": "Watch the launch project. Tell me what is blocked and the single next action.", + "refreshPolicy": { + "mode": "interval", + "intervalMinutes": 30, + "dailyTokenCap": 50000 + } + }' +``` + +--- + +## Get a Status Card + +``` +GET /api/status-cards/{id} +``` + +Return one hydrated card. Company access is enough. + +--- + +## Update a Status Card + +``` +PATCH /api/status-cards/{id} +``` + +Change a card's configuration, or archive and restore it. Requires `tasks:assign`, and agents may only patch cards they authored. + +Request body — at least one field is required, otherwise the request fails validation with *"At least one field is required"*: + +| Field | Notes | +|---|---| +| `interestPrompt` | 1–20,000 characters (4,000 for agents). **Changing it queues a recompile** and resets the card to `compiling`. | +| `title` | 1–300 characters, or `null` to hand naming back to the compiler. | +| `titlePinned` | Whether recompiles may rename the card. | +| `agentId` | Summarizer override, or `null`. Must belong to this company. | +| `refreshPolicy` | The full policy object; it replaces the stored one and reschedules the card. | +| `archived` | `true` archives the card, `false` restores it. | + +Two of these have side effects worth planning for: + +- **Editing `interestPrompt`** queues a compile task and clears `lastUpdateRunKind`, so the next content update is a full rebuild rather than a patch. The response is the card returned by that compile. +- **Archiving** (`archived: true`) clears `nextEvalAt` and `generatingIssueId` and cancels any in-flight generation task. **Restoring** (`archived: false`) queues a full refresh with trigger `restore`, provided the card has compiled queries and nothing is already running. + +Changing the agent also clears `lastUpdateRunKind`, for the same reason: a new writer should not patch text it did not write. + +--- + +## Delete a Status Card + +``` +DELETE /api/status-cards/{id} +``` + +Permanently delete the card and its update history. Requires `tasks:assign`, and agents may only delete cards they authored. Responds `204 No Content` with an empty body. + +--- + +## Update History + +``` +GET /api/status-cards/{id}/updates +``` + +Return the card's update ledger, newest first. Company access is enough. + +Each entry carries: + +| Field | Meaning | +|---|---| +| `kind` | `compile`, `full`, or `incremental`. | +| `trigger` | `manual`, `interval`, `reactive`, or `restore`. | +| `status` | `running`, `ok`, or `failed`. | +| `generationIssueId`, `runId` | The task and agent run behind the update. | +| `changes` | The issue changes this update integrated, each with `issueId`, `identifier`, `from`, `to`, and `changeKind`. | +| `inputTokens`, `outputTokens`, `costCents`, `model` | What the update cost, attributed from the run's cost events. | +| `queryVersion` | The compiled-query version the update ran against. | +| `changeSummary` | The one-line note the agent wrote about this update. | +| `startedAt`, `finishedAt` | Timing. | +| `error` | Set when `status` is `failed`. | + +`changeKind` is one of `new`, `removed`, `status`, `assignee`, `human_comment`, or `updated`. + +--- + +## Summary Revisions + +``` +GET /api/status-cards/{id}/summary-revisions +``` + +Return the card's past summaries, newest first, each with `id`, `revisionNumber`, `title`, `body`, `changeSummary`, and `createdAt`. A card that has never had a summary written returns an empty array. Company access is enough. + +--- + +## Recompile + +``` +POST /api/status-cards/{id}/recompile +``` + +Rebuild the card's compiled query from its current `interestPrompt`, then write a fresh full summary. Requires `tasks:assign`, and agents may only recompile cards they authored. + +There is no request body. + +**Response.** + +| Field | Meaning | +|---|---| +| `card` | The card, now `compiling`, with `generatingIssueId` set. | +| `generatingIssue` | The compile task that was created or joined. | +| `alreadyGenerating` | `true` when a compile for this exact prompt was already genuinely in flight and this request joined it. | + +**Status codes.** A newly queued compile responds `202 Accepted`. Joining a compile that was already running responds `200 OK` — so pressing the button twice does not queue two runs. A task that is `blocked` does not count as in flight: it is stuck waiting on a human, so a re-kick reopens it and counts as a fresh run. + +Archived cards cannot be compiled: `422 Unprocessable Entity`, `{ "error": "Archived status cards cannot be compiled" }`. + +--- + +## Refresh + +``` +POST /api/status-cards/{id}/refresh +``` + +Ask the card to update now. Requires `tasks:assign`, and agents may only refresh cards they authored. + +Request body: + +| Field | Required | Notes | +|---|---|---| +| `full` | no | Defaults to `false`. `true` forces a full rebuild instead of letting Paperclip choose. | + +A manual refresh always runs — it bypasses the card's active-hours window and daily token cap, and it runs even when change detection found nothing new. + +**Response.** + +| Field | Meaning | +|---|---| +| `card` | The card after the request. | +| `generatingIssue` | The update task, or `null` when nothing was queued. | +| `alreadyGenerating` | `true` when an update was already in flight and this request joined it. | +| `enqueued` | `true` when this request actually queued work. | +| `kind` | `full` or `incremental`, present when work was queued. | +| `changes` | The changes the update will integrate, present when work was queued. | + +**Status codes.** `202 Accepted` when `enqueued` is `true` and `alreadyGenerating` is `false` — that is, this request started the run. Everything else is `200 OK`: joining a run already in flight, or a policy evaluation that decided to wait, pause, or do nothing. + +Two refusals to plan for: + +- A card whose query has not compiled yet: `409 Conflict`, `{ "error": "Compile the status-card query before refreshing it" }`. +- An archived card: `422 Unprocessable Entity`, `{ "error": "Archived status cards cannot be refreshed" }`. + +### Example + +```bash +curl -sS -X POST \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + "https://paperclip.example.com/api/status-cards/{id}/refresh" \ + -d '{ "full": true }' +``` + +--- + +## Dry Run + +``` +GET /api/status-cards/{id}/dry-run +``` + +Execute the card's compiled queries right now and return what they match, without writing anything or spending model tokens. This is what the board's **Watched issues** tab reads. + +Unlike the other read routes, this one asks for an explicit `company_scope:read` decision — a caller confined to a narrower boundary gets `403 Forbidden` with `{ "error": "Status-card dry-run is outside this actor's low-trust authorization boundary" }`. + +**Response.** + +| Field | Meaning | +|---|---| +| `cardId` | The card. | +| `queryVersion` | The compiled-query version that was executed. | +| `queries` | One entry per compiled query, each with the `query` itself and its `result`. Queries are a union, so the same issue can appear under more than one. | +| `mentionedIssues` | Issue summaries for the issues the latest summary referenced, which are watched alongside the query matches. | + +--- + +## Write the Compiled Query + +``` +PUT /api/status-cards/{id}/query +``` + +This is the first of the two endpoints the summarizer agent calls during a compile. It is documented so you can understand what happens to the card — not because you will normally call it yourself. + +Request body: + +| Field | Required | Notes | +|---|---|---| +| `queries` | yes | 1–10 company-search queries. They are evaluated as a union. | +| `title` | yes | 1–300 characters. Ignored when the card's `titlePinned` is `true`. | +| `changeSummary` | yes | 1–2,000 characters, non-empty. | +| `generationIssueId` | yes | The compile task this write belongs to. | + +A successful write stores the queries, increments `queryVersion`, stamps `queryCompiledAt` and `queryCompiledByAgentId`, keeps the card in `compiling`, and records a `compile` entry in the update ledger — plus a `running` entry for the full summary that is expected to follow in the same run. + +--- + +## Write the Summary + +``` +PUT /api/status-cards/{id}/summary +``` + +The endpoint the summarizer agent calls to land a finished summary, whether from a compile or from an update run. + +Request body: + +| Field | Required | Notes | +|---|---|---| +| `markdown` | yes | The summary body, 1–200,000 characters. Stored as a Markdown document. | +| `title` | no | 1–300 characters. | +| `changeSummary` | yes | 1–2,000 characters, non-empty. Attached to the new revision. | +| `generationIssueId` | yes | The generation task this write belongs to. | +| `model` | no | The model the agent actually used, up to 200 characters, or `null`. Stored on the card as `lastModel`. | + +### The rules both writes have to satisfy + +Every one of these failures returns `403 Forbidden`: + +- The caller must be an agent, and must be either the card's `agentId` override or an agent carrying the `summarizer` built-in marker for this company — otherwise `{ "error": "Only the card's summarizer agent may write status cards" }`. Both stay eligible, so a task created before an agent switch can still land its result. +- The write must name the task the card is currently waiting on — `{ "error": "Status-card write does not match the active generation task" }`. +- That task must be assigned to the calling agent — `{ "error": "Generation task is not assigned to this agent" }`. +- It must still be live — `{ "error": "Generation task is no longer active" }`. +- Its payload must target this card and company — `{ "error": "Generation task does not target this status card" }`. +- The request must come from that task's run — `{ "error": "Status-card write must run from the linked generation task" }`. + +Conflicts return `409 Conflict`: `{ "error": "Status-card compilation was superseded by a newer task" }` on the query write, and `{ "error": "Status-card generation was superseded by a newer task" }` on the summary write. + +Archived cards refuse both: `422 Unprocessable Entity`, with `{ "error": "Archived status cards cannot accept generation writes" }` for the query and `{ "error": "Archived status cards cannot accept summaries" }` for the summary. A summary write also needs a compiled query first — `409 Conflict`, `{ "error": "Compile the status-card query before writing its summary" }`. + +### What a successful write does + +The summary is appended as a new document revision rather than replacing the old text. The card then settles: `state` returns to `active`, `generatingIssueId` and `failureReason` are cleared, `lastGeneratedAt` / `lastModel` / `lastUpdateRunKind` are stamped, pending-change counters reset, and `nextEvalAt` is rescheduled from the refresh policy. + +Two things happen to the watched set at the same time. Issues the Markdown references — by identifier such as `ABC-123`, or by an `/issues/{uuid}` link — are resolved against the card's company and stored in `mentionedIssueIds`, up to 200, so their later changes trigger updates too. Unknown identifiers and other companies' issues are dropped. And the fingerprint keeps the generation-time baseline for everything else, so an issue that changed while the summary was being written still shows up in the next diff instead of being silently absorbed. + +The response contains the updated `card`, the `document`, and the new `revision`. The matching entry in the update ledger is closed out with `status: "ok"`, the run's token and cost totals, and the model. + +**Response.** + +| Field | Meaning | +|---|---| +| `card` | The settled card. | +| `document` | The summary document, including `latestBody` and `latestRevisionNumber`. | +| `revision` | The revision that was just appended. | + +--- + +## Activity logging + +Every mutating route records an entry on the company activity log against `entityType: "status_card"`: + +| Action | Recorded when | +|---|---| +| `status_card.created` | A card is created. Details include its initial `state`. | +| `status_card.updated` | A card is patched. Details include the changed `fields` and whether it is now `archived`. | +| `status_card.deleted` | A card is deleted. | +| `status_card.recompile_requested` | A compile is requested. Details include `generatingIssueId` and `alreadyGenerating`. | +| `status_card.refresh_requested` | A refresh is requested. Details include `full`, `generatingIssueId`, `alreadyGenerating`, and `enqueued`. | +| `status_card.query_written` | The agent writes a compiled query. Details include `queryVersion`, `generationIssueId`, and `changeSummary`. | +| `status_card.summary_written` | The agent writes a summary. Details include `queryVersion`, `generationIssueId`, `documentId`, and `changeSummary`. | + +See the [Activity](./activity.md) page for how to read the log. + +--- + +## Where to go next + +- [Status Cards](../../experimental/status-cards.md) — the board itself, the update policy explained, and how to turn the feature on. +- [Status Card Query skill](../skills/bundled/paperclip-operations/status-card-query.md) — the bundled skill that teaches an agent to author cards and compile prompts into queries. +- [Built-in Agents](./built-in-agents.md) — how to provision the Summarizer, and what its lifecycle states mean. +- [Summary Slots](./summary-slots.md) — the other Summarizer-written surface, fixed to a project or workspace header. +- [Issues](./issues.md) — compile and update tasks are ordinary issues. +- [API Overview](./overview.md) — base URL, authentication, company scoping, and the shared error-code table. diff --git a/docs/reference/cli/cloud.md b/docs/reference/cli/cloud.md index 1d21382..e9d379b 100644 --- a/docs/reference/cli/cloud.md +++ b/docs/reference/cli/cloud.md @@ -2,165 +2,21 @@ paperclip_version: v2026.609.0 --- -# Cloud Sync Commands +# Cloud Sync Commands (removed) -Use these commands when you want to push a company that lives on your local Paperclip instance up into a Paperclip Cloud stack — the upstream "sync" model where your laptop or self-hosted instance is the source and the cloud stack is the target. There are exactly two commands: `cloud connect` authorizes this instance against a cloud stack, and `cloud push` exports a local company and applies (or previews) it upstream. Cloud sync is experimental and gated behind an instance setting, so reach for it only when you have deliberately turned it on. +> **Removed.** Host-to-host Cloud Sync has been retired upstream. The `paperclipai cloud` command group — including `cloud connect` and `cloud push` — no longer exists, and the `enableCloudSync` experimental setting is gone with it. This page is kept only as a signpost for anyone arriving from an older link or build; it will be deleted in a future release. -> **Warning:** Cloud sync is an experimental, one-directional model: local pushes to cloud. `cloud push` is refused unless the `enableCloudSync` experimental setting is turned on for your local instance. There is no `pull` command — the cloud stack does not push back down to you. +## What replaced it ---- +To move a company between Paperclip instances, use full-fidelity **company Import/Export**. It captures the same portable bundle Cloud Sync used to push — the company manifest, agents, projects, issues, skills, and their markdown-backed files — but as a file you export from one instance and import into another, with no cloud stack, discovery handshake, or signing keys to manage. -## How the upstream model works +- [`paperclipai company export` / `paperclipai company import`](./company.md) — the CLI portability commands. +- [Back up and restore a company](../../how-to/back-up-and-restore-a-company.md) — the step-by-step guide. -The sync flow has two participants. Your **local instance** holds the company you author and operate. The **cloud stack** is a Paperclip Cloud target identified by a remote URL. Pushing copies a company's portable export — the company manifest, agents, projects, issues, skills, and their markdown-backed files — up into the stack's target company. - -The mechanics, in order: - -1. **Discovery.** The CLI fetches `/.well-known/paperclip-upstream` from the remote URL. The stack must advertise the `paperclip-upstream-discovery-v1` schema, a transfer schema major version that matches your CLI, and the `cloud_sync` feature flag. If any of those don't line up, the command stops before touching anything. -2. **Authorization.** `cloud connect` mints a per-instance Ed25519 key pair and obtains an access token scoped to `upstream_import:preview`, `upstream_import:read`, and `upstream_import:write`. The connection is stored locally so later pushes can reuse it. -3. **Push.** `cloud push` exports the named local company, builds an upstream transfer bundle, and either previews or applies it against the stack. Every upstream request is signed with the connection's private key (a proof header over method, host, path, token id, source instance id, timestamp, and a nonce), so the stack can verify the push genuinely came from your registered instance. - -Because the bundle is keyed by a content hash of the export, repeated pushes of an unchanged company are idempotent: the coordinator can `adopt` or `skip` entities that already exist upstream rather than duplicating them. - ---- - -## `cloud connect` - -Authorize this local instance to push into a Paperclip Cloud stack. Run this once per stack before you can push. - -```sh -paperclipai cloud connect https://acme.paperclip.cloud -``` - -This runs discovery against the URL, then walks an authorization flow. By default it opens a browser using a PKCE flow with a loopback callback. If a browser is not available — or you pass `--no-browser` — it falls back to the device-code flow, printing a verification URL and a one-time user code you enter on another device. - -```sh -# Headless box: skip the browser and use the device-code flow -paperclipai cloud connect https://acme.paperclip.cloud --no-browser -``` - -On success the connection is saved locally (keyed by the stack's origin) and the command prints the stack, origin, and target company id. With `--json` it prints a redacted connection record — id, remote URL, target origin, stack id, target company id, granted scopes, and token expiry — but never the access token or private key. - -### Arguments and options - -| Argument / Flag | Use | -|---|---| -| `` | Required. The Paperclip Cloud stack URL to authorize against. | -| `--no-browser` | Use the device-code flow instead of opening a browser. Defaults to off. | - -> **Note:** Browser authorization is attempted only when a browser can actually be opened (macOS, Windows, or a Linux session with `DISPLAY`/`WAYLAND_DISPLAY`) and the stack advertises a PKCE endpoint. If the browser leg fails, the CLI prints a warning and automatically retries with the device-code flow. - ---- - -## `cloud push` - -Preview or apply a local company push into the connected cloud stack. This is the command you run after `cloud connect` whenever you want the upstream stack to reflect your local company. - -```sh -# Preview first — never apply blind -paperclipai cloud push --company --dry-run - -# Apply once the preview looks right -paperclipai cloud push --company -``` - -The push targets the stored connection for the company's stack. If you have connected more than one stack, select the right one with `--remote-url`: - -```sh -paperclipai cloud push --company --remote-url https://acme.paperclip.cloud -``` - -Before doing any work, `cloud push` checks your local instance's experimental settings and refuses to run unless `enableCloudSync` is `true`. It then re-runs discovery (the stack must still advertise a compatible schema and the `cloud_sync` flag), exports the company, and runs the coordinator in `preview` or `apply` mode. - -### Options - -| Flag | Use | -|---|---| -| `--company ` | Required. The local company id to export and push. | -| `--remote-url ` | Use a specific stored cloud connection. Omit it when you have a single connection. | -| `--dry-run` | Preview the push without applying anything upstream. Defaults to off. | -| `--max-entities-per-chunk ` | Chunk size for upstream uploads. Defaults to `100`. | - -> **Tip:** Always run `--dry-run` first. The preview reports exactly what an apply would create, update, adopt, skip, or conflict on, so you can catch a stale mapping or a target conflict before it lands. - -### Reading the output - -Both modes print a one-line summary of per-entity outcomes, plus the run id and the manifest hash for the bundle: - -``` -Cloud Push Applied -run= -manifest= -create=3 update=1 adopt=8 skip=0 conflict=0 staleMapping=0 -``` - -| Outcome | Meaning | -|---|---| -| `create` | A new entity was created upstream. | -| `update` | An existing upstream entity was updated to match local. | -| `adopt` | An already-matching upstream entity was linked to the local source without rewriting it. | -| `skip` | The entity was unchanged and left as-is. | -| `conflict` | The upstream target entity could not be reconciled with the local source. | -| `staleMapping` | A previously recorded source→target mapping no longer points at a valid upstream entity. | - -Any warnings from the local export are printed in yellow, conflicts in red (the first ten, then a truncation count), and — on apply — the last few run events. Use `--json` to capture the full result and run events for scripting. - -### Exit codes - -`cloud push` uses distinct exit codes so automation can branch on the outcome: - -| Exit code | Condition | -|---|---| -| `0` | Push completed with no conflicts. | -| `2` | The push reported one or more conflicts (`conflict + staleMapping > 0`). The push still ran; resolve the conflicts and push again. | -| `3` | The upstream schema did not match your CLI's transfer schema. Upgrade so both ends agree before retrying. | - -> **Note:** A conflict exit code (`2`) means the push was applied but some entities could not be reconciled — it is not a hard failure. Inspect the printed conflicts (or the `--json` output), fix the upstream or local state, and re-run. The content-hash idempotency means re-pushing an unchanged company will adopt or skip rather than duplicate. - ---- - -## Enabling cloud sync - -`cloud push` reads the instance experimental settings and aborts with a clear message if `enableCloudSync` is not `true`: - -``` -Cloud sync is disabled. Enable the cloud sync experimental setting before running `paperclipai cloud push`. -``` - -This flag lives on the local instance's experimental settings, not on the CLI. Turn it on deliberately — cloud sync moves company data off your instance into an external stack, so it is off by default. `cloud connect` does not require the flag (you can authorize ahead of time), but the first `push` will stop until it is enabled. - ---- - -## Typical workflow - -```sh -# 1. Authorize this instance against the cloud stack (once per stack) -paperclipai cloud connect https://acme.paperclip.cloud - -# 2. Enable the enableCloudSync experimental setting on your local instance - -# 3. Preview the push for the company you want to sync -paperclipai cloud push --company --dry-run - -# 4. Apply it -paperclipai cloud push --company -``` - -If the preview surfaces conflicts (exit code `2`), reconcile them before applying. If it reports a schema mismatch (exit code `3`), upgrade the CLI or the stack so the transfer schema majors agree, then retry. - ---- - -## Common flags - -Both `cloud connect` and `cloud push` accept the standard client options that resolve the local API and authentication: `--data-dir `, `--api-base `, `--api-key `, `--context `, `--profile `, and `--json`. These control how the CLI talks to your **local** instance (the source of the export). The upstream connection to the cloud stack is handled separately by the stored connection record and its signing key. - -See [Common Options](./common-options.md) for the full resolution order and behavior of these flags. - ---- +If you were relying on a scripted `cloud push`, the equivalent is `company export` on the source instance followed by `company import` on the target. Both accept `--json` for automation. ## See also +- [Company Commands](./company.md) — the export/import flow that now owns company portability - [Common Options](./common-options.md) — shared client flags and API base resolution - [Output and Scripting](./output-and-scripting.md) — using `--json` and exit codes in automation -- [Company Commands](./company.md) — `company export` / `company import`, the local portability flow that backs the push bundle -- [Authentication](./authentication.md) — connecting and authenticating against your local instance diff --git a/docs/reference/cli/company.md b/docs/reference/cli/company.md index f1e3486..2b48d6f 100644 --- a/docs/reference/cli/company.md +++ b/docs/reference/cli/company.md @@ -83,6 +83,10 @@ paperclipai company archive Paperclip companies are portable. `company export` writes a company to a folder of markdown-backed files (plus a `.paperclip.yaml` manifest and any binary assets), and `company import` reads that package back — from a local path, a `.zip`, or a GitHub repository — into a new or existing company. +The package is full-fidelity: alongside the company record, agents, projects, and skills, it carries tasks with their comments, labels, blocked-by relationships, documents, work products, and monitors, plus routines with their schedule triggers, and attachments as content-addressed files. Approvals, cost history, and activity log entries stay behind on the source instance — they describe what happened there rather than how the company is configured. + +> **Note:** This pair of commands is how you move a company between instances. It replaces the retired host-to-host Cloud Sync, so there is no longer any need to connect one instance to another first. See [Export & Import](../../guides/power/export-import.md) for the walkthrough. + ### Export a portable package ```sh @@ -139,6 +143,10 @@ The import always runs a server-side preview first and shows you the plan — pa When an imported agent has a `process` adapter with no explicit adapter, the CLI currently falls back to the `claude-local` adapter and tells you so in the preview/result info block. +Before any record is written, the server verifies every attachment in the package against the content hash it is filed under. A mismatch aborts the whole import with a message naming the offending file, so a corrupted or tampered-with package cannot leave a half-imported company behind. The CLI's folder import is also the recommended path for a very large company — the UI itself points you here when an upload is too big to preview in the browser. + +Imported agents always come in with their timer heartbeats switched off, regardless of what the package says, so nothing starts spending before you have reviewed it. + ### Raw API passthrough variants If you would rather drive the server's portability routes directly with a hand-built payload, four passthrough commands post your `--payload-json` to the raw routes. These do not read or write local files — they are the API endpoints behind `export`/`import` exposed for scripting. diff --git a/docs/reference/cli/installation.md b/docs/reference/cli/installation.md index beb9e55..8cf38d8 100644 --- a/docs/reference/cli/installation.md +++ b/docs/reference/cli/installation.md @@ -6,6 +6,8 @@ paperclip_version: v2026.609.0 The Paperclip CLI ships as a single binary, `paperclipai`. Use this page to get that binary on your machine, point it at an instance, and bring up a working local instance from a cold start. The shortest route from "nothing installed" to "a healthy server you can operate against" is two commands: `paperclipai onboard` to write a config, then `paperclipai run` to launch it. +If you already know Paperclip is going to live on this machine for a while, skip straight to [the managed install](#the-managed-install) — it is the version of the CLI you can update, roll back, and run as a background service. + --- ## Prerequisites @@ -28,7 +30,7 @@ That single requirement covers the common local install: `paperclipai run` can s npx paperclipai onboard --yes ``` -That is the canonical way to start, and it always runs the latest published version. If you would rather have a persistent `paperclipai` command on your `PATH`, install it globally: +That is the canonical way to *start*, and it always runs the latest published version. For a `paperclipai` command that persists, you have two choices: the [managed install](#the-managed-install) described in the next section, which is the one to prefer, or a plain global npm install: ```sh npm install -g paperclipai @@ -36,12 +38,95 @@ npm install -g paperclipai paperclipai --help ``` -The package exposes exactly one binary, `paperclipai`. **Every example in this documentation is written in the form `paperclipai `.** If you did not install globally, prepend `npx` — running it through `npx` is equivalent to calling the global binary directly. +The package exposes exactly one binary, `paperclipai`. **Every example in this documentation is written in the form `paperclipai `.** If you have not installed it, prepend `npx` — running it through `npx` is equivalent to calling the installed binary directly. > **Note:** If you are working inside a clone of the Paperclip monorepo, use the in-repo development alias `pnpm paperclipai ` instead. It runs the same CLI straight from source via `tsx`, which is what you want while developing the CLI itself. The monorepo also documents `npx paperclipai onboard --yes` as the standard quickstart for a packaged install. --- +## The managed install + +`npx` is perfect for trying Paperclip out, but it is temporary — every run resolves whatever happens to be cached, and there is nothing to update or roll back. When you want Paperclip to stay, install it into the **managed CLI store**: + +```sh +npx paperclipai install +``` + +That one command downloads the latest published release, verifies it, and installs a `paperclipai` command into `~/.local/bin`. From then on you type `paperclipai` directly, and you get three things `npx` cannot give you: one-command updates, instant rollback to the previous version, and the ability to run Paperclip as a [background service](./service.md). + +The managed install needs Node.js 20 or newer, the same as the rest of the CLI. + +### What it puts on disk + +A managed install keeps every version it has downloaded as its own self-contained payload, and points a symlink at whichever one is active. Nothing is installed system-wide, and nothing needs `sudo` — it all lives under your own home directory. + +| What | Path | +|---|---| +| Managed store root | `~/.paperclip/cli` | +| Downloaded payloads | `~/.paperclip/cli/installs` | +| Install manifest | `~/.paperclip/cli/install.json` | +| Link to the active payload | `~/.paperclip/cli/current` | +| The `paperclipai` command itself | `~/.local/bin/paperclipai` | + +The manifest also remembers the two payloads you were on before, which is what makes `paperclipai update --rollback` instant. Payloads older than that are pruned on the next successful update. + +If `~/.local/bin` is not already on your `PATH`, `install` offers to add it to your `~/.bashrc` or `~/.zshrc` for you, inside a clearly marked block it knows how to remove again later. Decline the prompt (or use a different shell) and it prints the line to add yourself: + +```sh +export PATH="$HOME/.local/bin:$PATH" +``` + +### Choosing what to install + +By default you get the latest stable release. Pass a flag to pick something else: + +```sh +paperclipai install # latest stable +paperclipai install --canary # the canary channel +paperclipai install --version 2026.609.0 # one exact published version +paperclipai install --ref main # built from GitHub source +``` + +| Flag | Use | +|---|---| +| `--canary` | Install the npm canary channel — newer features, rougher edges. | +| `--version ` | Install an exact published npm version. It has to be a complete `x.y.z` version; `latest` and partial versions are rejected. | +| `--ref ` | Install a GitHub branch, tag, or commit SHA, built from source on your machine. | +| `--repo ` | Override the GitHub repository used with `--ref` (default `paperclipai/paperclip`). Requires `--ref`. | +| `-y`, `--yes` | Consent to git-ref code execution and supported shell `PATH` updates without prompting. Use it in scripts and CI. | + +`--canary` and `--version` are mutually exclusive, and `--ref` cannot be combined with either — each install picks exactly one channel. + +> **Warning:** `--ref` downloads a repository at that commit and runs its dependency and build scripts on your machine. The CLI warns you and asks for confirmation before it fetches anything; in a non-interactive shell it refuses unless you pass `--yes`. Only point it at source you trust. + +Whichever route you take, the CLI runs a smoke check on the new payload — it executes the installed binary and confirms it reports the version you asked for — before making it the active one. If that check fails, nothing is switched over. + +Verify the result at any time: + +```sh +paperclipai --version +``` + +To move between channels or versions later, you do not need to reinstall — see [`paperclipai update`](../../how-to/update-paperclip.md). + +--- + +## Removing the managed install + +`paperclipai uninstall` removes the managed CLI and leaves every piece of your data alone. + +```sh +paperclipai uninstall +``` + +It takes no options. In order, it stops and removes the [background service](./service.md) if one is installed, deletes the `paperclipai` command from `~/.local/bin`, removes the `PATH` block it added to your `~/.bashrc` and `~/.zshrc`, and deletes the managed store at `~/.paperclip/cli`. + +Your companies, config, database, logs, and secrets all live elsewhere under `~/.paperclip` and are untouched — reinstall later and you pick up exactly where you left off. + +> **Note:** If `~/.local/bin/paperclipai` turns out not to be a Paperclip-managed shim (for example you replaced it by hand), `uninstall` leaves that file alone and tells you so rather than deleting someone else's command. It also refuses to run while services for *other* instances are still installed; uninstall those with [`paperclipai service uninstall --instance `](./service.md) first. + +--- + ## First run: bootstrap a local instance Two commands take you from an empty machine to a running server. @@ -79,6 +164,8 @@ When it finishes, onboarding prints the resolved configuration and the next comm | `--yes`, `-y` | Accept Quickstart defaults non-interactively and start immediately. Without `--bind`, this forces trusted-local loopback defaults, ignoring any reachability env vars. Use it in scripts and CI. | | `--run` | Save the config, then start the server right away (equivalent to answering "yes" to the start prompt). | | `--bind ` | Apply a reachability preset to the Quickstart server config. One of `loopback`, `lan`, or `tailnet`. | +| `--install-service` | Install and start the background service after onboarding, without asking. | +| `--no-install-service` | Do not install or suggest the background service. | | `--config `, `-c` | Write the config to a specific path instead of the default instance config. | | `--data-dir `, `-d` | Isolate all local Paperclip state away from `~/.paperclip` — handy for clean test instances and worktrees. | @@ -93,6 +180,8 @@ paperclipai onboard --run paperclipai onboard --bind lan ``` +In an interactive terminal, onboarding also asks whether you want to install Paperclip as a background service so it starts on login and keeps running after you close the terminal. Say yes and it installs and starts the service for you instead of running in the foreground. `--install-service` skips the question and does it; `--no-install-service` skips it and doesn't. See [service](./service.md) for what gets installed. + > **Note:** If Paperclip is already configured at the target path, rerunning `onboard` detects the existing install and keeps the current configuration unchanged. It still tops up the agent JWT secret and secrets key file if needed, then offers to start. To change settings on an existing install, use `paperclipai configure` rather than re-onboarding. --- @@ -115,6 +204,7 @@ paperclipai run | `--instance `, `-i` | Select the local instance id (default: `default`). Use this to run more than one isolated instance side by side. | | `--bind ` | On a *first* run (when no config exists yet), pass a reachability preset into onboarding: `loopback`, `lan`, or `tailnet`. | | `--repair` / `--no-repair` | Repair is on by default; `--no-repair` runs `doctor` in read-only mode so it reports problems without changing files. | +| `--force` | Run even when the same instance is active under the service manager. | | `--config `, `-c` | Use a specific config file. | | `--data-dir `, `-d` | Isolate local state away from `~/.paperclip`. | @@ -131,6 +221,8 @@ paperclipai run --no-repair > **Tip:** In a non-interactive environment (no TTY), `run` will not start onboarding for you. Run `paperclipai onboard` once first, then retry `paperclipai run`. +> **Note:** If the same instance is already running as a background service, `run` stops with an error rather than starting a second copy on the same port. Check on the running one with `paperclipai service status`, or pass `--force` if you really do want a foreground process alongside it. + > **Warning:** `run` (with no subcommand) is a *local bootstrap* command — it stands up a server on your machine. The `run` *subcommands* (`run list`, `run live`, `run get`, `run events`, `run log`, and the rest) are a different concept entirely: they inspect and control heartbeat runs through the API on an already-running instance. Do not confuse the two. See [run](./run.md) for the subcommands. --- @@ -157,6 +249,8 @@ That step is covered in [authentication](./authentication.md). For how the CLI f ## Next steps - [Authentication](./authentication.md) — connect the CLI, choose a persona, and mint board tokens or agent keys. +- [Service](./service.md) — keep Paperclip running in the background instead of holding a terminal open. +- [Update Paperclip](../../how-to/update-paperclip.md) — check for, apply, and roll back new versions. - [Setup commands](./setup-commands.md) — `doctor`, `configure`, `env`, and `allowed-hostname` for repairing and tuning an instance. - [Common options](common-options.md) — shared flags and how the CLI resolves which server to talk to. - [run](./run.md) — the `run` subcommands for inspecting heartbeat runs on a live instance. diff --git a/docs/reference/cli/service.md b/docs/reference/cli/service.md new file mode 100644 index 0000000..3287bd7 --- /dev/null +++ b/docs/reference/cli/service.md @@ -0,0 +1,148 @@ +# Service Commands + +Most people start out running Paperclip with `paperclipai run` in a terminal window, which works right up until you close the window. The `service` commands hand that job to your operating system instead: Paperclip starts when you log in, restarts itself if it crashes, keeps a log you can tail, and survives you quitting the terminal entirely. Reach for these commands when you want Paperclip to be *always there* rather than something you remember to start. + +On macOS the service is a launchd agent; on Linux it is a systemd **user** service. Either way it runs as you, in your own account — nothing here needs `sudo`, and nothing is installed system-wide. + +> **Note:** Every `service` subcommand accepts `-i, --instance ` to target a specific local instance (default `default`), and `--json` to print machine-readable output instead of text. If service management is not available — Windows, or a Linux container or WSL1 without a usable systemd user manager — each command says so and exits cleanly, and you should use [`paperclipai run`](./setup-commands.md#paperclipai-run-local-bootstrap) instead. + +--- + +## What gets installed + +The service definition is a small file in your own home directory that tells the OS to run `paperclipai run --instance `. It is generated from your current install, so it always points at the CLI you actually have. + +| | macOS | Linux | +|---|---|---| +| Supervisor | launchd | systemd (user) | +| Service name | `ing.paperclip.paperclipai` | `paperclipai.service` | +| Definition file | `~/Library/LaunchAgents/ing.paperclip.paperclipai.plist` | `~/.config/systemd/user/paperclipai.service` | +| Logs | `~/.paperclip/instances//logs/service.log` and `service.err.log` | the journal, via `journalctl --user` | + +Non-default instances get their own service so they never collide: instance `dev` becomes `ing.paperclip.paperclipai.dev` on macOS and `paperclipai-dev.service` on Linux. + +The service is started with `PAPERCLIP_SERVICE_MANAGED=1`, `PAPERCLIP_INSTANCE_ID`, and `PAPERCLIP_HOME` set, so the server knows it is running under a supervisor and which instance and home directory to use. + +--- + +## Install the service + +`service install` writes the definition, registers it with the supervisor, and starts Paperclip. + +```sh +paperclipai service install +paperclipai service install --no-start-now +paperclipai service install --no-start-on-login +paperclipai service install --enable-linger +``` + +| Flag | Use | +|---|---| +| `--no-start-now` | Install without starting now. Use it when you want the service registered but intend to start it yourself later. | +| `--no-start-on-login` | Install without enabling start on login. The service exists but stays dormant until you start it. | +| `--enable-linger` | Allow systemd startup without an active login session. Linux only. | +| `-i, --instance ` | Local instance id (default: `default`). | +| `--json` | Print machine-readable JSON. | + +On Linux, a user service normally stops when you log out — which is not what you want on a headless box you only ever SSH into. "Lingering" is the systemd setting that keeps it running anyway. In an interactive terminal the CLI asks whether to enable it; `--enable-linger` answers yes up front. Enabling it runs `loginctl enable-linger` for your user and may request system authorization. + +The command reports whether the definition changed, the platform, the service name, the path to the definition file, and whether lingering was enabled. + +> **Tip:** Rerunning `service install` is safe and is the normal way to repair a service whose definition has drifted — for example after moving your install. It rewrites the definition only if the contents actually changed. + +You can also get all of this during first-run setup: [`paperclipai onboard`](./setup-commands.md#paperclipai-onboard) offers to install the service for you, and `paperclipai onboard --install-service` skips the question. + +--- + +## Start, stop, and check on it + +```sh +paperclipai service status +paperclipai service start +paperclipai service stop +``` + +| Command | What it does | +|---|---| +| `status` | Show supervisor and health status. | +| `start` | Start the background service. | +| `stop` | Stop the background service. | + +`status` is the one to reach for first when something looks wrong, because it answers two different questions at once. From the supervisor it reports the platform, service name, whether the service is `installed`, `active`, and `enabled`, and the process id. Then it probes the instance's own health endpoint and reports whether the server actually answered and which version it is running. + +Those can disagree, and the disagreement is the useful part: a service that is `active` but not healthy is a server that started and then failed, and a healthy instance while the service is inactive usually means a foreground `paperclipai run` is holding the port. + +`start` and `stop` both print the resulting status, so you can see the effect of what you just did. + +--- + +## Restart without losing work + +A blunt stop-then-start would kill agents mid-run. `service restart` is a **hot restart**: it tells the running server a restart is coming so in-flight agent runs can be handed over to the new process rather than dropped. + +```sh +paperclipai service restart +paperclipai service restart --wait +paperclipai service restart --expected-version 2026.609.0 +``` + +| Flag | Use | +|---|---| +| `--wait` | Wait for active runs to drain instead of adopting them. | +| `--expected-version ` | Require the restarted server to report this version. | + +Here is what happens. The CLI takes a restart lock for the instance so two restarts can never overlap, records the current server's process id and version, asks the supervisor to restart, then polls the health endpoint until the server comes back — up to a minute. If you passed `--expected-version`, it keeps waiting until the version the server reports matches, so a restart that silently came back on the old build is reported as a failure instead of a success. Finally it prints the new status, the health result, and the server's own report of how the handover went. + +By default active runs are *adopted* by the new process. Pass `--wait` when you would rather let them finish first. + +> **Note:** If a restart is interrupted, the next one may report that another restart is still running. The message includes the path to the lock file (`hot-restart.lock`, inside the instance directory) so you can remove it and retry once you have confirmed nothing else is restarting. + +`paperclipai update` performs this same hot restart for you when it detects an active service, so you rarely need to run it by hand after an update. See [Update Paperclip](../../how-to/update-paperclip.md). + +--- + +## Read the logs + +```sh +paperclipai service logs +paperclipai service logs --follow +paperclipai service logs --lines 500 +``` + +| Flag | Use | +|---|---| +| `-f`, `--follow` | Follow new log output. | +| `-n`, `--lines ` | Number of recent lines (default `100`). Must be a positive integer. | + +The output goes straight to your terminal. On Linux this reads the systemd journal for the service; on macOS it tails the service's stdout and stderr log files under the instance's `logs` directory. + +> **Tip:** `paperclipai service logs --follow` in one window while you trigger work in another is the quickest way to watch a startup problem happen in real time. + +--- + +## Remove the service + +```sh +paperclipai service uninstall +``` + +`uninstall` stops the service, disables it so it will not come back on login, and deletes its definition file. It then re-checks the supervisor and fails loudly if the service is somehow still loaded, so a partial removal is never reported as success. + +This removes only the service. Your Paperclip install, config, and data are untouched, and `paperclipai run` still works exactly as before. To remove the CLI itself, see [`paperclipai uninstall`](./installation.md#removing-the-managed-install) — which stops and removes the service for you as part of its own cleanup. + +--- + +## How doctor sees the service + +[`paperclipai doctor`](./setup-commands.md#paperclipai-doctor) checks the service as part of its normal run, so you usually find out something is wrong before you go looking. It verifies that the definition on disk matches what the current install would generate, that the service is active, that the health endpoint reports OK, and that the running version matches your managed install. On Linux it also warns when start-on-login is enabled but lingering is off — the combination that quietly stops Paperclip when you log out. + +If no service is installed, doctor passes and says so: running Paperclip in the foreground is a perfectly valid choice. + +--- + +## See also + +- [Installing the CLI](./installation.md) — the managed install the service runs, and how to remove it. +- [Setup Commands](./setup-commands.md) — `run`, `onboard`, and `doctor`, including the flags that interact with the service. +- [Update Paperclip](../../how-to/update-paperclip.md) — updating an instance that runs as a service. +- [Common Options](./common-options.md) — the shared flags used across the CLI. diff --git a/docs/reference/cli/setup-commands.md b/docs/reference/cli/setup-commands.md index 6bc7798..dd64bdf 100644 --- a/docs/reference/cli/setup-commands.md +++ b/docs/reference/cli/setup-commands.md @@ -21,6 +21,8 @@ Reach for setup commands when you need to: - take a one-off database snapshot ([`db:backup`](#paperclipai-dbbackup)) - trust a private hostname in authenticated mode ([`allowed-hostname`](#paperclipai-allowed-hostname)) - bootstrap and launch a local server in one step ([`run`](#paperclipai-run-local-bootstrap)) +- keep an instance running in the background across logins ([`service`](./service.md)) +- install, update, roll back, or remove the CLI itself ([Installing the CLI](./installation.md) and [Update Paperclip](../../how-to/update-paperclip.md)) - mint the first board credential headlessly ([`auth bootstrap-ceo`](#paperclipai-auth-bootstrap-ceo)) - trigger one agent heartbeat for debugging ([`heartbeat run`](#paperclipai-heartbeat-run)) - pause every routine in a company during an incident ([`routines disable-all`](#paperclipai-routines-disable-all)) @@ -56,9 +58,12 @@ paperclipai run --no-repair | `--bind ` | On first run only, pass an onboarding reachability preset: `loopback`, `lan`, or `tailnet`. | | `--repair` | Attempt automatic repairs during doctor. Enabled by default. | | `--no-repair` | Disable automatic repairs during doctor. | +| `--force` | Run even when the same instance is active under the service manager. | > **Tip:** `--bind` only matters on the very first `run`, when it forwards the preset to onboarding. Once a config exists, `run` reuses it; change binding later with [`configure --section server`](#paperclipai-configure). +> **Warning:** Before anything else, `run` checks whether this instance is already active under the service manager. If it is, `run` refuses to start — two servers on one port and one database is not a state you want — and points you at `paperclipai service status --instance `. `--force` bypasses that check when you genuinely want a foreground process anyway. See [Service](./service.md). + --- ## `paperclipai onboard` @@ -85,8 +90,12 @@ The first prompt offers two paths: | `-d, --data-dir ` | Isolate all local state from `~/.paperclip`. | | `--bind ` | Quickstart reachability preset: `loopback`, `lan`, or `tailnet`. | | `-y, --yes` | Accept Quickstart defaults non-interactively and start immediately. Without `--bind`, this forces trusted-local loopback defaults and ignores conflicting reachability env vars. | +| `--install-service` | Install and start the background service after onboarding. | +| `--no-install-service` | Do not install or suggest the background service. | | `--run` | Start the server immediately after saving the config. | +Near the end of the wizard, `onboard` offers to install Paperclip as a background service so it starts on login and keeps running after you close the terminal. `--install-service` installs it without asking; `--no-install-service` skips both the install and the suggestion. When the service is installed, onboarding does *not* also start a foreground server — the service is already running Paperclip for you. On a platform where service management is unavailable, `--install-service` warns and continues. See [Service](./service.md) for what gets installed and how to manage it. + > **Note:** If a valid config already exists, `onboard` preserves it unchanged, ensures the agent JWT secret and secrets key exist, and prints next-step commands. Use [`configure`](#paperclipai-configure) to change settings on an existing install rather than re-onboarding. When the instance is `authenticated` with the embedded database, the bootstrap CEO invite is deferred until the server is running — `onboard` reminds you to run `paperclipai run` then [`auth bootstrap-ceo`](#paperclipai-auth-bootstrap-ceo). @@ -114,9 +123,24 @@ It loads the config's `.env`, then runs these checks in order, stopping early on - LLM provider - log directory - listen port +- Node.js runtime +- the managed install: store, shim, `PATH`, and payload retention +- the background service: definition, runtime, health, version, and lingering Each line reports `✓ pass`, `! warn`, or `✗ fail` with a repair hint. The summary counts passed/warned/failed, and a clear "fix and re-run" message is printed when anything fails. +The last three groups cover *how Paperclip is installed and supervised*, not how it is configured: + +| Check group | What it looks at | +|---|---| +| Node.js runtime | Fails on anything older than Node.js 20. | +| Managed install | Only runs if you have a [managed install](./installation.md#the-managed-install). Confirms the manifest and active payload agree, that `~/.local/bin/paperclipai` is a real Paperclip shim, that its directory is on `PATH` (a warning if not), and that no orphaned payloads are left behind. | +| Background service | Only runs if a [service](./service.md) is installed for this instance. Confirms the definition on disk matches what your current install would generate, that the service is active, that the health endpoint answers, and that the running version matches your managed install. On Linux it warns when start-on-login is on but systemd lingering is off. | + +All three are optional by design. Running Paperclip through `npx`, a global npm install, or a source checkout, with no background service, passes cleanly — the checks report "not present" rather than complaining. + +> **Note:** `doctor` (like `run`) also does a quick, cached, once-a-day check for a newer published version and prints a one-line notice when one exists. Turn it off by setting `PAPERCLIP_UPDATE_CHECK=0`, or by setting `updates.checkEnabled` to `false` in your config. + | Flag | Use | |---|---| | `-c, --config ` | Path to the config file. | @@ -320,6 +344,8 @@ Use `--data-dir` (or `--instance`) to run an isolated instance — handy for cle ## See also - [Installation](./installation.md) — install the CLI and pick a deployment shape +- [Service](./service.md) — run an instance in the background with `paperclipai service` +- [Update Paperclip](../../how-to/update-paperclip.md) — check, apply, and roll back new versions - [Authentication](./authentication.md) — board tokens, agent keys, and the login flows after bootstrap - [Runs](./run.md) — inspecting and controlling heartbeat runs (the `run` *subcommands*) - [Routines](./routine.md) — create, edit, and resume scheduled automation diff --git a/docs/reference/cli/skills.md b/docs/reference/cli/skills.md index 302700b..2650eb5 100644 --- a/docs/reference/cli/skills.md +++ b/docs/reference/cli/skills.md @@ -17,10 +17,10 @@ There are two command groups. The plural `paperclipai skills` group is the one y | Operation | Commands | What it changes | | --- | --- | --- | | **Company install** | `skills install`, `import`, `create`, `scan-projects` | Adds or updates a skill in the company's library. Available to the whole company; does **not** attach it to any agent. | -| **Agent attach** | `skills agent sync`, `skills agent clear` | Replaces an agent's *desired* company-skill set. A desired-state operation — it is not additive. | +| **Agent attach** | `skills agent sync`, `skills agent clear` | Changes an agent's *desired* company-skill set. `sync` takes a required `--mode` (`add`, `remove`, or `replace`) so you can grow, prune, or overwrite the set; `clear` empties it. | | **Adapter runtime sync** | reported by `skills agent list`; triggered by `sync`/`clear` | The server-side adapter reconciles the desired set with files on disk and returns an `AgentSkillSnapshot`. | -> **Note:** `skills agent sync` and `skills agent clear` operate on the agent's full desired set — `clear` empties it. Bundled Paperclip skills still live in the company library as read-only entries, but the server no longer force-attaches them to an agent. +> **Note:** `skills agent sync` now requires a `--mode` (`add`, `remove`, or `replace`), so you choose whether the named skills join, leave, or overwrite the desired set. `skills agent clear` empties the set outright (it uses `replace` internally with an empty list). Bundled Paperclip skills still live in the company library as read-only entries, but the server no longer force-attaches them to an agent. > **Tip:** Add `--json` to any command to print the raw API result for scripting. Every company-scoped command takes `--company-id ` (and respects your selected profile/context). See [common options](./common-options.md). @@ -162,26 +162,33 @@ paperclipai skills remove --yes --company-id ## Agent attach: desired skills -Once a skill is in the library, attaching it to an agent is a desired-state replacement, not an addition. `skills agent sync` sets the agent's full desired set; anything you omit is dropped. The adapter then reconciles and the command returns an `AgentSkillSnapshot`. +Once a skill is in the library, `skills agent sync` decides how the skills you name meet the agent's current desired set. That is what `--mode` is for, and it is required — you pick one of three behaviours: + +- `add` keeps everything the agent already has and unions in the skills you name (a skill already present is updated in place). +- `remove` drops only the skills you name and leaves the rest of the set alone. +- `replace` overwrites the complete desired set with exactly the skills you pass, detaching anything you omit. This is the old full-replacement behaviour. + +The adapter then reconciles and the command returns an `AgentSkillSnapshot`. ```sh paperclipai skills agent list --company-id -paperclipai skills agent sync --skill review-prs --skill agent-browser --company-id +paperclipai skills agent sync --skill review-prs --skill agent-browser --mode add --company-id paperclipai skills agent clear --yes --company-id ``` | Command | Use | | --- | --- | | `agent list ` | Show the agent's runtime skill snapshot: adapter type, whether sync is `supported`, the `mode`, the desired count, and per-skill runtime entries (state, origin, managed flag). | -| `agent sync ` | Replace the agent's desired company skills and sync runtime state. Requires at least one `--skill`. | -| `agent clear ` | Clear the desired set (sends an empty list). Prompts in a TTY; requires `--yes` otherwise. | +| `agent sync ` | Add, remove, or replace the agent's desired company skills and sync runtime state. Requires at least one `--skill` and a `--mode`. | +| `agent clear ` | Clear the desired set (empties it via an internal `replace` with an empty list). Prompts in a TTY; requires `--yes` otherwise. | | Flag | Use | | --- | --- | | `--skill ` | (`sync`) A desired company skill `id`, `key`, or `slug`. Repeat for several. At least one is required. | +| `--mode ` | (`sync`) **Required.** Merge mode: `add` keeps other skills; `remove` deletes only named skills; `replace` destructively overwrites the complete set. | | `--yes` | (`clear`) Confirm without the interactive prompt. | -An `` is an agent `id` or shortname/url-key. Because `sync` is a full replacement, list the complete desired set every time — anything you omit is detached, and `clear` detaches everything. +An `` is an agent `id` or shortname/url-key. With `--mode replace` you must list the complete desired set every time — anything you omit is detached — while `--mode add` and `--mode remove` only touch the skills you name. `clear` detaches everything. --- diff --git a/docs/reference/deploy/docker.md b/docs/reference/deploy/docker.md index 901e64e..4be7999 100644 --- a/docs/reference/deploy/docker.md +++ b/docs/reference/deploy/docker.md @@ -6,7 +6,7 @@ paperclip_version: v2026.609.0 Use Docker when you want a self-contained Paperclip instance without installing Node or pnpm on the host machine. -This page covers the quickstart image, the manual image build, and what persists between container restarts. +This page covers the quickstart image, the manual image build, the cloud image variant, and what persists between container restarts. --- @@ -45,7 +45,7 @@ PAPERCLIP_PORT=3200 PAPERCLIP_DATA_DIR=../data/pc \ If you want a plain container run instead of compose, build and start the image manually: ```sh -docker build -t paperclip-local . +docker build --target production -t paperclip-local . docker run --name paperclip \ -p 3100:3100 \ -e HOST=0.0.0.0 \ @@ -56,6 +56,38 @@ docker run --name paperclip \ Use this when you want tight control over the container lifecycle or are embedding Paperclip into a larger Docker workflow. +> **Note:** Name the `production` stage explicitly. The Dockerfile declares a `cloud` stage *after* `production`, and a build with no `--target` picks the last stage in the file — so leaving it off would quietly give you the cloud variant instead. Paperclip's own published image is pinned the same way. + +--- + +## Cloud Image Variant + +Paperclip publishes a second image alongside the regular one: the **cloud variant**. If you are self-hosting, you almost certainly want the plain tag. The cloud variant exists for managed deployments and carries extra weight you do not need. + +The only difference is what is pre-built inside it. The cloud variant is the production image plus a small set of sandbox-provider plugins that have already been compiled. Managed instances receive a `plugins.autoInstall` list through `PAPERCLIP_MANAGED_CONFIG` and install those plugins from the bundled catalog at boot, which works only when each plugin's `dist/` output is already present in the image. The default image ships the plugin source but not the build output, so auto-install skips those plugins and logs that the bundle is not present. + +Cloud images are published under the same tag set as the regular image, each tag carrying a `-cloud` suffix: + +- `sha--cloud` +- `latest-cloud` +- `-cloud` + +You can build the variant yourself by naming the `cloud` stage: + +```sh +docker build --target cloud -t paperclip-cloud . +``` + +Which plugins get built in is controlled by the `CLOUD_BUNDLED_PLUGINS` build argument — a space-separated list of directory names under `packages/plugins/sandbox-providers`. It defaults to `daytona`: + +```sh +docker build --target cloud \ + --build-arg CLOUD_BUNDLED_PLUGINS="daytona" \ + -t paperclip-cloud . +``` + +Every name you add pulls that plugin's dependencies into the image, so keep the list to what your deployment actually auto-installs. An unknown directory name fails the build rather than shipping a variant that is quietly missing a plugin. + --- ## What Persists diff --git a/docs/reference/deploy/environment-variables.md b/docs/reference/deploy/environment-variables.md index c720e9e..4d516a0 100644 --- a/docs/reference/deploy/environment-variables.md +++ b/docs/reference/deploy/environment-variables.md @@ -78,6 +78,107 @@ A malformed value (a stray sign, leading zeros, or an unrecognised token) makes --- +## Cloud-Managed Instances + +| Variable | Default | Meaning | +|---|---|---| +| `PAPERCLIP_MANAGED_CONFIG` | unset (self-hosted) | Set only by the Paperclip Cloud harness. Holds one JSON document describing which experimental feature flags the harness manages for this instance, the plugin keys it declares for auto-install, and optionally the sandbox environment it provisions for you at boot. **Absent** means self-hosted: no overlay, no behavior change. **Present but blank, or malformed, makes the server refuse to start.** | + +You don't set this variable yourself. It exists so that a hosted instance gets its feature configuration from the fleet that runs it rather than from its own database, and it's documented here so you can recognise it in a running container's environment. + +The value is a single JSON document: + +```json +{ + "v": 1, + "mode": "cloud", + "catalogVersion": "2026.720.0", + "features": { "enableApps": false, "enableCases": true }, + "plugins": { "autoInstall": ["daytona", "kubernetes"] }, + "environments": [ + { "name": "Daytona", "provider": "daytona", "config": { "target": "us" } } + ] +} +``` + +Six top-level keys are allowed — `v`, `mode`, `catalogVersion`, `features`, `plugins`, and `environments` — and anything else makes the server refuse to start. + +Five of them are required: `v`, `mode`, `catalogVersion`, `features`, and `plugins.autoInstall`, though `features` may be `{}` and `plugins.autoInstall` may be `[]`. `mode` is always `"cloud"`, and `catalogVersion` records the feature-catalog version the document was validated against. + +`environments` is the one **optional** section. It arrived later than the others, and documents delivered before it existed still have to boot on newer builds — a fleet image roll can't be lockstepped with a config re-delivery — so leaving it out simply means "this instance declares no managed environment". That's different from a missing `features` or `plugins.autoInstall`, where absence would silently drop a control the harness meant to be in force. When the section *is* present, it's validated just as strictly as everything else. [The sandbox environment it provisions](#the-sandbox-environment-it-provisions) below has the details. + +### It fails closed + +Only an **absent** variable means self-hosted. Everything else is treated as a harness misconfiguration and stops the server at startup with a precise error, so a truncated or mistyped document can never silently drop a control that was supposed to be in force. The server refuses to start when the variable is: + +- set but blank (unset it entirely for self-hosted mode) +- not valid JSON, or not a JSON object +- carrying an unknown top-level key, or a `v` other than `1`, or a `mode` other than `"cloud"` +- missing `features` or `plugins.autoInstall` +- naming a feature key this build doesn't have, or one whose feature-catalog tier isn't `managed` +- giving a non-boolean value for a feature, or a blank/duplicate plugin key +- carrying a malformed `environments` section — more than one entry, an unknown key inside an entry, a blank `name` or `provider`, a `provider` that isn't also listed in `plugins.autoInstall`, a `config` that sets `provider` itself, or a `config` key that looks like a credential + +### What it can and can't reach + +`features` may only target the boolean feature-flag keys of the instance's experimental settings — the ones on **Settings → Instance settings → Experimental**. Server-managed bookkeeping fields (activation cutoffs, lookback hours) aren't booleans and are excluded by construction, so a managed document can't touch them. + +On top of that, each flag carries a tier in the build's feature catalog: `preference` (a tenant's own taste setting the harness leaves alone), `managed` (the harness may set it), or `floor` (pinned in code). Only `managed` keys are accepted. A key whose tier differs in the running build is version skew between the document's `catalogVersion` and the app, and the server refuses to start rather than apply a control with mismatched semantics. + +Paperclip publishes that tier map per release as a `feature-catalog.json` artifact — generated by `scripts/generate-feature-catalog.ts --version ` — which the cloud harness imports and validates its feature writes against. + +### Nothing is written to the database + +The overlay is never persisted. The values are applied when settings are read, which means they re-assert on every read — a database restore or a hand-edited settings row cannot resurrect a capability the harness disabled. + +In the UI, every key the overlay controls renders with a lock badge reading **Managed by Paperclip Cloud** and a disabled toggle. Self-hosted responses carry no managed keys at all, so every toggle stays editable. See [Experimental features](../../experimental/overview.md#if-a-toggle-is-locked) for the user-facing view. + +### The sandbox environment it provisions + +If you open **Settings → Instance settings → Environments** on a hosted instance and find a sandbox environment already waiting there — one you never created — the `environments` section is where it came from. The fleet declares it, and the server sets it up for you on boot so agents have somewhere to run without you wiring up a provider first. + +The feature overlay above is read-time only, but this part is different: an entry provisions a real environment row you can see in the UI, and the server re-applies it on every boot. That's safe to repeat — the step is idempotent, so a restart refreshes the row rather than piling up duplicates. + +Each entry looks like this: + +```json +{ + "name": "Daytona", + "description": "Managed sandboxes for agent runs", + "provider": "daytona", + "config": { "target": "us" } +} +``` + +- **`name`** is the display name of the environment row. +- **`description`** is optional. It mirrors the document, so dropping it later clears the description rather than pinning the old one forever. +- **`provider`** is the sandbox plugin's driver key — the same key the plugin uses in `plugins.autoInstall`, like `daytona` or `kubernetes`. It has to appear in that auto-install list too: on a managed instance the fleet is the only way a plugin gets installed, so an environment whose provider plugin isn't provisioned could never serve a run. That mismatch is caught at startup rather than at the first run. +- **`config`** is stored verbatim on the environment row and validated by the provider plugin when it acquires a lease. See [Sandbox Providers](../adapters/sandbox-providers.md) for the fields each provider accepts. + +### One entry, and never any credentials + +Two rules shape what this section is allowed to declare. + +**At most one entry.** The database enforces a single Paperclip-managed sandbox row per instance (a partial unique index named `environments_managed_sandbox_idx`), and every entry in the list provisions that one row — so a longer list could never be satisfied and is rejected at parse time. For the same reason, a **non-empty** `environments` section and a forced `PAPERCLIP_EXECUTION_MODE` are mutually exclusive: both claim the same row, and configuring both stops the server rather than letting boot order pick a winner. An empty `"environments": []` declares nothing, so it sits alongside a forced execution mode without complaint. + +**No secrets, ever.** A managed-config document carries no credentials, so any `config` key that *looks* like one — matching `api_key`, `apiKey`, `token`, `secret`, `password`, or `credential`, at any nesting depth — is treated as a misrouted credential and refuses startup. Provider credentials reach a managed instance as ordinary process environment variables instead: every bundled sandbox provider falls back to its own documented variable, such as `DAYTONA_API_KEY`, when `config` leaves the key out. + +### When the provider isn't ready + +Provisioning waits for the bundled-plugin startup pass to finish, then checks that the entry's provider plugin is installed, `ready`, **and** running a live worker. A plugin whose record says `ready` but whose activation failed has no worker and can't serve a lease, so that check matters. + +If the provider isn't up, Paperclip skips the entry rather than writing an active row — and if an earlier boot already provisioned one, it archives that row so run scheduling stops selecting an environment whose leases would fail. The server logs the reason and keeps booting; the cost is that this one environment is unavailable, not a fleet stuck in a crash loop. + +Recovery is automatic in the common case. When the plugin record is `ready` and only its worker is down — a crash in restart-backoff — Paperclip listens for the worker manager to respawn it and re-runs the setup the moment it comes back, so the environment returns without anyone restarting the server. A missing or non-`ready` plugin needs the next healthy boot instead. + +That's the same deliberate split you see elsewhere in the document: a **malformed** section refuses startup, while a **failed setup step** logs and boots degraded. + +### Removing an entry doesn't remove the environment + +Dropping the `environments` section, or the entry inside it, stops future refreshes — it does not delete or archive the environment row. This matches how `plugins.autoInstall` behaves ([Plugins](../../administration/plugins.md#what-it-wont-undo) covers the plugin side): runs may still hold leases against that environment, so withdrawing it is an explicit action someone takes, not a side effect of an edited document. The archiving described above is scoped to a provider that was declared but couldn't come up, never to an entry you removed. + +--- + ## Secrets | Variable | Meaning | @@ -101,6 +202,7 @@ These values are covered in more detail in [Secrets](./secrets.md). | `PAPERCLIP_STORAGE_S3_ENDPOINT` | Custom S3-compatible endpoint for MinIO, R2, and similar providers. | | `PAPERCLIP_STORAGE_S3_PREFIX` | Optional object key prefix. | | `PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE` | Enable path-style S3 requests when the provider needs them. | +| `PAPERCLIP_IMPORT_ZIP_MAX_BYTES` | Maximum size, in bytes, of a company-import `.zip` upload. Defaults to `1073741824` (1 GB). Set it higher to accept larger packages, or lower to tighten the cap. Values are clamped to the range 1 byte – 64 GiB; anything non-numeric, fractional-to-zero, or out of range falls back to the 1 GB default. | --- diff --git a/docs/reference/plugins/sdk.md b/docs/reference/plugins/sdk.md index 4ce481a..6d1ae32 100644 --- a/docs/reference/plugins/sdk.md +++ b/docs/reference/plugins/sdk.md @@ -51,7 +51,63 @@ All identifiers below are exported from `@paperclipai/plugin-sdk`. They are the | `runWorker` | Boots the worker JSON-RPC loop against the supplied plugin and `import.meta.url`. | At the bottom of your worker entrypoint, after `definePlugin`. | | `startWorkerRpcHost` | Lower-level entry that returns a `WorkerRpcHost` you can manage yourself (for tests or custom harnesses). | Embedding the worker in a non-default transport (e.g. an in-process test). | -Types: `PluginDefinition`, `PaperclipPlugin`, `PluginHealthDiagnostics`, `PluginConfigValidationResult`, `PluginWebhookInput`, `PluginApiRequestInput`, `PluginApiResponse`, `RunWorkerOptions`, `WorkerRpcHostOptions`, `WorkerRpcHost`. +Types: `PluginDefinition`, `PaperclipPlugin`, `PluginHealthDiagnostics`, `PluginConfigChangeContext`, `PluginConfigValidationResult`, `PluginWebhookInput`, `PluginApiRequestInput`, `PluginApiResponse`, `RunWorkerOptions`, `WorkerRpcHostOptions`, `WorkerRpcHost`. + +#### Knowing *which* company's config changed + +When an operator saves your plugin's configuration, the host calls your optional `onConfigChanged` hook so you can apply the change without a worker restart. Plugin configuration in Paperclip is company-scoped, and a worker is spawned once per plugin — not once per company — so the interesting question is always "whose config is this?" + +`onConfigChanged` now answers it. The hook takes a second argument: + +```ts +onConfigChanged?( + newConfig: Record, + context?: PluginConfigChangeContext, +): Promise; +``` + +`PluginConfigChangeContext` has a single field, `companyId: string | null` — the company whose configuration changed, or `null` for an instance/global save that is not bound to a specific company. + +You will also see this hook fire earlier than you might expect. When your worker starts, the host replays each configured company's stored config through the same `configChanged` path an operator save uses. That is what makes a **proactive** plugin possible: one that does its company work from `setup()` rather than waiting for an event, and so has no company-scoped invocation to read `ctx.config.get(companyId)` inside. The host also authorizes your plugin's configured companies as the worker's proactive scopes, so worker-to-host calls made from your own timers and loops resolve to one of those companies instead of being rejected for missing company context. Any *other* company stays denied. + +The startup replay is best-effort, so it is a head start rather than a contract: a worker that doesn't implement `onConfigChanged` simply keeps reading config at runtime with `ctx.config.get(companyId)`. Because the same config can be replayed more than once, make your `onConfigChanged` idempotent. + +#### Declaring multi-company support with `multiCompanyConfig` + +Because that replay fans out every configured company, a plugin that keeps one worker-global config would quietly end up running as whichever company arrived last. That is a cross-tenant mix-up — one company's token applied to another company's traffic — so the host **fails closed** rather than letting it happen. + +Set `multiCompanyConfig: true` on your plugin definition when your worker genuinely serves more than one company from a single process, and key your per-company state on `context.companyId`: + +```ts +import { definePlugin, runWorker, type PluginLogger } from "@paperclipai/plugin-sdk"; + +const configByCompany = new Map>(); +let logger: PluginLogger | null = null; + +const plugin = definePlugin({ + multiCompanyConfig: true, + + async setup(ctx) { + logger = ctx.logger; + }, + + async onConfigChanged(newConfig, context) { + if (!context?.companyId) { + // Instance-wide save — nothing company-specific to rebind. + return; + } + configByCompany.set(context.companyId, newConfig); + logger?.info("Applied config for one company", { companyId: context.companyId }); + }, +}); + +export default plugin; +runWorker(plugin, import.meta.url); +``` + +Leave `multiCompanyConfig` off (the default) and your plugin is treated as single-tenant. "Fails closed" then means something specific: once a `configChanged` delivery has applied one company's config, a delivery carrying a **different** config for a **different** company is rejected with `PLUGIN_RPC_ERROR_CODES.CROSS_TENANT_CONFIG` instead of overwriting what is already applied. Re-delivering the *same* config under a different company id is treated as an idempotent replay and still allowed, so duplicate scope rows that embed identical config are harmless. + +The `companyId` has always travelled on the wire — it is the optional `companyId?: string | null` field on `ConfigChangedParams`. What changed is that the SDK now hands it to your hook instead of dropping it. ### Plugin context @@ -70,22 +126,27 @@ Types: `PluginDefinition`, `PaperclipPlugin`, `PluginHealthDiagnostics`, `Plugin | `PluginStateClient` | Scoped key-value state under a `ScopeKey`. | | `PluginEntitiesClient` | Upsert and query plugin-owned entities (`PluginEntityUpsert`, `PluginEntityQuery`, `PluginEntityRecord`). | | `PluginProjectsClient`, `PluginExecutionWorkspacesClient`, `PluginCompaniesClient`, `PluginIssuesClient`, `PluginIssueRelationsClient`, `PluginIssueSummariesClient`, `PluginAgentsClient`, `PluginAgentSessionsClient`, `PluginGoalsClient`, `PluginSkillsClient` | Read/write access to the core Paperclip domain via the host. | +| `ctx.approvals` | Read and decide company approvals — see [Responding to interactions and approvals](#responding-to-interactions-and-approvals). Requires `approvals.read` for `list` / `get` and `approvals.respond` for `decide`. The interface is named `PluginApprovalsClient` in the SDK source but is not currently re-exported as a name; it is reachable from `PluginContext`. | | `ctx.routines` | Resolve and reconcile plugin-managed Paperclip routines (`ctx.routines.managed`). Requires the `routines.managed` capability. The interface type is not currently re-exported as a name, but it is reachable from `PluginContext`. | +| `ctx.execution` | Stream live output from a long-running `execute` call — see [Streaming live command output](#streaming-live-command-output). The interface is named `PluginExecutionClient` in the SDK source but is not currently re-exported as a name; it is reachable from `PluginContext`. | | `PluginDataClient` | Register data feeds the UI can query (`ctx.data.register(...)`). | | `PluginActionsClient` | Register host-invokable actions. | | `PluginStreamsClient` | Stream-style host APIs. | | `PluginToolsClient` | Register tool implementations declared in the manifest (`ToolRunContext`, `ToolResult`). | | `PluginMetricsClient`, `PluginTelemetryClient` | Emit metrics and telemetry. | +| `PluginTracer`, `PluginSpan` | Open tracing spans around your plugin's work (`ctx.tracer.startSpan(...)`). | | `PluginLogger` | Structured logger (`ctx.logger.info/warn/error`). | | `PluginDatabaseClient` | Access the managed Postgres namespace declared for the plugin. | When you emit a metric with `metrics.write` (via `PluginMetricsClient`) or write a line with `log` (via `PluginLogger`), you can pass an optional `companyId` to scope that record to a company so it is cascade-deleted when the company is removed; omit it or pass `null` to keep the record at instance scope. +`ctx.tracer` lets you wrap a piece of work in a span so it shows up in the host's traces. You use it the same way as `ctx.logger`: call `ctx.tracer.startSpan("my-work", { attributes: { … } })`, set attributes or a status on the returned `PluginSpan`, and call `end()` when you're done. It's a deliberately minimal, OpenTelemetry-free contract — the SDK never pulls in `@opentelemetry/api` — and the host does the real recording. Two things to keep in mind: a span you open when tracing is off (or when there's no active host trace context) is a harmless no-op that records nothing, so you can always open one without guarding; and the host re-clamps every attribute at its trust boundary, so an attribute outside the host's allowlist never makes it onto a recorded span. If you need the default explicitly — say, to keep tests free of a real tracer — import the shared no-ops `NOOP_PLUGIN_TRACER` and `NOOP_PLUGIN_SPAN`. + Issue-domain helpers: `PluginIssueMutationActor`, `PluginIssueRelationSummary`, `PluginIssueCheckoutOwnership`, `PluginIssueWakeupResult`, `PluginIssueWakeupBatchResult`, `PluginIssueRunSummary`, `PluginIssueApprovalSummary`, `PluginIssueCostSummary`, `PluginBudgetIncidentSummary`, `PluginIssueInvocationBlockSummary`, `PluginIssueOrchestrationSummary`, `PluginIssueSubtreeOptions`, `PluginIssueAssigneeSummary`, `PluginIssueSubtree`, `IssueDocumentSummary`. Workspace metadata for `ctx.executionWorkspaces`: `PluginExecutionWorkspaceMetadata`. -Agent-session helpers: `AgentSession`, `AgentSessionEvent`, `AgentSessionSendResult`. +Agent-session helpers: `AgentSession`, `AgentSessionEvent`, `AgentSessionSendResult`. When you send a message with `ctx.agents.sessions.sendMessage(...)`, the `message` field on the `AgentSessionEvent` you receive with `eventType: "done"` is the canonical final user-facing assistant reply for that run — or `null` when the run produced no reply text. That is the field to relay back to whoever asked; you no longer need to reassemble it from the `"chunk"` events. An `eventType: "error"` event carries a run-status string in `message` instead. Workspace, event, and scope helpers: `PluginWorkspace`, `PluginEvent`, `EventFilter`, `ScopeKey`, `PluginJobContext`. @@ -138,6 +199,116 @@ Keys are stable identity. Renaming `agentKey`, `projectKey`, `routineKey`, or `s For the full manifest example and authoring rules, see the parent `doc/plugins/PLUGIN_AUTHORING_GUIDE.md`; the declaration types listed under [Manifest types](#manifest-types) above are the source of truth for what each managed entry accepts. +### Responding to interactions and approvals + +If your plugin bridges Paperclip to somewhere people already work — a chat app, a ticketing tool — the interesting half is the round trip *back*. An agent asks a question or requests a confirmation, someone answers it where they saw it, and the decision has to land on the board exactly as if they had clicked the button in Paperclip themselves. + +`ctx.issues` and `ctx.approvals` give you that, plus the ability to read the files people attached along the way. All of it is capability-gated, and the write halves all ask you to name the human the decision belongs to. + +A note on imports: the domain types these methods hand back — `IssueThreadInteraction`, `Approval`, `IssueAttachment` — come from `@paperclipai/shared`, not from the SDK's own export list. Import them from there. + +#### Reading and resolving issue-thread interactions + +Interactions are the decision cards an agent posts into an issue thread: suggested tasks, questions, confirmations. Your plugin could already create them; now it can read them back and resolve them. + +| Method | Signature | Capability | +|---|---|---| +| `ctx.issues.listInteractions` | `listInteractions(issueId, companyId)` → `Promise` | `issue.interactions.read` | +| `ctx.issues.respondInteraction` | `respondInteraction(issueId, interactionId, input, companyId)` → `Promise<{ interaction: IssueThreadInteraction; applied: boolean }>` | `issue.interactions.respond` | + +`input` is `{ action: "accept" | "reject"; actorUserId?: string; reason?: string | null }`. + +```ts +const { interaction, applied } = await ctx.issues.respondInteraction( + issueId, + interactionId, + { action: "accept", actorUserId, reason: null }, + companyId, +); + +if (!applied) { + ctx.logger.info("Interaction was already resolved", { status: interaction.status }); +} +``` + +Three things worth planning for: + +- **`actorUserId` is optional in the type but required in practice.** Resolving an interaction is a board-user action, so the host rejects the call when it is missing. It is not a value the host takes on trust either: it independently re-verifies that the user is an active human member of the issue's company at apply time, and rejects a member whose `membershipRole` is `viewer`, because the web app treats viewer access as read-only on exactly these routes. Your plugin can only ever act as an identity that could have taken the same action in Paperclip. +- **`applied` tells you whether *this* call did the work.** It is `true` when your call performed the resolution and `false` when the interaction had already converged to a resolved state. A duplicate button tap relayed from chat is therefore a safe no-op rather than an error, and the already-resolved interaction still comes back so you can render its final state. +- **Accepting can wake the agent.** When the resolved interaction's `continuationPolicy` is `wake_assignee` — or `wake_assignee_on_accept` and the decision was an accept — the host wakes the issue's assignee so the agent resumes, the same way it would after a decision made in the web app. + +#### Deciding approvals + +`ctx.approvals` is the new client for company approvals. + +| Method | Signature | Capability | +|---|---|---| +| `ctx.approvals.list` | `list({ companyId, status? })` → `Promise` | `approvals.read` | +| `ctx.approvals.get` | `get(approvalId, companyId)` → `Promise` | `approvals.read` | +| `ctx.approvals.decide` | `decide(approvalId, input, companyId)` → `Promise<{ approval: Approval; applied: boolean }>` | `approvals.respond` | + +`input` on `decide` is `{ action: "approve" | "reject"; actorUserId?: string; decisionNote?: string | null }`. The `actorUserId` rules and the meaning of `applied` are identical to `respondInteraction` above. + +Two behaviours to know about: + +- **Payloads are redacted on the way out.** `list` and `get` return approvals whose `payload` has been redacted host-side to match Paperclip's own approval read surface, so the bridge never hands your plugin secrets the web app itself hides from an approval reader. +- **A fresh decision wakes the requester.** When `applied` is `true` and the approval has a `requestedByAgentId`, the host wakes that agent so it resumes after the decision. + +#### Reading attachments + +| Method | Signature | Capability | +|---|---|---| +| `ctx.issues.listAttachments` | `listAttachments(issueId, companyId)` → `Promise` | `issue.attachments.read` | +| `ctx.issues.getAttachmentContent` | `getAttachmentContent(attachmentId, companyId, options?)` → `Promise` | `issue.attachments.read` | + +`options` is `{ maxBytes?: number | null }`. `PluginIssueAttachmentContent` is declared in the SDK's `types.ts` but is not currently re-exported as a name; it is reachable as the return type of `getAttachmentContent`, and it carries: + +| Field | Type | Declares | +|---|---|---| +| `attachmentId` | `string` | The attachment's id. | +| `contentType` | `string` | The stored MIME type. | +| `byteSize` | `number` | The number of bytes actually read. | +| `sha256` | `string` | Content hash of the stored object. | +| `originalFilename` | `string \| null` | The filename as uploaded, when one was recorded. | +| `contentBase64` | `string` | The raw bytes, base64-encoded. | + +```ts +const attachments = await ctx.issues.listAttachments(issueId, companyId); + +for (const attachment of attachments) { + const content = await ctx.issues.getAttachmentContent(attachment.id, companyId, { + maxBytes: 5 * 1024 * 1024, + }); + if (!content) continue; + + ctx.logger.info("Read attachment", { + originalFilename: content.originalFilename, + contentType: content.contentType, + byteSize: content.byteSize, + }); +} +``` + +`getAttachmentContent` hands you **bytes, not a URL** — the read goes through the capability-scoped host bridge, and the result has no URL surface of its own. A few consequences: + +- **Unknown and cross-company ids are indistinguishable.** Both return `null`, by design, so the bridge is not an existence oracle across companies. +- **`maxBytes` refuses rather than truncates.** Pass it and the host throws when the stored size is over the cap, and throws again if the object turns out to exceed the cap mid-stream. You never receive a partial file that looks whole. +- **Reads are audit-logged.** Each successful content read is recorded in the host activity log against the issue. + +#### The five new capability strings + +Declare these in your manifest's `capabilities` — the host's capability validator rejects a plugin that calls these methods without them: + +| Capability | Grants | +|---|---| +| `issue.interactions.read` | `ctx.issues.listInteractions` | +| `issue.interactions.respond` | `ctx.issues.respondInteraction` | +| `issue.attachments.read` | `ctx.issues.listAttachments` and `ctx.issues.getAttachmentContent` | +| `approvals.read` | `ctx.approvals.list` and `ctx.approvals.get` | +| `approvals.respond` | `ctx.approvals.decide` | + +All five are members of `PLUGIN_CAPABILITIES`, which the SDK re-exports if you want to check values at runtime. + ### External-object reference providers An **external-object reference provider** teaches Paperclip to recognise URLs that point at work living in another system — a GitHub PR, a Linear issue — and to keep a status-aware reference to that object alongside your issues. When an operator pastes a supported URL into issue content, the host detects it, asks your plugin to resolve the current remote status, and then refreshes it on a schedule so the reference renders as a live, status-aware chip across issue surfaces instead of a plain link. @@ -201,11 +372,34 @@ Helpers and constants: - `serializeMessage`, `parseMessage` - `JsonRpcParseError`, `JsonRpcCallError` +`PLUGIN_RPC_ERROR_CODES` includes `CROSS_TENANT_CONFIG`, the code the worker raises when a `configChanged` delivery would collapse a single-tenant worker onto a second company's configuration — see [Declaring multi-company support with `multiCompanyConfig`](#declaring-multi-company-support-with-multicompanyconfig). + +The worker-to-host method table `WorkerToHostMethods` gained the calls that back the clients above: `issues.listInteractions`, `issues.respondInteraction`, `issues.listAttachments`, `issues.getAttachmentContent`, `approvals.list`, `approvals.get`, and `approvals.decide`. + Protocol types: `JsonRpcId`, `JsonRpcRequest`, `JsonRpcSuccessResponse`, `JsonRpcError`, `JsonRpcErrorResponse`, `JsonRpcResponse`, `JsonRpcNotification`, `JsonRpcMessage`, `JsonRpcErrorCode`, `PluginRpcErrorCode`, plus the parameter shapes for each RPC method: `InitializeParams`, `InitializeResult`, `ConfigChangedParams`, `ValidateConfigParams`, `OnEventParams`, `RunJobParams`, `GetDataParams`, `PerformActionParams`, `ExecuteToolParams`, and the host method tables `HostToWorkerMethods` / `HostToWorkerMethodName` / `WorkerToHostMethods` / `WorkerToHostMethodName` / `HostToWorkerRequest` / `HostToWorkerResponse` / `WorkerToHostRequest` / `WorkerToHostResponse` / `WorkerToHostNotifications` / `WorkerToHostNotificationName`. External-object protocol shapes: `PluginExternalObjectUrlCandidate`, `PluginExternalObjectSourceContext`, `DetectExternalObjectsParams`, `PluginExternalObjectDetection`, `DetectExternalObjectsResult`, `PluginExternalObjectRecordSnapshot`, `ResolveExternalObjectParams`, `PluginExternalObjectResolvedSnapshot`, `PluginExternalObjectResolveResult`, `RefreshExternalObjectsParams`, `RefreshExternalObjectsResult`. See [External-object reference providers](#external-object-reference-providers) for the lifecycle that uses them. -Environment-driver protocol shapes: `PluginEnvironmentDiagnostic`, `PluginEnvironmentDriverBaseParams`, `PluginEnvironmentValidateConfigParams`, `PluginEnvironmentValidationResult`, `PluginEnvironmentProbeParams`, `PluginEnvironmentProbeResult`, `PluginEnvironmentLease`, `PluginEnvironmentAcquireLeaseParams`, `PluginEnvironmentResumeLeaseParams`, `PluginEnvironmentReleaseLeaseParams`, `PluginEnvironmentDestroyLeaseParams`, `PluginEnvironmentRealizeWorkspaceParams`, `PluginEnvironmentRealizeWorkspaceResult`, `PluginEnvironmentExecuteParams`, `PluginEnvironmentExecuteResult`, `PluginSyncFileMapping`, `PluginSyncOperation`, `PluginEnvironmentSyncInParams`, `PluginEnvironmentSyncOutParams`, `PluginEnvironmentSyncResult`, `PluginEnvironmentInteractiveSetupStatus`, `PluginEnvironmentInteractiveSetupConnectionType`, `PluginEnvironmentTemplateRefKind`, `PluginEnvironmentInteractiveSetupConnectionSummary`, `PluginEnvironmentInteractiveSetupConnectionPayload`, `PluginEnvironmentInteractiveSetupSession`, `PluginEnvironmentStartInteractiveSetupParams`, `PluginEnvironmentGetInteractiveSetupParams`, `PluginEnvironmentCaptureTemplateParams`, `PluginEnvironmentCaptureTemplateResult`, `PluginEnvironmentCancelInteractiveSetupParams`, `PluginEnvironmentCancelInteractiveSetupResult`, `PluginEnvironmentDeleteTemplateParams`, `PluginEnvironmentDeleteTemplateResult`, `PluginEnvironmentTemplateConfigBinding`. The `PluginSync*` and `PluginEnvironmentSync*` shapes back the optional sandbox file-sync hooks, and the interactive-setup and template-capture shapes back the setup hooks — both described below. +Environment-driver protocol shapes: `PluginEnvironmentDiagnostic`, `PluginEnvironmentDriverBaseParams`, `PluginEnvironmentValidateConfigParams`, `PluginEnvironmentValidationResult`, `PluginEnvironmentProbeParams`, `PluginEnvironmentProbeResult`, `PluginEnvironmentLease`, `PluginEnvironmentAcquireLeaseParams`, `PluginEnvironmentResumeLeaseParams`, `PluginEnvironmentReleaseLeaseParams`, `PluginEnvironmentDestroyLeaseParams`, `PluginEnvironmentRealizeWorkspaceParams`, `PluginEnvironmentRealizeWorkspaceResult`, `PluginEnvironmentExecuteParams`, `PluginEnvironmentExecuteResult`, `PluginSyncFileMapping`, `PluginPostUploadCommand`, `PluginSyncOperation`, `PluginEnvironmentSyncInParams`, `PluginEnvironmentSyncOutParams`, `PluginEnvironmentSyncResult`, `PluginEnvironmentInteractiveSetupStatus`, `PluginEnvironmentInteractiveSetupConnectionType`, `PluginEnvironmentTemplateRefKind`, `PluginEnvironmentInteractiveSetupConnectionSummary`, `PluginEnvironmentInteractiveSetupConnectionPayload`, `PluginEnvironmentInteractiveSetupSession`, `PluginEnvironmentStartInteractiveSetupParams`, `PluginEnvironmentGetInteractiveSetupParams`, `PluginEnvironmentCaptureTemplateParams`, `PluginEnvironmentCaptureTemplateResult`, `PluginEnvironmentCancelInteractiveSetupParams`, `PluginEnvironmentCancelInteractiveSetupResult`, `PluginEnvironmentDeleteTemplateParams`, `PluginEnvironmentDeleteTemplateResult`, `PluginEnvironmentTemplateConfigBinding`. The `PluginSync*` and `PluginEnvironmentSync*` shapes back the optional sandbox file-sync hooks, and the interactive-setup and template-capture shapes back the setup hooks — both described below. + +#### Streaming live command output + +When your driver runs a long-lived command through `execute`, you don't have to wait for it to finish to show its output. While an `execute` call is still running, call `ctx.execution.log(stream, chunk)` for each fresh piece of output the command produces, and the host forwards it to that call's log callback as it arrives — before the final `execute` result lands. The host knows which `execute` invocation the chunk belongs to because it correlates every chunk with the invocation currently running (by the host-issued invocation id carried on the message envelope), so you just report chunks as they come and the host does the matching. + +The one method is `log(stream: "stdout" | "stderr", chunk: string): void`. Pass `"stdout"` or `"stderr"` for `stream`, and the text of the new output as `chunk` — it is a string, not raw bytes. A few things make this safe to sprinkle in wherever your command loop reads output: + +- **The default is a no-op that never throws.** A driver that doesn't stream keeps its current behaviour, and you never need to guard the call — if there's nothing to stream to, `log` simply does nothing. +- **The host drops anything malformed.** A `stream` that isn't exactly `stdout` or `stderr` is dropped, and so is a `chunk` that is empty or too large. Bad input never reaches the log callback and never throws back at you. + +So a provider that wants live output just calls `ctx.execution.log("stdout", chunk)` (or `"stderr"`) each time it reads a new chunk from the running command, and a provider that doesn't stream can ignore the client entirely. + +#### Running a command outside the persistent session + +`PluginEnvironmentExecuteParams` carries an optional `bypassSession?: boolean`. It matters only if your driver opens a **persistent session** — one shell or connection it keeps alive across a lease's commands. + +The host sets `bypassSession: true` on a command that must run *before* the run's agent work — the workspace provision command is the canonical example. When you see the flag, run that command one-shot and leave the persistent session closed: don't open the session for it. The session then opens naturally on the first real in-run command instead, so provisioning never leaks into the session the agent works in. + +When the flag is absent or `false`, nothing changes — an ordinary in-run command opens and reuses the persistent session exactly as before. And a driver that doesn't keep a persistent session at all can ignore `bypassSession` entirely; there's no session to bypass. #### Sandbox file sync (optional) @@ -229,6 +423,50 @@ Each params object carries the current `PluginEnvironmentLease` plus an ordered Return a `PluginEnvironmentSyncResult`: an `operations` array echoing each `operationId` with its `filesTransferred` and `bytesTransferred` counts, for host-side observability. The contract is provider-agnostic — transfer a directory however you like, as long as the observable result matches the mappings. For the full authoring rules, see the parent `doc/plugins/SANDBOX_FILE_SYNC_HOOKS.md`. +##### Running commands after the files land + +Some files are not useful the moment they arrive. A dependency manifest needs an install step, an archive needs unpacking, a checked-out tree needs a fixup before the run can start. So a sync operation can now carry a short list of shell commands that run inside the sandbox *after* that operation's files have been placed. + +`PluginSyncOperation` gained one optional field for this: + +| Field | Type | Declares | +|---|---|---| +| `postUploadCommands?` | `PluginPostUploadCommand[]` | Ordered control commands run after this operation's files land, in array order, fail-fast. | + +Absent means "no commands", and an operation without the field is byte-identical to a pre-contract one — so if you never look at it, your driver behaves exactly as it did before. + +Each entry is a `PluginPostUploadCommand`, exported from `@paperclipai/plugin-sdk`: + +| Field | Type | Declares | +|---|---|---| +| `command` | `string` | The opaque, adapter-authored shell command to run after upload. Executed verbatim by the provider. | +| `cwd?` | `string` | Working directory for the command. When present, must be an absolute POSIX path confined under the operation's allowed sandbox target root. | +| `timeoutMs?` | `number` | Optional per-command timeout in milliseconds. | + +The ordering rules are simple, and you should follow them exactly: + +- **Files first, then commands.** Run the list once per operation, after *every* mapping in that operation has been placed — not after each individual file. +- **Array order, fail-fast.** The first non-zero exit or timeout aborts the operation. Don't skip ahead, and don't fall back to a silent partial success. + +###### The security contract you have to honour + +This is the part that matters most, because `command` is a string your driver hands to a shell. Paperclip keeps it safe by controlling where the string can come from, and your provider is the second half of that contract. + +**`command` is a Paperclip/adapter-authored control operation.** It may be supplied only by core or adapter code. It never comes from a server route, from issue or comment content, from project or workspace file content, from a provider-plugin callback, or from arbitrary adapter config. Any path embedded in it is built by adapter/core helpers out of already-confined paths and shell-quoted before it ever reaches you. + +**Treat the command as opaque.** You may execute it or reject it. You must **not** rewrite it, concatenate it, or append shell fragments of your own. In practice that means the working directory rides as a structured argument, never as a `cd … &&` prefix glued onto the front of the command string. + +**Re-validate `cwd` before you exec.** When `cwd` is present, confine it under the same sandbox root you use for file placement, with the same guards — `..`, absolute-escape, and symlink-escape all rejected fail-closed, *before* any command runs. When `cwd` is absent, default to the resolved sync remote/runtime root. Never fall back to a process default cwd. + +The two native providers show what that looks like in practice, and they are worth reading if you are writing your own: + +- **Daytona** (`packages/plugins/sandbox-providers/daytona/src/file-sync.ts`) re-runs its lexical `assertConfinedSandboxPath` check plus the realpath/symlink `assertSandboxPathsConfined` guard on a present `cwd`, then calls `sandbox.process.executeCommand(command.command, cwd, undefined, commandTimeoutSeconds)` — the command string is passed through untouched and the directory travels as its own argument. +- **Kubernetes** (`packages/plugins/sandbox-providers/kubernetes/src/file-sync.ts`) validates `cwd` lexically on the host first, then runs a fixed wrapper script in the pod that resolves the directory through realpath, pins it via `/proc/self/fd`, `cd`s into the pinned inode, and only then execs the command. Both the directory and the command ride as positional parameters (`$1` and `$2`) — the wrapper interpolates neither into its own script text, so the command runs byte-for-byte as authored. An escape attempt exits `42` before the command runs. + +Kubernetes executes these commands symmetrically with Daytona rather than dropping them silently: a native provider that ignored the field would fail open, which is exactly the outcome the contract exists to prevent. + +Both native providers run the commands on the way **in** only — inside `performSyncIn`'s per-operation loop, once that operation's mappings have landed. Neither runs them on sync-out. Use the same placement in your own driver. + #### Interactive setup and reusable templates (optional) An environment-driver plugin can go beyond leasing and executing: it can stand up a sandbox interactively so an operator can log in and get it just right, then capture that live sandbox as a reusable "custom image" template for future leases. These hooks are **optional** — only implement them if your driver supports interactive setup or capturing reusable environment templates. A driver that just leases and executes can ignore them entirely. @@ -255,6 +493,8 @@ For embedding the host side of the bridge in tests or custom integrations: Types: `HostServices`, `HostClientFactoryOptions`, `HostClientHandlers`. +`HostServices` groups the handlers you must supply by domain. Alongside the existing `issues` group — which now also needs `listInteractions`, `respondInteraction`, `listAttachments`, and `getAttachmentContent` — there is a new `approvals` group providing `list`, `get`, and `decide`. `getRequiredCapability` is the place to confirm which capability each call sits behind. + ### Bundling and dev server Helpers for the plugin's build pipeline: @@ -276,9 +516,20 @@ The SDK ships a first-class test harness so you do not have to spin up a real ho Types: `TestHarness`, `TestHarnessOptions`, `TestHarnessLogEntry`, `EnvironmentTestHarness`, `EnvironmentTestHarnessOptions`, `EnvironmentEventRecord`, `FakeEnvironmentDriverOptions`. +To test the interaction, approval, and attachment paths, seed them through `harness.seed({ ... })`, which accepts three new keys: + +| Key | Type | +|---|---| +| `issueInteractions` | `IssueThreadInteraction[]` | +| `issueAttachments` | `Array` | +| `approvals` | `Approval[]` | + +The harness deliberately mirrors the host's own write bar rather than waving it through: `respondInteraction` and `approvals.decide` both throw when `actorUserId` is missing, when it is not an active `user` member of the company, or when that member's `membershipRole` is `viewer`. Seed the members you want to act as through `harness.seed({ accessMembers: [...] })`, and a plugin test can't pass an attribution production would reject. The harness also honours `maxBytes` on `getAttachmentContent` and returns `applied: false` on replays, so idempotency is testable without a real host. + ### Re-exports - `z` — `zod` is re-exported so plugin authors do not need to add a separate dependency. Use it for `instanceConfigSchema` and tool `parametersSchema` declarations. +- `NOOP_PLUGIN_TRACER`, `NOOP_PLUGIN_SPAN` — the default no-op tracer and span (values, not types). Handy as a stand-in when you want the do-nothing default explicitly, e.g. in tests. - Constants from `@paperclipai/shared`: `PLUGIN_API_VERSION`, `PLUGIN_STATUSES`, `PLUGIN_CATEGORIES`, `PLUGIN_CAPABILITIES`, `PLUGIN_UI_SLOT_TYPES`, `PLUGIN_UI_SLOT_ENTITY_TYPES`, `PLUGIN_STATE_SCOPE_KINDS`, `PLUGIN_JOB_STATUSES`, `PLUGIN_JOB_RUN_STATUSES`, `PLUGIN_JOB_RUN_TRIGGERS`, `PLUGIN_WEBHOOK_DELIVERY_STATUSES`, `PLUGIN_EVENT_TYPES`, `PLUGIN_BRIDGE_ERROR_CODES`. --- diff --git a/docs/reference/skills.md b/docs/reference/skills.md index 690b4a1..c6e05b9 100644 --- a/docs/reference/skills.md +++ b/docs/reference/skills.md @@ -117,6 +117,24 @@ scripts/paperclip-upload-artifact.sh path/to/output.webm \ It reads the run's environment — `PAPERCLIP_API_URL`, `PAPERCLIP_API_KEY`, `PAPERCLIP_COMPANY_ID`, `PAPERCLIP_TASK_ID`, and `PAPERCLIP_RUN_ID` — then uploads the file as an issue attachment, creates an attachment-backed artifact work product (the default), and prints issue-safe markdown links the agent can drop into its final comment. The underlying upload route is the attachment endpoint documented under [Issues API → Attachments](./api/issues.md#attachments). +### Monitors and watchers in the bundled `paperclip` skill + +When an agent writes "I've set a watcher and I'll check back once CI finishes", you want that to be true — otherwise the issue quietly stalls while you wait for a wake-up that was never scheduled. The bundled `paperclip` skill has a **Monitors and Watchers** section whose whole job is to keep what an agent says in its comments matched to what is actually stored on the issue. + +The reason it needs saying: a run (heartbeat) is an ephemeral execution window, and nothing keeps watching after it exits. The only thing that can auto-resume an issue on its own is a persisted **issue monitor** — durable state on the issue itself (`monitorNextCheckAt`, `monitorScheduledBy`, plus an execution-policy `monitor` block carrying `kind`, `serviceName`, `externalRef`, `timeoutAt`, and `maxAttempts`). A server-side scheduler (`tickDueIssueMonitors`) polls for issues whose `monitorNextCheckAt` has passed and wakes the assignee agent again with `PAPERCLIP_WAKE_REASON=issue_monitor_due`. That is timer-based polling, not an event subscription: Paperclip is not notified the moment CI or another external check finishes, it just wakes the agent on a schedule so it can look again. + +A stored timestamp is necessary but not sufficient. For a monitor to fire, the issue must be assigned to an agent (`assigneeAgentId` set) with **no** user assignee (`assigneeUserId` is null), and it must be in `in_progress` or `in_review`. The on-demand trigger enforces the same conditions, so a monitor parked on a user-assigned, `backlog`, `blocked`, or closed issue never fires. + +So the skill holds agents to three rules: + +- **Only claim a watcher exists after actually scheduling one.** Describing one in a comment does not create it. The agent schedules it by setting `executionPolicy.monitor.nextCheckAt` (with `kind`/`serviceName`/`externalRef`/`timeoutAt`/`maxAttempts`) via `PATCH /api/issues/{id}`, then confirms the issue really reports a non-null `monitorNextCheckAt` under the eligibility conditions above. A check can be run on demand with `POST /api/issues/{id}/monitor/check-now`. +- **Describe it in checkable terms** — the monitor's kind, its next check time, and its attempt/timeout bounds, rather than a vague "a watcher will wake me". If the agent cannot name those, it has not scheduled one and must not imply that it has. +- **Never imply a live watcher on a task being marked `done`.** `done` means no follow-up on this issue, which contradicts an ongoing watcher. If real re-checking is still needed, the agent keeps the issue in `in_progress`/`in_review` with a scheduled monitor instead of closing it. + +None of this rests on the agent's good manners. The disposition guard rejects an agent move to `in_review` with `invalid_issue_disposition` unless a real review path exists — an interaction, an approval, a human reviewer, a typed execution participant, or an actually-scheduled monitor with a real `monitorNextCheckAt` — and the recovery classifier flags `in_review_without_action_path` for anything parked with no live wake path. + +Issue monitors are a separate mechanism from [task watchdogs](../guides/projects-workflow/task-watchdogs.md), which you attach to an issue yourself to double-check a subtree that has come to rest. + --- ## 2. Installation pipeline @@ -199,12 +217,14 @@ 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`, `summarize-status` | -| `optional` | Extra skills you opt into when you need them. | `agent-browser`, `release-announcement`, `design-critique`, `last30days` | +| `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`, `status-card-query` | +| `optional` | Extra skills you opt into when you need them. | `agent-browser`, `release-announcement`, `simplified-english`, `design-critique`, `last30days`, `prepare-mcp-integration` | 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. +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, and it is deliberately actions-first. Every summary opens with the 1–3 specific, concrete actions you need to take right now to unblock the work — "merge the install PR", "answer the org-accounts question", "approve the OAuth plan" — each saying what to do, why it is the thing holding up progress, and linking the issue inline. After the actions comes a brief paragraph or two of plain conversational status on where things stand, written for someone who has not memorised every issue id. If genuinely nothing needs you, you get one honest line saying so plus the next thing worth watching, rather than padding with filler actions. Like `wireframe`, its `defaultInstall` is `false`, so it arrives only when you pick it. + +Sitting alongside it is the `paperclip-operations`-category **`status-card-query`** skill (also recommended for the `general` and `manager` roles), which backs the experimental [Status Cards](../experimental/status-cards.md) feature. A status card starts as a sentence you write in plain English — "blocked or in-review launch work updated this week" — and this skill covers both halves of turning that into a live card. In its authoring mode an agent creates and refines its own cards through the public API, capped at 20 cards each and 4,000 characters of prompt. In its compilation mode the Summarizer translates that prose into bounded `CompanySearchQuery` objects — structured filters for status, priority, assignee, project, label, and recency rather than a keyword blob — and then writes the card's first summary back from the same assigned run. It is available only when `enableStatusCards` is turned on, and like the rest of the baseline kit its `defaultInstall` is `false`. Full details are on the [Status Card Query](./skills/bundled/paperclip-operations/status-card-query.md) page. **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. @@ -337,6 +357,8 @@ The server resolves each reference to its canonical key and persists exactly tha This route reconciles the agent's attachments to match exactly what you send: any skill in the list is attached, anything not in the list is detached. An empty list detaches everything. +Each entry may also be an object instead of a bare string — `{ "key": "...", "versionId": "..." }` — which pins the agent to a specific version of that skill. The server checks only that the version belongs to the skill you named; any non-null `versionId` additionally requires `enableBetaSkills` to be on. The UI is narrower than the API here: it only offers a version picker for the bundled `paperclip` skill's releases. See [Beta releases of the bundled `paperclip` skill](#beta-releases-of-the-bundled-paperclip-skill). + The response is an `AgentSkillSnapshot`: ```json @@ -474,7 +496,7 @@ Behaviour depends on the source: | `url` | No | None | Treat as a point-in-time snapshot. Re-import the URL to refresh. | | `local_path` (managed) | Live | None — files refresh on read | Stored under `/skills/{companyId}/`. Edited via `PATCH /skills/{id}/files`. | | `local_path` (project scan) | Live | Re-run the scan endpoint | Skills whose `SKILL.md` disappears are pruned automatically (`pruneMissingLocalPathSkills`). | -| `paperclip_bundled` | Pinned to the Paperclip release | Upgrade Paperclip itself | Re-imported on every list call; cannot be edited. | +| `paperclip_bundled` | Pinned to the Paperclip release | Upgrade Paperclip itself | Re-imported on every list call; cannot be edited. The bundled `paperclip` skill additionally carries beta releases an agent can pin — see [below](#beta-releases-of-the-bundled-paperclip-skill). | | `catalog` | Pinned to the shipped catalog version (`metadata.originHash`) | `GET /skills/{id}/update-status` → `POST /skills/{id}/install-update`; `POST /skills/{id}/reset` to restore the origin | App-shipped catalog skills. Updates compare against the version the app ships. See [App-shipped catalog](#3-app-shipped-catalog). | ### `update-status` response @@ -496,6 +518,46 @@ For non-GitHub sources, `supported: false` is returned with a reason. Calling `i `install-update` re-runs the import for the same source URL, finds the matching skill (by canonical key or slug), upserts the row in place, and returns the refreshed record. The skill's `id` does not change — anything attached by ID continues to work without re-syncing. +### Beta releases of the bundled `paperclip` skill + +The bundled `paperclip` skill (canonical key `paperclipai/paperclip/paperclip`) also carries **releases**: frozen snapshots an agent can be pinned to instead of the live default. For why you would pin one and how the picker behaves, read [Pinning a beta release of the Paperclip skill](../guides/org/skills.md#pinning-a-beta-release-of-the-paperclip-skill) in the guide. This section is the data and wire shape. + +**The flag.** `enableBetaSkills` is an instance experimental setting, catalogued as **Beta skills** — *"Allow agents to pin beta releases of the Paperclip core skill."* Its tier is `preference`, meaning a tenant-controlled setting the Paperclip Cloud harness never manages, in contrast to the `managed`-tier flags that can arrive locked. Both `cloudDefault` and `selfHostedDefault` are `false`, so it is off on every instance until an operator turns it on. + +**Where the snapshots come from.** The server reads `skills-releases/paperclip/releases.json` and seeds one skill version per manifest entry into every company that has the bundled `paperclip` skill. Each entry carries `id`, `releaseName`, `releasedAt`, `notes`, and `dir` — the folder holding that snapshot's files, resolved relative to the registry root. Seeding never moves the skill's current version: the live default is left alone, and a release only takes effect on agents that pin it. + +Two releases ship today: + +| `id` | `releaseName` | `releasedAt` | +|---|---|---| +| `v0` | `V0 — Original` | `2026-07-15T07:52:54-05:00` | +| `v7-roster` | `V7 — Roster champion` | `2026-07-21` | + +Both are frozen historical snapshots. `v7-roster` predates three later edits to `SKILL.md`, `references/api-reference.md`, and `references/company-skills.md`; the live default keeps that newer guidance. + +Re-seeding is byte-checked. If a company already holds a version for a release id whose stored inventory no longer hashes to the registry snapshot, the seed fails with `Bundled skill release does not match its seeded snapshot.` rather than rewriting a release that agents may already be pinned to. + +**Storage.** `company_skill_versions` rows carry three release columns — `release_id`, `release_name`, and `released_at`, surfaced on `CompanySkillVersion` as `releaseId`, `releaseName`, and `releasedAt`. All three are null for ordinary versions. A partial unique index, `company_skill_versions_skill_release_idx` on (`company_skill_id`, `release_id`) where `release_id is not null`, guarantees one skill has at most one version per release id. + +**Listing releases.** `GET /api/companies/{companyId}/skills/{skillId}/versions` returns the skill's versions; the releases are the rows with a non-null `releaseId`. + +**Pinning.** `POST /api/agents/{agentId}/skills/sync` takes either shape per entry: + +```json +{ + "desiredSkills": [ + "code-review", + { "key": "paperclipai/paperclip/paperclip", "versionId": "" } + ] +} +``` + +The bare string form and `"versionId": null` both mean *no pin* — the live default. The returned `AgentSkillSnapshot` reports pins on `desiredSkillEntries` (a list of `{ key, versionId }`) alongside the flat `desiredSkills` list. + +If any entry carries a non-null `versionId` while `enableBetaSkills` is off, the request fails with `400` and the message `Beta skill version pins require the Beta skills experimental setting to be enabled.` + +**With the flag off.** Existing pins are kept but not applied. Both the heartbeat and agent config resolution build their version selection map with `versionPinsEnabled: false`, which maps every key to `null`, so every agent resolves to the live skill. Turning the flag back on restores the saved pins. + --- ## 9. Troubleshooting: why a skill isn't loading @@ -542,6 +604,12 @@ Walk down this list in order. The first match is usually the problem. - It's bundled-required. `paperclipai/paperclip/*` skills with `metadata.sourceKind: paperclip_bundled` are unioned into every resolved `desiredSkills` set by the server. Removing them from the request has no effect. +### "I pinned a beta release but the agent still runs the default" + +- Check **Beta skills** on **Settings → Instance settings → Experimental**. With `enableBetaSkills` off, saved pins are ignored rather than deleted, and every agent resolves to the live skill. +- A pin only applies while its skill is in the agent's desired set. Detaching the skill drops the pin along with it. +- Confirm the pin actually persisted: with the flag **on**, `GET /api/agents/{agentId}/skills` reports it under `desiredSkillEntries`. With the flag off that response reports `versionId: null` even though the pin is still stored on the agent — so use it to check a pin, not to conclude one was lost. + ### "I deleted a skill and it came back" - It's a bundled skill. The server re-imports `skills/` from disk on every company list call. To suppress a bundled skill, remove it from the Paperclip release or run a forked build. diff --git a/docs/reference/skills/bundled.md b/docs/reference/skills/bundled.md index da86c9a..9d48ecc 100644 --- a/docs/reference/skills/bundled.md +++ b/docs/reference/skills/bundled.md @@ -15,6 +15,7 @@ For the catalog mechanics, start with the [Skills reference](../skills.md#3-app- ## Paperclip operations - [Issue Triage](./bundled/paperclip-operations/issue-triage.md) — classify stale, blocked, in-review, or stalled Paperclip issues and choose the next action. +- [Status Card Query](./bundled/paperclip-operations/status-card-query.md) — author Paperclip status cards, and compile a card's prose prompt into bounded search queries and its first summary. - [Task Planning](./bundled/paperclip-operations/task-planning.md) — turn approved plans into Paperclip issues with ownership, dependencies, and acceptance criteria. ## Product diff --git a/docs/reference/skills/bundled/paperclip-operations/status-card-query.md b/docs/reference/skills/bundled/paperclip-operations/status-card-query.md new file mode 100644 index 0000000..e86d87f --- /dev/null +++ b/docs/reference/skills/bundled/paperclip-operations/status-card-query.md @@ -0,0 +1,158 @@ +# Status Card Query + +> Create and maintain agent-authored Paperclip status cards, or compile a prose interest prompt into bounded CompanySearchQuery objects and write the first summary from the assigned Summarizer run. + +A status card lets you describe a slice of work in plain English — "blocked or in-review launch work updated this week" — and get a card that keeps itself current. This skill teaches an agent both halves of that: how to author a card in the first place, and how to turn its prose prompt into the bounded search queries that decide which issues the card actually covers, then write the first summary. + +This is a **bundled** catalog skill — part of the bundled baseline kit. For how to install, audit, update, assign, and reset catalog skills, see the [Skills reference](../../../skills.md#3-app-shipped-catalog). + +Status cards are an experimental feature. The authoring routes are available only when `enableStatusCards` is turned on — see [Status Cards](../../../../experimental/status-cards.md) for the feature itself and the [Status Cards API](../../../api/status-cards.md) for the full route reference. + +## When to use + +- An agent wants a standing card for the work it watches, and needs to create one through the public API. +- An owned card's prompt is too vague or too broad, and you want to refine it or ask for a refresh. +- You are the Summarizer and a generation issue has been assigned to you naming a `statusCardId`. +- A later generation issue asks you to update an existing card's summary. + +### When not to use + +- You are authoring a card. Don't call `/query` or `/summary` — those write-back routes belong to the assigned Summarizer run. +- The card was authored by someone else. An agent may manage only the cards it authored. +- The prompt names projects or labels you cannot resolve to ids. Report the ambiguity rather than inventing ids. + +## Catalog metadata + +| Field | Value | +|---|---| +| Catalog id | `paperclipai:bundled:paperclip-operations:status-card-query` | +| Canonical key | `paperclipai/bundled/paperclip-operations/status-card-query` | +| Catalog path | `catalog/bundled/paperclip-operations/status-card-query` | +| Kind | `bundled` | +| Category | `paperclip-operations` | +| Slug | `status-card-query` | +| Entrypoint | `SKILL.md` | +| Trust level | `markdown_only` | +| Compatibility | `compatible` | +| Default install | `false` | +| Recommended roles | `general`, `manager` | +| Requires | — | +| Tags | `paperclip`, `status`, `search`, `reporting`, `operations` | +| Files | 1 | +| Content hash | `sha256:2b6e53bf8491f027afdbd6961ad84f677cfd385a0a6a73616ea84111ca8886ed` | +| Package | `@paperclipai/skills-catalog@0.3.1` | + +## File inventory + +| Path | Kind | Bytes | +|---|---|---:| +| `SKILL.md` | `skill` | 6,368 | + +## Mode 1 — authoring a card + +Agent-authored cards need the `tasks:assign` permission and stay company-scoped. Three limits keep them tidy: + +- An agent may manage only the cards it authored. +- An agent may author at most 20 cards. +- An `interestPrompt` may be at most 4,000 characters. + +The skill normalises the run-provided API base first, so the same snippet works whether `PAPERCLIP_API_URL` ends in `/api` or not, then creates the card: + +```bash +PAPERCLIP_API_BASE="${PAPERCLIP_API_URL%/}" +PAPERCLIP_API_BASE="${PAPERCLIP_API_BASE%/api}" + +curl -sS -X POST \ + -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"interestPrompt":"Blocked or in-review launch work updated this week"}' \ + "$PAPERCLIP_API_BASE/api/companies/$PAPERCLIP_COMPANY_ID/status-cards" +``` + +Creation returns `201` and queues compilation on its own — you don't have to schedule anything. Keep the card id from the response. From there, `PATCH /api/status-cards/{statusCardId}` sharpens the prompt on a card you own, and `POST /api/status-cards/{statusCardId}/refresh` with `{"full": false}` asks for a refresh. + +## Mode 2 — compiling the prompt + +As the Summarizer, your job is to turn the card's prose prompt into an array of Paperclip company-search queries. The array has **union semantics**: an issue that matches any query belongs to the card. So prefer a single narrow query, and add a second only when the prompt genuinely describes two distinct populations. + +### CompanySearchQuery fields + +| Field | What it takes | +|---|---| +| `q` | Optional free text, for concepts no structured filter covers. | +| `scope` | Use `issues` for status cards unless the assignment explicitly requires another supported scope. | +| `status` | Issue-status array. | +| `priority` | Issue-priority array. | +| `assigneeAgentId` / `assigneeUserId` | A resolved assignee id. | +| `projectId` | One resolved project UUID. | +| `labelId` | One resolved label UUID. | +| `updatedWithin` | A bounded duration such as `24h`, `7d`, `4w`, or `3m`. | +| `sort` | `relevance`, `updated`, `created`, or `priority`. | +| `limit` | 1–50. Cap status-card queries at the smallest useful value — normally 20, never above 50. | +| `offset` | Normally 0. | + +Resolve project and label names to ids before you write the query; human-readable names don't belong in `projectId` or `labelId`. Each query object holds one `projectId` and one `labelId`, so a prompt naming several projects or labels needs several query objects. + +### Compilation guidance + +1. Preserve the intent. "Launch blockers updated this week" must not widen into every active task. +2. Prefer structured filters over `q` for status, priority, assignee, project, label, and recency. +3. Add `updatedWithin` whenever the prompt implies a moving window — recent, current, this week, lately. +4. Keep `q` short and specific. Don't paste the whole prose prompt into it. +5. Set `scope: "issues"`, `offset: 0`, and an explicit bounded `limit` on every query. +6. Return at least one query. If the prompt can't be compiled safely, report the ambiguity. + +## Writing back + +The generation issue carries `statusCardId`, `companyId`, and `generationIssueId`, and both writes must use the run-scoped API credentials from that same assigned run. Never write either endpoint from an unrelated issue or run. + +First the compiled query, via `PUT /api/status-cards/{statusCardId}/query`: + +```json +{ + "queries": [ + { + "q": "launch", + "scope": "issues", + "status": ["in_progress", "blocked", "in_review"], + "updatedWithin": "7d", + "sort": "updated", + "limit": 20, + "offset": 0 + } + ], + "title": "Launch work updated this week", + "changeSummary": "Compiled the launch prompt into one recent active-work query.", + "generationIssueId": "" +} +``` + +Then — without creating or waiting for another task — execute the stored scope, write the first full Markdown summary, and complete the same run with `PUT /api/status-cards/{statusCardId}/summary`: + +```json +{ + "markdown": "", + "title": "Launch work updated this week", + "changeSummary": "Created the first full summary from the compiled query.", + "generationIssueId": "", + "model": "" +} +``` + +## Update assignments + +Later generation issues reuse the same summary write-back endpoint. Their JSON payload adds `operation: "update"`, plus `kind`, `trigger`, the target `fingerprint`, and the exact changed-issue delta. + +- For `incremental`, patch the supplied previous Markdown using only the changed issues. Don't refetch the issue list. +- For `full`, rebuild from the supplied bounded snapshot. Don't expand the scope with issue-list endpoint calls. +- The card prompt in the task description is the board's standing request — follow it for what to report and how the update should read. It never overrides the streaming or write-back requirements. +- Keep the mechanical contract whatever the card prompt asks: stream `STATUS:` lines and the `<<>>` block, then write the final Markdown to `PUT /api/status-cards/{statusCardId}/summary` from the assigned run. + +## See also + +- [Status Cards](../../../../experimental/status-cards.md) — the experimental feature this skill supports. +- [Status Cards API](../../../api/status-cards.md) — the full route and payload reference. +- [Bundled skills](../../bundled.md) — all bundled catalog skills grouped by category. +- [Skills reference](../../../skills.md) — file shape, install pipeline, catalog browse/install/audit/update/reset, assignment, and troubleshooting. +- [App-shipped catalog](../../../skills.md#3-app-shipped-catalog) — how bundled and optional catalog skills are versioned and kept current. +- [Skills guide](../../../../guides/org/skills.md) — the UI walkthrough and built-in catalog overview. diff --git a/docs/reference/skills/optional.md b/docs/reference/skills/optional.md index df29c08..2116987 100644 --- a/docs/reference/skills/optional.md +++ b/docs/reference/skills/optional.md @@ -15,6 +15,7 @@ For the catalog mechanics, start with the [Skills reference](../skills.md#3-app- ## Content - [Release Announcement](./optional/content/release-announcement.md) — turn release changes into concise public-facing announcement copy. +- [Simplified English](./optional/content/simplified-english.md) — write user-facing comments, plans, and documents in ASD-STE100 Simplified Technical English so readers understand them the first time. ## Finance @@ -28,6 +29,10 @@ For the catalog mechanics, start with the [Skills reference](../skills.md#3-app- - [Last30Days](./optional/research/last30days.md) — research recent public discussion about a topic across Reddit, X, YouTube, and the web. +## Software Development + +- [Prepare MCP Integration](./optional/software-development/prepare-mcp-integration.md) — research a new MCP or vendor integration, gate it on exact-revision human approval, then ship one governed Paperclip connector per approved connection. + ## See also - [Skills Reference](../skills.md) — file shape, install pipeline, catalog browse/install/audit/update/reset, assignment, and troubleshooting. diff --git a/docs/reference/skills/optional/content/simplified-english.md b/docs/reference/skills/optional/content/simplified-english.md new file mode 100644 index 0000000..b4bd572 --- /dev/null +++ b/docs/reference/skills/optional/content/simplified-english.md @@ -0,0 +1,96 @@ +# Simplified English + +> Write user-facing comments, plans, and documents in ASD-STE100 Simplified Technical English — short, unambiguous sentences with approved words and one meaning each — so readers understand them the first time. + +When an agent writes something a person will actually read — a comment on a task, a plan, a handover document — you want that person to understand it on the first pass. This skill holds the agent to ASD-STE100 Simplified Technical English: short sentences, one instruction at a time, active voice, and a controlled vocabulary where each word carries a single meaning. + +This is an **optional** catalog skill — opt-in (install when you need it). For how to install, audit, update, assign, and reset catalog skills, see the [Skills reference](../../../skills.md#3-app-shipped-catalog). + +## When to use + +- Your readers include people who work in a second language, and ambiguity costs them time. +- An agent writes plans, comments, or documents that humans act on directly. +- You want a consistent house style for user-facing writing across a team of agents. + +### When not to use + +- The output is code, config, or machine-read data rather than prose for a reader. +- The surface needs marketing voice or narrative flourish — a controlled vocabulary works against that. + +## Catalog metadata + +| Field | Value | +|---|---| +| Catalog id | `paperclipai:optional:content:simplified-english` | +| Canonical key | `paperclipai/optional/content/simplified-english` | +| Catalog path | `catalog/optional/content/simplified-english` | +| Kind | `optional` | +| Category | `content` | +| Slug | `simplified-english` | +| Entrypoint | `SKILL.md` | +| Trust level | `markdown_only` | +| Compatibility | `compatible` | +| Default install | `false` | +| Recommended roles | `engineer`, `product`, `writer`, `devrel` | +| Requires | — | +| Tags | `writing`, `communication`, `clarity`, `style` | +| Files | 1 | +| Content hash | `sha256:642321f40d51e0125df05c6fbace49aeb07ec30392058756909c259c9c5eac6a` | +| Package | `@paperclipai/skills-catalog@0.3.1` | + +## File inventory + +| Path | Kind | Bytes | +|---|---|---:| +| `SKILL.md` | `skill` | 1,578 | + +## Full skill definition + +### Skill frontmatter + +- **name:** simplified-english +- **description:** Write user-facing comments, plans, and documents in ASD-STE100 Simplified Technical English — short, unambiguous sentences with approved words and one meaning each — so readers understand them the first time. +- **key:** paperclipai/optional/content/simplified-english +- **recommendedForRoles:** + - engineer + - product + - writer + - devrel +- **tags:** + - writing + - communication + - clarity + - style + +### Skill instructions +# Simplified English + +For user-facing comments, plans, and documents, write using only ASD-STE100 Simplified Technical English. + +## Core rules + +- Use short sentences (procedures ≤ 20 words, descriptions ≤ 25 words). +- Give one instruction per sentence. +- Use approved words with one meaning each; avoid synonyms and jargon. +- Use the active voice and the present tense. +- Use articles ("the", "a") and do not drop words to save space. +- Write positive instructions; avoid negative or vague qualifiers. +- Keep paragraphs to one topic. + +## Approved words + +The approved words are the ASD-STE100 controlled vocabulary — the Dictionary in the current ASD-STE100 specification, plus the technical names and technical verbs that your subject needs. When a word is not approved, use the simplest common word that has one meaning. Prefer these house choices: + +- "start" / "stop" (not "initiate", "commence", "terminate", "kill") +- "make" (not "implement", "leverage", "utilize") +- "before" / "after" (not "prior to", "subsequent to") +- "about" (not "regarding", "in relation to") +- "help" (not "facilitate") +- "use" (not "utilize", "employ") + +## See also + +- [Optional skills](../../optional.md) — all optional catalog skills grouped by category. +- [Skills reference](../../../skills.md) — file shape, install pipeline, catalog browse/install/audit/update/reset, assignment, and troubleshooting. +- [App-shipped catalog](../../../skills.md#3-app-shipped-catalog) — how bundled and optional catalog skills are versioned and kept current. +- [Skills guide](../../../../guides/org/skills.md) — the UI walkthrough and built-in catalog overview. diff --git a/docs/reference/skills/optional/software-development/prepare-mcp-integration.md b/docs/reference/skills/optional/software-development/prepare-mcp-integration.md new file mode 100644 index 0000000..ed5131c --- /dev/null +++ b/docs/reference/skills/optional/software-development/prepare-mcp-integration.md @@ -0,0 +1,66 @@ +# Prepare MCP Integration + +> Take a new MCP or vendor integration from an input link or brief all the way to delivery — cited research first, an explicit human approval gate in the middle, then one governed Paperclip connector per approved connection. + +A two-phase playbook for researching and shipping MCP/vendor integrations in a way people can review and sign off on. It turns a link or a short vendor brief into a cited research pull request, waits for a human to accept the exact revision, and only then writes the connector code — one Paperclip pull request per approved connection. + +This is an **optional** catalog skill — opt-in (install when you need it). For how to install, audit, update, assign, and reset catalog skills, see the [Skills reference](../../../skills.md#3-app-shipped-catalog). + +## When to use + +- You're researching a new MCP server or vendor integration and want the findings written up with sources before anyone commits to building. +- An engineer, product manager, or researcher needs a paper trail — what the connector would do, which connections it covers, and who approved it — before code lands. +- You want new connector work delivered under the playbooks and approval gates, not stitched together ad hoc. + +### When not to use + +- You're doing ad hoc connector coding that intentionally bypasses the playbooks — this skill exists to prevent that. +- There's no integration to research or deliver yet, just an open-ended question about a vendor. + +## The two-phase flow + +The skill keeps research and implementation strictly separate, with a human decision between them. + +### Phase A — Research (no implementation) + +Starting from the input link or vendor brief, Phase A produces a reviewable, cited **research pull request** in the `paperclip-content` repository. It gathers what the integration would involve and writes it up with its sources, so a person can read the findings and decide. No connector code is written in this phase. + +### Human-approval gate + +Phase B does not start on its own. A human has to explicitly accept **the exact research revision** — bound to a single pull-request head SHA — along with the specific set of connections to build. Approving one revision does not approve a later one; if the research changes, it needs approval again. + +### Phase B — Implementation + +Only after that approval does the skill implement the connector in the Paperclip app. It opens **one Paperclip app pull request per approved connection**, so each connection is reviewed and shipped on its own. + +## The one-PR-per-connection rule + +Every approved connection gets its own Paperclip app pull request. Connections are not bundled together — this keeps each one reviewable in isolation and makes it clear exactly what was approved and shipped for each. + +## The secrets rule + +Vendor credentials always live in approved secret storage — never anywhere else. They must not appear in briefs, catalog files, issue text, plans, logs, branch names, commits, or pull requests. If a credential is needed, it is referenced from secret storage, not pasted into any artifact the skill produces. + +## Catalog metadata + +| Field | Value | +|---|---| +| Catalog id | `paperclipai:optional:software-development:prepare-mcp-integration` | +| Canonical key | `paperclipai/optional/software-development/prepare-mcp-integration` | +| Catalog path | `catalog/optional/software-development/prepare-mcp-integration` | +| Kind | `optional` | +| Category | `software-development` | +| Slug | `prepare-mcp-integration` | +| Trust level | `markdown_only` | +| Compatibility | `compatible` | +| Default install | `false` | +| Recommended roles | `engineer`, `product-manager`, `researcher` | +| Requires | `git`, `gh`, `curl` | +| Tags | `mcp`, `integrations`, `connectors`, `research`, `github`, `human-approval` | + +## See also + +- [Optional skills](../../optional.md) — all optional catalog skills grouped by category. +- [Skills reference](../../../skills.md) — file shape, install pipeline, catalog browse/install/audit/update/reset, assignment, and troubleshooting. +- [App-shipped catalog](../../../skills.md#3-app-shipped-catalog) — how bundled and optional catalog skills are versioned and kept current. +- [Skills guide](../../../../guides/org/skills.md) — the UI walkthrough and built-in catalog overview. diff --git a/docs/user-guides/screenshots/SCREENSHOTS_PLAN_v2026.817.0.md b/docs/user-guides/screenshots/SCREENSHOTS_PLAN_v2026.817.0.md new file mode 100644 index 0000000..833d3bb --- /dev/null +++ b/docs/user-guides/screenshots/SCREENSHOTS_PLAN_v2026.817.0.md @@ -0,0 +1,222 @@ +# Screenshot plan — v2026.817.0 + +Working notes for the release screenshot overhaul. Baseline `903bd157c` (v2026.720.0, +2026-07-20) → release `213dabab4` (2026-08-17). 375 parent commits, 439 changed +`ui/src` files, +41,580 / −5,295 lines. + +> **Capture against `213dabab4`, not `master`.** The release commit lives on +> `upstream/candidate/release-2026.817.0` and is *not* an ancestor of the parent's +> `master`; local `master` (2026-08-13) is four days *older* than it and carries 93 +> commits the release does not. Capturing against `master` would both miss release +> UI and leak post-tag UI. No git tag points at the commit — `git describe` gives +> `beta/v2026.811.0-beta.0-4-g213dabab4`. + +--- + +## A. Existing shots — recapture (310 registry entries) + +228 of 310 are flagged in `SCREENSHOTS_PENDING.md`. Given the staleness, the whole +registry is recaptured with `--all`. Three groups need work *beyond* a recapture, +because a blind `--all` run would produce confidently-wrong images: + +### A1. Dangling `depends_on` — page deleted upstream + +| Target(s) | Current `depends_on` | Reality at `213dabab4` | +|---|---|---| +| `activity/activity-log-full`, `activity/activity-filters`, `activity/activity-filtered-by-agent` | `ui/src/pages/Activity.tsx` | **Deleted.** `/activity` now renders `pages/audit/CompanyActivity.tsx` (+ `pages/audit/AuditFeed.tsx`). `/audit` redirects to `/activity?mode=agents`. | +| `experimental/cloud-upstream` | `ui/src/pages/CloudUpstream.tsx` | **Deleted.** `/company/settings/cloud-upstream` now redirects to `/company/export`. | + +These are the only two `depends_on` paths in `routes.mjs` that no longer exist at the +release commit (checked against `git ls-tree -r 213dabab4`). + +- Activity: repoint `depends_on`, keep the route, recapture. The page genuinely + still exists and `docs/guides/day-to-day/activity-log.md` embeds three of its shots. +- Cloud upstream: the shot is dead. `docs/experimental/cloud-sync.md:11` still embeds + it. Retarget to the surface that replaced it (`/company/export`) rather than + shooting a redirect and filing it under the old name. + +### A2. Routes that are now redirects + +`/instance/settings/*` (8 targets: profile, general, access, heartbeats, experimental, +plugins ×5, adapters ×4, environments) still *work* — `LegacySettingsRedirect` bounces +them to the canonical `/{prefix}/company/settings/instance/*`. Screenshots are +unaffected (no URL chrome in a Playwright shot), but the redirect renders +`` first, which is exactly the blank-page failure mode +`capture.mjs` retries around. Repoint to canonical paths: cheaper and non-flaky. + +Also: `company/access` → `/{prefix}/company/settings/access` now hits +`CompanyAccessLegacyRoute`; members moved to `/company/settings/members`. + +### A3. Stale seed flag + +`seed.mjs:686` sets `enableCloudSync: true`. That flag no longer exists in +`instanceExperimentalSettingsSchema`. The patch validator is `.strip()`, not +`.strict()`, so the call still succeeds and the key is silently dropped — harmless, +but remove it. `enableDecisions` and `enableStatusCards` must be added. + +--- + +## B. Net-new screens — no shot exists + +Ordered by doc need. The three pages below carry **zero images today**. + +### B1. Decisions — `docs/guides/day-to-day/decisions.md` (115 lines, 0 images) + +Routes: `/{prefix}/decisions` (`WhatNeedsMe`), `/{prefix}/decisions/queues/:key` +(`DecisionQueuePage`), `/{prefix}/decisions/training`. + +| New target | Screen | Doc section | +|---|---|---| +| `decisions/queue-overview` | Ranked feed | Read the queue | +| `decisions/decision-card` | Agent-proposed decision, options + input fields | Answer a decision an agent proposed | +| `decisions/decision-card-stale-target` | "1 target changed since this was proposed" | (same, safety bullet) | +| `decisions/toolbar-filters` | Type/Severity/Project/Workspace + group + sort | Focus on the next decision | +| `decisions/named-queue` | A queue lane (PRs / Plans / Questions) | Group work into named queues | +| `decisions/triage-chips` | decide-by today/week/whenever + snooze | Say when something needs you | +| `decisions/decided-expired-sections` | Collapsed history | (Answer a decision, last para) | +| `decisions/empty-state` | "You're all caught up" | When the queue is empty | + +Components: `DecisionCard`, `DecisionsToolbar`, `DecisionQueueRail`, `DecisionShelf`, +`DecisionTriageStrip`, `DecisionDateChips`, `DecisionResolver`. + +### B2. Status cards — `docs/experimental/status-cards.md` (136 lines, 0 images) + +Routes: `/{prefix}/status`, `/{prefix}/status/:cardId`. Gated by +`enableStatusCards`; also needs the built-in **Summarizer** agent provisioned. + +| New target | Screen | Doc section | +|---|---|---| +| `status-cards/board` | Tile grid + header totals | Reading the board | +| `status-cards/new-card-dialog` | "What do you want to keep an eye on?" | Creating your first card | +| `status-cards/detail-summary` | Drawer, Summary tab + revision picker | Reading the board | +| `status-cards/detail-settings` | Update policy + Advanced triggers | How updating works | +| `status-cards/detail-watched-issues` | Live query matches | Reading the board | +| `status-cards/detail-history` | Per-update tokens/cost/model | What it costs | +| `status-cards/archived-row` | Show archived → View / Restore | Archiving and restoring | + +Components: `pages/StatusCards/{index,StatusCardTile,StatusCardDetailDrawer, +CreateStatusCardDialog,StatusCardSettingsForm,ArchivedStatusCardRow}.tsx`. + +### B3. Secret proposals — `docs/administration/secret-scopes.md` (has 4 images, none of proposals) + +Route: `/{prefix}/company/settings/secrets`, **Proposals** tab (local `activeTab` +state → needs a `steps` click, not a URL). + +| New target | Screen | +|---|---| +| `secrets/proposals-tab` | Pending proposals list with the amber count badge | +| `secrets/proposal-review` | One proposal expanded with Approve / Reject | +| `secrets/proposal-resolved` | An approved + a rejected proposal | + +Components: `pages/secrets/ProposalsTab.tsx`, `proposal-review.tsx`, +`components/AgentSecretAccessEditor.tsx`. + +### B4. Chat-style tasks — `docs/experimental/task-chat.md` (135 lines, 0 images) + +Gated by `enableTaskChatRedesign`. Renders on the ordinary issue detail route +(`/{prefix}/issues/{issueId}`) — no new route. + +| New target | Screen | Doc section | +|---|---|---| +| `task-chat/thread` | Bubbles, author headers, mode chips | Reading the thread | +| `task-chat/folded-turn` | `✓ Worked · 38s · 3 tools · +34 −3` | Turns | +| `task-chat/tool-call-diff` | Expanded tool row + diff panel | Tool calls and diffs | +| `task-chat/composer-modes` | Agent · Plan · Ask picker | The composer | +| `task-chat/side-pane-plan` | Properties · Plan · Artifacts pane | The side pane | + +> **Open risk.** Rich turn content (thinking blocks, tool calls, diffs, live status +> line) needs a real adapter transcript. The screenshot instance has no LLM provider, +> and the `process` demo adapter emits plain stdout — it cannot produce these. The +> parent ships a fixture-driven dev harness at `/dev/task-chat-lab` +> (`pages/TaskChatLab.tsx` + `task-chat-fixtures.ts`), DEV-only and flag-gated, and +> the pipeline does serve the UI through vite dev middleware. Decision recorded in +> Phase 2: shoot the base thread from real seeded data, and only fall back to the lab +> for states that are genuinely unreachable otherwise — a docs screenshot taken from +> a fixture harness must still be a truthful picture of the shipped UI. + +### B5. Catalog skills — 3 new reference pages + +`reference/skills/bundled/paperclip-operations/status-card-query.md`, +`reference/skills/optional/content/simplified-english.md`, +`reference/skills/optional/software-development/prepare-mcp-integration.md`. + +Skill reference pages carry no screenshots by convention — verified against the +existing `reference/skills/**` pages. The one shared surface worth a shot is the +skills catalog with the new entries visible; `skills/list` and `skills/skills-list` +already cover it and are in the recapture set. **No new targets.** The new skills do +need to be installed by the seed so they appear in that catalog. + +--- + +## C. Seeding — the blocking constraint + +Decisions, status-card summaries, and secret proposals are all **agent-authored**. +`POST /companies/:id/decisions` returns 403 *"Agent run context required"* unless +`req.actor.type === "agent"` with both `agentId` and `runId` +(`server/src/routes/decisions.ts:41`). The seed currently talks to the instance as the +local board user, so it cannot create any of them. + +Supported path, no DB surgery needed: + +1. `POST /api/agents/:id/keys` → returns a plaintext `token` + (`services/agents.ts:896`, board-only). +2. Call the agent-scoped endpoints with `Authorization: Bearer ` and + `X-Paperclip-Run-Id: ` — `middleware/auth.ts:372` sets + `actor.type = "agent"` with `runId` from that header. +3. The seed already produces a real run id (`runnerRunId`, from the Task Runner + wakeup), so a genuine run can back the context. + +This unlocks all three: `POST /companies/:id/decisions`, +`PUT /status-cards/:id/{query,summary}`, and `POST /agents/me/secret-proposals`. + +Risk to verify at runtime: `createApiKey` stores `responsibleUserId = req.actor.userId +?? null`, and the agent-key auth path rejects a key with no responsible user +(`forbidden("Responsible user is unavailable for this agent key")`). If the +local_trusted board actor has no `userId`, the key is unusable and the seed needs a +real board user first. + +Direct DB insert (the `seed-execution-workspace.mjs` pattern) is the fallback, but +decisions are HMAC-signed (`services/decision-signing.ts`, `decision-spec-v1`), so +hand-inserted rows would need a valid signature. Prefer the agent-key route. + +--- + +## C2. Selector drift found by the first full run + +The first `--all` pass captured 342/342 with nothing skipped, but logged 12 step +failures and 8 clip fallbacks. Every one was a **pre-existing** target whose +selector no longer matches the redesigned UI — none of the new targets failed. A +failed step or clip is silent in the output (the capturer deliberately falls back +to "whatever state was reached"), so these had been quietly producing wrong +images: a plain inbox where the doc promised a mode picker, a whole dashboard +where it promised one panel. + +| Target | Was | Now | +|---|---|---| +| `work-modes/work-mode-picker` | `press "c"` to open the composer | the `c` shortcut is gone — click the sidebar **New Task** button | +| `skills/assign-to-agent` | button "Attach to agents" | renamed to **Add to agent** | +| `routines/cron-picker` | click the first `combobox` | RoutineDetail is tabbed now — go straight to `/routines/:id/triggers` | +| `org/org-chart-add-agent` | `/Add agent/i` | button is **New agent** | +| `onboarding/sidebar-new-company-button` | `/workspace switcher/i` | label is **Open \ company switcher** | +| `issues/detail-sidebar` | click `title="Show properties"` | the pane is open by default, so that toggle is hidden — no step needed | +| `dashboard/activity-feed` | `//h3[…]/ancestor::div[@class~rounded]` | the Card is a **sibling** of the heading now — `parent::div` | +| `dashboard/task-breakdown-panel` | same ancestor xpath | same fix | +| `dashboard/stale-tasks-panel` | clip on a "Stale" panel | **panel removed from `Dashboard.tsx`** — target dropped (was unreferenced) | +| `tasks/comment-input-box` | clip a textarea ancestor | still falls back to full page; unreferenced by any doc, left as-is | + +All eight referenced targets were fixed and reshot in both themes with zero step +failures. + +> **Leftover to delete by hand:** dropping `dashboard/stale-tasks-panel` leaves +> `light/dashboard/stale-tasks-panel.png` and its dark twin tracked but unused. +> They are no longer in the registry and nothing references them. + +## D. Order of work + +1. Phase 1 — registry entries, `routes.mjs` targets, `SCREENSHOTS_TODO.md` rows, doc + image embeds for B1–B4; fix A1/A2 routes and `depends_on`. +2. Phase 2 — extend `seed.mjs` (agent-key helper, decisions, status cards, secret + proposals, new skills, flags); bring the instance up at `213dabab4` and eyeball + every new screen before any capture. +3. Phase 3 — `npm run screenshots:refresh:all`, then review PNGs. +4. Phase 4 — prepare the deploy; do not execute without approval. diff --git a/docs/user-guides/screenshots/SCREENSHOTS_TODO.md b/docs/user-guides/screenshots/SCREENSHOTS_TODO.md index 7f8132f..7442839 100644 --- a/docs/user-guides/screenshots/SCREENSHOTS_TODO.md +++ b/docs/user-guides/screenshots/SCREENSHOTS_TODO.md @@ -356,6 +356,64 @@ Manual follow-ups after the batch: --- +## Target paths and routes (v2026.817.0 release batch) + +New surfaces that shipped in v2026.817.0. All of these are seeded automatically — +`seed-new-surfaces.mjs` proposes the decisions, writes the status-card summaries, +and raises the secret proposals through genuine agent runs — so the routes below +render real content rather than empty states. `routes.mjs` is the authoritative +machine-readable copy; keep the two in step. + +### Decisions (`screenshots/light/decisions/`) + +Requires the `enableDecisions` experimental flag (set by `seed.mjs`). + +| File | Route | Notes | +|---|---|---| +| `queue-overview.png` | `/{slug}/decisions` | The ranked feed. | +| `decision-card.png` | `/{slug}/decisions` | Expand the "Ship dark mode…" row. | +| `triage-chips.png` | `/{slug}/decisions` | Clipped to the triage strip only. | +| `toolbar-filters.png` | `/{slug}/decisions` | Click **Filter** to open the panel. | +| `named-queue.png` | `/{slug}/decisions/queues/launch-blockers` | A named lane with two items. | +| `history-curtains.png` | `/{slug}/decisions` | Open the **Decided** curtain. | +| `empty-state.png` | `/{emptySlug}/decisions` | "You're all caught up" — needs the empty second company. | + +### Status cards (`screenshots/light/status-cards/`) + +Requires `enableStatusCards` **and** a `ready` built-in Summarizer. The seed +switches the Summarizer to a long-running `process` adapter and clears its +`pausedAt`, then writes each card's compiled query and summary as the agent. + +| File | Route | Notes | +|---|---|---| +| `board.png` | `/{slug}/status` | Two Fresh tiles. | +| `new-card-dialog.png` | `/{slug}/status` | Click **New card**. | +| `detail-summary.png` | `/{slug}/status/{cardId}` | Drawer, Summary tab. | +| `detail-settings.png` | `/{slug}/status/{cardId}` | Settings tab. | +| `detail-watched-issues.png` | `/{slug}/status/{cardId}` | Watched issues tab. | +| `detail-history.png` | `/{slug}/status/{cardId}` | History tab. | + +### Secret proposals (`screenshots/light/secrets/`) + +| File | Route | Notes | +|---|---|---| +| `proposals-tab.png` | `/{slug}/company/settings/secrets` | Click the **Proposals** tab; one pending, one approved, one rejected. | + +### Chat-style tasks (`screenshots/light/task-chat/`) + +Captured in a **separate pass**. `enableTaskChatRedesign` is instance-wide and +replaces the ordinary task page, so leaving it on would turn every classic +issue/task/work-mode shot into the redesigned chat. `run.mjs` enables it, shoots +only the `phase: "task-chat"` targets, and disables it again. + +| File | Route | Notes | +|---|---|---| +| `thread.png` | `/{slug}/issues/{issueId}` | The conversation view. | +| `composer-modes.png` | `/{slug}/issues/{issueId}` | Open the Agent · Plan · Ask picker. | +| `side-pane.png` | `/{slug}/issues/{issueId}` | Properties · Plan · Artifacts pane. | + +--- + ## After capture - Verify no real user data (avatars, company names, task titles, emails) is visible in any PNG. diff --git a/docs/user-guides/screenshots/dark/activity/activity-filtered-by-agent.png b/docs/user-guides/screenshots/dark/activity/activity-filtered-by-agent.png index 44aa50b..3f4d2b0 100644 Binary files a/docs/user-guides/screenshots/dark/activity/activity-filtered-by-agent.png and b/docs/user-guides/screenshots/dark/activity/activity-filtered-by-agent.png differ diff --git a/docs/user-guides/screenshots/dark/activity/activity-filters.png b/docs/user-guides/screenshots/dark/activity/activity-filters.png index 88fb774..a30ac87 100644 Binary files a/docs/user-guides/screenshots/dark/activity/activity-filters.png and b/docs/user-guides/screenshots/dark/activity/activity-filters.png differ diff --git a/docs/user-guides/screenshots/dark/activity/activity-log-full.png b/docs/user-guides/screenshots/dark/activity/activity-log-full.png index 1487f9a..a9762ae 100644 Binary files a/docs/user-guides/screenshots/dark/activity/activity-log-full.png and b/docs/user-guides/screenshots/dark/activity/activity-log-full.png differ diff --git a/docs/user-guides/screenshots/dark/adapters/detail.png b/docs/user-guides/screenshots/dark/adapters/detail.png index dda2d26..3bd9e46 100644 Binary files a/docs/user-guides/screenshots/dark/adapters/detail.png and b/docs/user-guides/screenshots/dark/adapters/detail.png differ diff --git a/docs/user-guides/screenshots/dark/adapters/health.png b/docs/user-guides/screenshots/dark/adapters/health.png index dda2d26..051363c 100644 Binary files a/docs/user-guides/screenshots/dark/adapters/health.png and b/docs/user-guides/screenshots/dark/adapters/health.png differ diff --git a/docs/user-guides/screenshots/dark/adapters/install.png b/docs/user-guides/screenshots/dark/adapters/install.png index dda2d26..a330cb3 100644 Binary files a/docs/user-guides/screenshots/dark/adapters/install.png and b/docs/user-guides/screenshots/dark/adapters/install.png differ diff --git a/docs/user-guides/screenshots/dark/adapters/list.png b/docs/user-guides/screenshots/dark/adapters/list.png index dda2d26..41f22b7 100644 Binary files a/docs/user-guides/screenshots/dark/adapters/list.png and b/docs/user-guides/screenshots/dark/adapters/list.png differ diff --git a/docs/user-guides/screenshots/dark/agents/adapter-type-dropdown.png b/docs/user-guides/screenshots/dark/agents/adapter-type-dropdown.png index f5f26d8..f6e5ec5 100644 Binary files a/docs/user-guides/screenshots/dark/agents/adapter-type-dropdown.png and b/docs/user-guides/screenshots/dark/agents/adapter-type-dropdown.png differ diff --git a/docs/user-guides/screenshots/dark/agents/agent-detail-idle.png b/docs/user-guides/screenshots/dark/agents/agent-detail-idle.png index 1c07c77..0a397c1 100644 Binary files a/docs/user-guides/screenshots/dark/agents/agent-detail-idle.png and b/docs/user-guides/screenshots/dark/agents/agent-detail-idle.png differ diff --git a/docs/user-guides/screenshots/dark/agents/agent-detail-run-history.png b/docs/user-guides/screenshots/dark/agents/agent-detail-run-history.png index 4112cd0..74d9752 100644 Binary files a/docs/user-guides/screenshots/dark/agents/agent-detail-run-history.png and b/docs/user-guides/screenshots/dark/agents/agent-detail-run-history.png differ diff --git a/docs/user-guides/screenshots/dark/agents/agent-status-running.png b/docs/user-guides/screenshots/dark/agents/agent-status-running.png index 8db1391..61da4f4 100644 Binary files a/docs/user-guides/screenshots/dark/agents/agent-status-running.png and b/docs/user-guides/screenshots/dark/agents/agent-status-running.png differ diff --git a/docs/user-guides/screenshots/dark/agents/agents-list-empty.png b/docs/user-guides/screenshots/dark/agents/agents-list-empty.png index 89c3455..401c1ae 100644 Binary files a/docs/user-guides/screenshots/dark/agents/agents-list-empty.png and b/docs/user-guides/screenshots/dark/agents/agents-list-empty.png differ diff --git a/docs/user-guides/screenshots/dark/agents/budget-and-heartbeat-fields.png b/docs/user-guides/screenshots/dark/agents/budget-and-heartbeat-fields.png index 9e822f4..56b5c66 100644 Binary files a/docs/user-guides/screenshots/dark/agents/budget-and-heartbeat-fields.png and b/docs/user-guides/screenshots/dark/agents/budget-and-heartbeat-fields.png differ diff --git a/docs/user-guides/screenshots/dark/agents/budget.png b/docs/user-guides/screenshots/dark/agents/budget.png index dd771fc..b41fe41 100644 Binary files a/docs/user-guides/screenshots/dark/agents/budget.png and b/docs/user-guides/screenshots/dark/agents/budget.png differ diff --git a/docs/user-guides/screenshots/dark/agents/claude-local-config-filled.png b/docs/user-guides/screenshots/dark/agents/claude-local-config-filled.png index ea2e424..5fc0a36 100644 Binary files a/docs/user-guides/screenshots/dark/agents/claude-local-config-filled.png and b/docs/user-guides/screenshots/dark/agents/claude-local-config-filled.png differ diff --git a/docs/user-guides/screenshots/dark/agents/codex-local-config.png b/docs/user-guides/screenshots/dark/agents/codex-local-config.png index 83de12c..697a374 100644 Binary files a/docs/user-guides/screenshots/dark/agents/codex-local-config.png and b/docs/user-guides/screenshots/dark/agents/codex-local-config.png differ diff --git a/docs/user-guides/screenshots/dark/agents/configuration.png b/docs/user-guides/screenshots/dark/agents/configuration.png index 8a6d578..942c6f7 100644 Binary files a/docs/user-guides/screenshots/dark/agents/configuration.png and b/docs/user-guides/screenshots/dark/agents/configuration.png differ diff --git a/docs/user-guides/screenshots/dark/agents/dashboard.png b/docs/user-guides/screenshots/dark/agents/dashboard.png index 2399379..3cec695 100644 Binary files a/docs/user-guides/screenshots/dark/agents/dashboard.png and b/docs/user-guides/screenshots/dark/agents/dashboard.png differ diff --git a/docs/user-guides/screenshots/dark/agents/heartbeat-toggle-disabled.png b/docs/user-guides/screenshots/dark/agents/heartbeat-toggle-disabled.png index 1220459..1b4d3a8 100644 Binary files a/docs/user-guides/screenshots/dark/agents/heartbeat-toggle-disabled.png and b/docs/user-guides/screenshots/dark/agents/heartbeat-toggle-disabled.png differ diff --git a/docs/user-guides/screenshots/dark/agents/heartbeat-toggle-enabled.png b/docs/user-guides/screenshots/dark/agents/heartbeat-toggle-enabled.png index 1a4f773..a080c4f 100644 Binary files a/docs/user-guides/screenshots/dark/agents/heartbeat-toggle-enabled.png and b/docs/user-guides/screenshots/dark/agents/heartbeat-toggle-enabled.png differ diff --git a/docs/user-guides/screenshots/dark/agents/http-adapter-config.png b/docs/user-guides/screenshots/dark/agents/http-adapter-config.png index 3b979ba..52e1260 100644 Binary files a/docs/user-guides/screenshots/dark/agents/http-adapter-config.png and b/docs/user-guides/screenshots/dark/agents/http-adapter-config.png differ diff --git a/docs/user-guides/screenshots/dark/agents/instructions.png b/docs/user-guides/screenshots/dark/agents/instructions.png index 75ffae8..b2c475b 100644 Binary files a/docs/user-guides/screenshots/dark/agents/instructions.png and b/docs/user-guides/screenshots/dark/agents/instructions.png differ diff --git a/docs/user-guides/screenshots/dark/agents/list.png b/docs/user-guides/screenshots/dark/agents/list.png index 2373426..3c518f8 100644 Binary files a/docs/user-guides/screenshots/dark/agents/list.png and b/docs/user-guides/screenshots/dark/agents/list.png differ diff --git a/docs/user-guides/screenshots/dark/agents/new-agent-name-role.png b/docs/user-guides/screenshots/dark/agents/new-agent-name-role.png index 0a9c124..8197bdf 100644 Binary files a/docs/user-guides/screenshots/dark/agents/new-agent-name-role.png and b/docs/user-guides/screenshots/dark/agents/new-agent-name-role.png differ diff --git a/docs/user-guides/screenshots/dark/agents/new-agent-reports-to-field.png b/docs/user-guides/screenshots/dark/agents/new-agent-reports-to-field.png index 582a64d..29fa4f5 100644 Binary files a/docs/user-guides/screenshots/dark/agents/new-agent-reports-to-field.png and b/docs/user-guides/screenshots/dark/agents/new-agent-reports-to-field.png differ diff --git a/docs/user-guides/screenshots/dark/agents/new.png b/docs/user-guides/screenshots/dark/agents/new.png index 83f0d1e..40b5fef 100644 Binary files a/docs/user-guides/screenshots/dark/agents/new.png and b/docs/user-guides/screenshots/dark/agents/new.png differ diff --git a/docs/user-guides/screenshots/dark/agents/run-history-in-progress.png b/docs/user-guides/screenshots/dark/agents/run-history-in-progress.png index b3b9ca2..42fec1e 100644 Binary files a/docs/user-guides/screenshots/dark/agents/run-history-in-progress.png and b/docs/user-guides/screenshots/dark/agents/run-history-in-progress.png differ diff --git a/docs/user-guides/screenshots/dark/agents/run-transcript-view.png b/docs/user-guides/screenshots/dark/agents/run-transcript-view.png index fb8551d..dba0e63 100644 Binary files a/docs/user-guides/screenshots/dark/agents/run-transcript-view.png and b/docs/user-guides/screenshots/dark/agents/run-transcript-view.png differ diff --git a/docs/user-guides/screenshots/dark/agents/runs.png b/docs/user-guides/screenshots/dark/agents/runs.png index 77b2315..51bf67f 100644 Binary files a/docs/user-guides/screenshots/dark/agents/runs.png and b/docs/user-guides/screenshots/dark/agents/runs.png differ diff --git a/docs/user-guides/screenshots/dark/agents/skills.png b/docs/user-guides/screenshots/dark/agents/skills.png index 93d3abd..ecf4cd8 100644 Binary files a/docs/user-guides/screenshots/dark/agents/skills.png and b/docs/user-guides/screenshots/dark/agents/skills.png differ diff --git a/docs/user-guides/screenshots/dark/agents/test-environment-fail.png b/docs/user-guides/screenshots/dark/agents/test-environment-fail.png index 1dfc844..472e5e8 100644 Binary files a/docs/user-guides/screenshots/dark/agents/test-environment-fail.png and b/docs/user-guides/screenshots/dark/agents/test-environment-fail.png differ diff --git a/docs/user-guides/screenshots/dark/agents/test-environment-success.png b/docs/user-guides/screenshots/dark/agents/test-environment-success.png index 839d55f..6c162b1 100644 Binary files a/docs/user-guides/screenshots/dark/agents/test-environment-success.png and b/docs/user-guides/screenshots/dark/agents/test-environment-success.png differ diff --git a/docs/user-guides/screenshots/dark/approvals/approvals-list.png b/docs/user-guides/screenshots/dark/approvals/approvals-list.png index 5ec0e66..3082b13 100644 Binary files a/docs/user-guides/screenshots/dark/approvals/approvals-list.png and b/docs/user-guides/screenshots/dark/approvals/approvals-list.png differ diff --git a/docs/user-guides/screenshots/dark/approvals/approvals-queue-strategy.png b/docs/user-guides/screenshots/dark/approvals/approvals-queue-strategy.png index 5ec0e66..22f8459 100644 Binary files a/docs/user-guides/screenshots/dark/approvals/approvals-queue-strategy.png and b/docs/user-guides/screenshots/dark/approvals/approvals-queue-strategy.png differ diff --git a/docs/user-guides/screenshots/dark/approvals/approve-reject-buttons.png b/docs/user-guides/screenshots/dark/approvals/approve-reject-buttons.png index e26cfff..669182c 100644 Binary files a/docs/user-guides/screenshots/dark/approvals/approve-reject-buttons.png and b/docs/user-guides/screenshots/dark/approvals/approve-reject-buttons.png differ diff --git a/docs/user-guides/screenshots/dark/approvals/approve-reject-revision-buttons.png b/docs/user-guides/screenshots/dark/approvals/approve-reject-revision-buttons.png index 85c237a..954184b 100644 Binary files a/docs/user-guides/screenshots/dark/approvals/approve-reject-revision-buttons.png and b/docs/user-guides/screenshots/dark/approvals/approve-reject-revision-buttons.png differ diff --git a/docs/user-guides/screenshots/dark/approvals/approved-approval.png b/docs/user-guides/screenshots/dark/approvals/approved-approval.png index 1bc7e6f..510e674 100644 Binary files a/docs/user-guides/screenshots/dark/approvals/approved-approval.png and b/docs/user-guides/screenshots/dark/approvals/approved-approval.png differ diff --git a/docs/user-guides/screenshots/dark/approvals/hire-approval-detail.png b/docs/user-guides/screenshots/dark/approvals/hire-approval-detail.png index e26cfff..05566cc 100644 Binary files a/docs/user-guides/screenshots/dark/approvals/hire-approval-detail.png and b/docs/user-guides/screenshots/dark/approvals/hire-approval-detail.png differ diff --git a/docs/user-guides/screenshots/dark/approvals/queue-filters.png b/docs/user-guides/screenshots/dark/approvals/queue-filters.png index aca7624..1172c3d 100644 Binary files a/docs/user-guides/screenshots/dark/approvals/queue-filters.png and b/docs/user-guides/screenshots/dark/approvals/queue-filters.png differ diff --git a/docs/user-guides/screenshots/dark/approvals/revision-request-input.png b/docs/user-guides/screenshots/dark/approvals/revision-request-input.png index 57350db..613e437 100644 Binary files a/docs/user-guides/screenshots/dark/approvals/revision-request-input.png and b/docs/user-guides/screenshots/dark/approvals/revision-request-input.png differ diff --git a/docs/user-guides/screenshots/dark/approvals/strategy-approval-detail.png b/docs/user-guides/screenshots/dark/approvals/strategy-approval-detail.png index 9f4229e..870fc6c 100644 Binary files a/docs/user-guides/screenshots/dark/approvals/strategy-approval-detail.png and b/docs/user-guides/screenshots/dark/approvals/strategy-approval-detail.png differ diff --git a/docs/user-guides/screenshots/dark/company/access.png b/docs/user-guides/screenshots/dark/company/access.png index 2806922..cdbf7cd 100644 Binary files a/docs/user-guides/screenshots/dark/company/access.png and b/docs/user-guides/screenshots/dark/company/access.png differ diff --git a/docs/user-guides/screenshots/dark/company/company-budget-setting.png b/docs/user-guides/screenshots/dark/company/company-budget-setting.png index 0715742..9ce7550 100644 Binary files a/docs/user-guides/screenshots/dark/company/company-budget-setting.png and b/docs/user-guides/screenshots/dark/company/company-budget-setting.png differ diff --git a/docs/user-guides/screenshots/dark/company/company-goal-field.png b/docs/user-guides/screenshots/dark/company/company-goal-field.png index 6e9ae05..d1e2133 100644 Binary files a/docs/user-guides/screenshots/dark/company/company-goal-field.png and b/docs/user-guides/screenshots/dark/company/company-goal-field.png differ diff --git a/docs/user-guides/screenshots/dark/company/company-saved.png b/docs/user-guides/screenshots/dark/company/company-saved.png index 0715742..3ac7cfc 100644 Binary files a/docs/user-guides/screenshots/dark/company/company-saved.png and b/docs/user-guides/screenshots/dark/company/company-saved.png differ diff --git a/docs/user-guides/screenshots/dark/company/export.png b/docs/user-guides/screenshots/dark/company/export.png index 2c5b473..fc1afeb 100644 Binary files a/docs/user-guides/screenshots/dark/company/export.png and b/docs/user-guides/screenshots/dark/company/export.png differ diff --git a/docs/user-guides/screenshots/dark/company/import.png b/docs/user-guides/screenshots/dark/company/import.png index 63e1687..c7c4608 100644 Binary files a/docs/user-guides/screenshots/dark/company/import.png and b/docs/user-guides/screenshots/dark/company/import.png differ diff --git a/docs/user-guides/screenshots/dark/company/invites.png b/docs/user-guides/screenshots/dark/company/invites.png index 642138f..682abff 100644 Binary files a/docs/user-guides/screenshots/dark/company/invites.png and b/docs/user-guides/screenshots/dark/company/invites.png differ diff --git a/docs/user-guides/screenshots/dark/company/join-requests.png b/docs/user-guides/screenshots/dark/company/join-requests.png index c2e0eac..1af82f1 100644 Binary files a/docs/user-guides/screenshots/dark/company/join-requests.png and b/docs/user-guides/screenshots/dark/company/join-requests.png differ diff --git a/docs/user-guides/screenshots/dark/company/new-company-form.png b/docs/user-guides/screenshots/dark/company/new-company-form.png index 390ece2..2730536 100644 Binary files a/docs/user-guides/screenshots/dark/company/new-company-form.png and b/docs/user-guides/screenshots/dark/company/new-company-form.png differ diff --git a/docs/user-guides/screenshots/dark/company/settings.png b/docs/user-guides/screenshots/dark/company/settings.png index 876b070..1ccb1f0 100644 Binary files a/docs/user-guides/screenshots/dark/company/settings.png and b/docs/user-guides/screenshots/dark/company/settings.png differ diff --git a/docs/user-guides/screenshots/dark/costs/agent-budget-100pct-paused.png b/docs/user-guides/screenshots/dark/costs/agent-budget-100pct-paused.png index 65577d3..37bc93e 100644 Binary files a/docs/user-guides/screenshots/dark/costs/agent-budget-100pct-paused.png and b/docs/user-guides/screenshots/dark/costs/agent-budget-100pct-paused.png differ diff --git a/docs/user-guides/screenshots/dark/costs/agent-budget-50pct.png b/docs/user-guides/screenshots/dark/costs/agent-budget-50pct.png index 93ea28d..b93dcf1 100644 Binary files a/docs/user-guides/screenshots/dark/costs/agent-budget-50pct.png and b/docs/user-guides/screenshots/dark/costs/agent-budget-50pct.png differ diff --git a/docs/user-guides/screenshots/dark/costs/agent-budget-80pct-warning.png b/docs/user-guides/screenshots/dark/costs/agent-budget-80pct-warning.png index 4c09b7a..1876f4b 100644 Binary files a/docs/user-guides/screenshots/dark/costs/agent-budget-80pct-warning.png and b/docs/user-guides/screenshots/dark/costs/agent-budget-80pct-warning.png differ diff --git a/docs/user-guides/screenshots/dark/costs/agent-budget-bar.png b/docs/user-guides/screenshots/dark/costs/agent-budget-bar.png index c9852f6..5e14946 100644 Binary files a/docs/user-guides/screenshots/dark/costs/agent-budget-bar.png and b/docs/user-guides/screenshots/dark/costs/agent-budget-bar.png differ diff --git a/docs/user-guides/screenshots/dark/costs/agent-budget-field.png b/docs/user-guides/screenshots/dark/costs/agent-budget-field.png index 587d8d0..4e50560 100644 Binary files a/docs/user-guides/screenshots/dark/costs/agent-budget-field.png and b/docs/user-guides/screenshots/dark/costs/agent-budget-field.png differ diff --git a/docs/user-guides/screenshots/dark/costs/billers.png b/docs/user-guides/screenshots/dark/costs/billers.png index 2323469..af83324 100644 Binary files a/docs/user-guides/screenshots/dark/costs/billers.png and b/docs/user-guides/screenshots/dark/costs/billers.png differ diff --git a/docs/user-guides/screenshots/dark/costs/budgets.png b/docs/user-guides/screenshots/dark/costs/budgets.png index 2323469..fdeb2c6 100644 Binary files a/docs/user-guides/screenshots/dark/costs/budgets.png and b/docs/user-guides/screenshots/dark/costs/budgets.png differ diff --git a/docs/user-guides/screenshots/dark/costs/company-budget-field.png b/docs/user-guides/screenshots/dark/costs/company-budget-field.png index 4a09490..1b14c4e 100644 Binary files a/docs/user-guides/screenshots/dark/costs/company-budget-field.png and b/docs/user-guides/screenshots/dark/costs/company-budget-field.png differ diff --git a/docs/user-guides/screenshots/dark/costs/costs-dashboard-overview.png b/docs/user-guides/screenshots/dark/costs/costs-dashboard-overview.png index 2323469..a1e4719 100644 Binary files a/docs/user-guides/screenshots/dark/costs/costs-dashboard-overview.png and b/docs/user-guides/screenshots/dark/costs/costs-dashboard-overview.png differ diff --git a/docs/user-guides/screenshots/dark/costs/finance.png b/docs/user-guides/screenshots/dark/costs/finance.png index 2323469..417c755 100644 Binary files a/docs/user-guides/screenshots/dark/costs/finance.png and b/docs/user-guides/screenshots/dark/costs/finance.png differ diff --git a/docs/user-guides/screenshots/dark/costs/overview.png b/docs/user-guides/screenshots/dark/costs/overview.png index 1b4b40d..04eaa5e 100644 Binary files a/docs/user-guides/screenshots/dark/costs/overview.png and b/docs/user-guides/screenshots/dark/costs/overview.png differ diff --git a/docs/user-guides/screenshots/dark/costs/per-run-cost-detail.png b/docs/user-guides/screenshots/dark/costs/per-run-cost-detail.png index 13d50aa..e1fd213 100644 Binary files a/docs/user-guides/screenshots/dark/costs/per-run-cost-detail.png and b/docs/user-guides/screenshots/dark/costs/per-run-cost-detail.png differ diff --git a/docs/user-guides/screenshots/dark/costs/providers.png b/docs/user-guides/screenshots/dark/costs/providers.png index 2323469..69a6cc9 100644 Binary files a/docs/user-guides/screenshots/dark/costs/providers.png and b/docs/user-guides/screenshots/dark/costs/providers.png differ diff --git a/docs/user-guides/screenshots/dark/dashboard/activity-feed.png b/docs/user-guides/screenshots/dark/dashboard/activity-feed.png index 0f3abee..28b5e8b 100644 Binary files a/docs/user-guides/screenshots/dark/dashboard/activity-feed.png and b/docs/user-guides/screenshots/dark/dashboard/activity-feed.png differ diff --git a/docs/user-guides/screenshots/dark/dashboard/agent-status-panel.png b/docs/user-guides/screenshots/dark/dashboard/agent-status-panel.png index 010a33c..ace2d61 100644 Binary files a/docs/user-guides/screenshots/dark/dashboard/agent-status-panel.png and b/docs/user-guides/screenshots/dark/dashboard/agent-status-panel.png differ diff --git a/docs/user-guides/screenshots/dark/dashboard/cost-summary-panel.png b/docs/user-guides/screenshots/dark/dashboard/cost-summary-panel.png index 2e3e121..705f38b 100644 Binary files a/docs/user-guides/screenshots/dark/dashboard/cost-summary-panel.png and b/docs/user-guides/screenshots/dark/dashboard/cost-summary-panel.png differ diff --git a/docs/user-guides/screenshots/dark/dashboard/dashboard-overview.png b/docs/user-guides/screenshots/dark/dashboard/dashboard-overview.png index cceecfb..470cdb4 100644 Binary files a/docs/user-guides/screenshots/dark/dashboard/dashboard-overview.png and b/docs/user-guides/screenshots/dark/dashboard/dashboard-overview.png differ diff --git a/docs/user-guides/screenshots/dark/dashboard/stale-tasks-panel.png b/docs/user-guides/screenshots/dark/dashboard/stale-tasks-panel.png deleted file mode 100644 index 2e3e121..0000000 Binary files a/docs/user-guides/screenshots/dark/dashboard/stale-tasks-panel.png and /dev/null differ diff --git a/docs/user-guides/screenshots/dark/dashboard/task-breakdown-panel.png b/docs/user-guides/screenshots/dark/dashboard/task-breakdown-panel.png index 0b364e2..df408b2 100644 Binary files a/docs/user-guides/screenshots/dark/dashboard/task-breakdown-panel.png and b/docs/user-guides/screenshots/dark/dashboard/task-breakdown-panel.png differ diff --git a/docs/user-guides/screenshots/dark/decisions/decision-card.png b/docs/user-guides/screenshots/dark/decisions/decision-card.png new file mode 100644 index 0000000..7d2d690 Binary files /dev/null and b/docs/user-guides/screenshots/dark/decisions/decision-card.png differ diff --git a/docs/user-guides/screenshots/dark/decisions/empty-state.png b/docs/user-guides/screenshots/dark/decisions/empty-state.png new file mode 100644 index 0000000..c82f962 Binary files /dev/null and b/docs/user-guides/screenshots/dark/decisions/empty-state.png differ diff --git a/docs/user-guides/screenshots/dark/decisions/history-curtains.png b/docs/user-guides/screenshots/dark/decisions/history-curtains.png new file mode 100644 index 0000000..4d3eaa4 Binary files /dev/null and b/docs/user-guides/screenshots/dark/decisions/history-curtains.png differ diff --git a/docs/user-guides/screenshots/dark/decisions/named-queue.png b/docs/user-guides/screenshots/dark/decisions/named-queue.png new file mode 100644 index 0000000..b6fe339 Binary files /dev/null and b/docs/user-guides/screenshots/dark/decisions/named-queue.png differ diff --git a/docs/user-guides/screenshots/dark/decisions/queue-overview.png b/docs/user-guides/screenshots/dark/decisions/queue-overview.png new file mode 100644 index 0000000..a4ad712 Binary files /dev/null and b/docs/user-guides/screenshots/dark/decisions/queue-overview.png differ diff --git a/docs/user-guides/screenshots/dark/decisions/toolbar-filters.png b/docs/user-guides/screenshots/dark/decisions/toolbar-filters.png new file mode 100644 index 0000000..4730428 Binary files /dev/null and b/docs/user-guides/screenshots/dark/decisions/toolbar-filters.png differ diff --git a/docs/user-guides/screenshots/dark/decisions/triage-chips.png b/docs/user-guides/screenshots/dark/decisions/triage-chips.png new file mode 100644 index 0000000..d45fb76 Binary files /dev/null and b/docs/user-guides/screenshots/dark/decisions/triage-chips.png differ diff --git a/docs/user-guides/screenshots/dark/experimental/cloud-upstream.png b/docs/user-guides/screenshots/dark/experimental/cloud-upstream.png index e541d08..8175981 100644 Binary files a/docs/user-guides/screenshots/dark/experimental/cloud-upstream.png and b/docs/user-guides/screenshots/dark/experimental/cloud-upstream.png differ diff --git a/docs/user-guides/screenshots/dark/experimental/environments.png b/docs/user-guides/screenshots/dark/experimental/environments.png index 7c11118..ac3c5aa 100644 Binary files a/docs/user-guides/screenshots/dark/experimental/environments.png and b/docs/user-guides/screenshots/dark/experimental/environments.png differ diff --git a/docs/user-guides/screenshots/dark/experimental/external-objects.png b/docs/user-guides/screenshots/dark/experimental/external-objects.png index d7529f2..688734a 100644 Binary files a/docs/user-guides/screenshots/dark/experimental/external-objects.png and b/docs/user-guides/screenshots/dark/experimental/external-objects.png differ diff --git a/docs/user-guides/screenshots/dark/experimental/file-viewer-browse.png b/docs/user-guides/screenshots/dark/experimental/file-viewer-browse.png index 8f5627a..e0b6198 100644 Binary files a/docs/user-guides/screenshots/dark/experimental/file-viewer-browse.png and b/docs/user-guides/screenshots/dark/experimental/file-viewer-browse.png differ diff --git a/docs/user-guides/screenshots/dark/experimental/recovery-preview.png b/docs/user-guides/screenshots/dark/experimental/recovery-preview.png index 6dd0e16..a78d4d9 100644 Binary files a/docs/user-guides/screenshots/dark/experimental/recovery-preview.png and b/docs/user-guides/screenshots/dark/experimental/recovery-preview.png differ diff --git a/docs/user-guides/screenshots/dark/experimental/server-info.png b/docs/user-guides/screenshots/dark/experimental/server-info.png index 44756be..6b52f29 100644 Binary files a/docs/user-guides/screenshots/dark/experimental/server-info.png and b/docs/user-guides/screenshots/dark/experimental/server-info.png differ diff --git a/docs/user-guides/screenshots/dark/export-import/export-dialog.png b/docs/user-guides/screenshots/dark/export-import/export-dialog.png index 79c8ee9..5e4c6ee 100644 Binary files a/docs/user-guides/screenshots/dark/export-import/export-dialog.png and b/docs/user-guides/screenshots/dark/export-import/export-dialog.png differ diff --git a/docs/user-guides/screenshots/dark/export-import/import-dialog.png b/docs/user-guides/screenshots/dark/export-import/import-dialog.png index de04a77..afa419c 100644 Binary files a/docs/user-guides/screenshots/dark/export-import/import-dialog.png and b/docs/user-guides/screenshots/dark/export-import/import-dialog.png differ diff --git a/docs/user-guides/screenshots/dark/goals/detail.png b/docs/user-guides/screenshots/dark/goals/detail.png index 3c098e7..f08c663 100644 Binary files a/docs/user-guides/screenshots/dark/goals/detail.png and b/docs/user-guides/screenshots/dark/goals/detail.png differ diff --git a/docs/user-guides/screenshots/dark/goals/list.png b/docs/user-guides/screenshots/dark/goals/list.png index d331127..1631af7 100644 Binary files a/docs/user-guides/screenshots/dark/goals/list.png and b/docs/user-guides/screenshots/dark/goals/list.png differ diff --git a/docs/user-guides/screenshots/dark/interactions/checkbox-confirmation-card.png b/docs/user-guides/screenshots/dark/interactions/checkbox-confirmation-card.png index 8c58df7..0a3cb7e 100644 Binary files a/docs/user-guides/screenshots/dark/interactions/checkbox-confirmation-card.png and b/docs/user-guides/screenshots/dark/interactions/checkbox-confirmation-card.png differ diff --git a/docs/user-guides/screenshots/dark/issues/detail-activity.png b/docs/user-guides/screenshots/dark/issues/detail-activity.png index 8aac5f4..e4697df 100644 Binary files a/docs/user-guides/screenshots/dark/issues/detail-activity.png and b/docs/user-guides/screenshots/dark/issues/detail-activity.png differ diff --git a/docs/user-guides/screenshots/dark/issues/detail-chat.png b/docs/user-guides/screenshots/dark/issues/detail-chat.png index 2be9849..7ee3e4f 100644 Binary files a/docs/user-guides/screenshots/dark/issues/detail-chat.png and b/docs/user-guides/screenshots/dark/issues/detail-chat.png differ diff --git a/docs/user-guides/screenshots/dark/issues/detail-sidebar.png b/docs/user-guides/screenshots/dark/issues/detail-sidebar.png index 6839631..2ad4cc4 100644 Binary files a/docs/user-guides/screenshots/dark/issues/detail-sidebar.png and b/docs/user-guides/screenshots/dark/issues/detail-sidebar.png differ diff --git a/docs/user-guides/screenshots/dark/issues/inbox-requests.png b/docs/user-guides/screenshots/dark/issues/inbox-requests.png index 48ebc2d..f689443 100644 Binary files a/docs/user-guides/screenshots/dark/issues/inbox-requests.png and b/docs/user-guides/screenshots/dark/issues/inbox-requests.png differ diff --git a/docs/user-guides/screenshots/dark/issues/inbox-unread.png b/docs/user-guides/screenshots/dark/issues/inbox-unread.png index 2b4b459..606386d 100644 Binary files a/docs/user-guides/screenshots/dark/issues/inbox-unread.png and b/docs/user-guides/screenshots/dark/issues/inbox-unread.png differ diff --git a/docs/user-guides/screenshots/dark/issues/inbox.png b/docs/user-guides/screenshots/dark/issues/inbox.png index 4d1d38f..fb4a62c 100644 Binary files a/docs/user-guides/screenshots/dark/issues/inbox.png and b/docs/user-guides/screenshots/dark/issues/inbox.png differ diff --git a/docs/user-guides/screenshots/dark/issues/list.png b/docs/user-guides/screenshots/dark/issues/list.png index f2f9b20..798a584 100644 Binary files a/docs/user-guides/screenshots/dark/issues/list.png and b/docs/user-guides/screenshots/dark/issues/list.png differ diff --git a/docs/user-guides/screenshots/dark/issues/my-issues.png b/docs/user-guides/screenshots/dark/issues/my-issues.png index ffc5303..0050875 100644 Binary files a/docs/user-guides/screenshots/dark/issues/my-issues.png and b/docs/user-guides/screenshots/dark/issues/my-issues.png differ diff --git a/docs/user-guides/screenshots/dark/onboarding/budget-field.png b/docs/user-guides/screenshots/dark/onboarding/budget-field.png index 83babaf..7ce3d85 100644 Binary files a/docs/user-guides/screenshots/dark/onboarding/budget-field.png and b/docs/user-guides/screenshots/dark/onboarding/budget-field.png differ diff --git a/docs/user-guides/screenshots/dark/onboarding/empty-dashboard.png b/docs/user-guides/screenshots/dark/onboarding/empty-dashboard.png index 0a7f626..d72a775 100644 Binary files a/docs/user-guides/screenshots/dark/onboarding/empty-dashboard.png and b/docs/user-guides/screenshots/dark/onboarding/empty-dashboard.png differ diff --git a/docs/user-guides/screenshots/dark/onboarding/goal-field.png b/docs/user-guides/screenshots/dark/onboarding/goal-field.png index 21c095b..ce4304e 100644 Binary files a/docs/user-guides/screenshots/dark/onboarding/goal-field.png and b/docs/user-guides/screenshots/dark/onboarding/goal-field.png differ diff --git a/docs/user-guides/screenshots/dark/onboarding/new-company-modal-empty.png b/docs/user-guides/screenshots/dark/onboarding/new-company-modal-empty.png index a63f966..545c2a7 100644 Binary files a/docs/user-guides/screenshots/dark/onboarding/new-company-modal-empty.png and b/docs/user-guides/screenshots/dark/onboarding/new-company-modal-empty.png differ diff --git a/docs/user-guides/screenshots/dark/onboarding/sidebar-new-company-button.png b/docs/user-guides/screenshots/dark/onboarding/sidebar-new-company-button.png index dab4ab3..13df0e5 100644 Binary files a/docs/user-guides/screenshots/dark/onboarding/sidebar-new-company-button.png and b/docs/user-guides/screenshots/dark/onboarding/sidebar-new-company-button.png differ diff --git a/docs/user-guides/screenshots/dark/org/chart.png b/docs/user-guides/screenshots/dark/org/chart.png index 7e62832..3a7da2e 100644 Binary files a/docs/user-guides/screenshots/dark/org/chart.png and b/docs/user-guides/screenshots/dark/org/chart.png differ diff --git a/docs/user-guides/screenshots/dark/org/org-chart-add-agent.png b/docs/user-guides/screenshots/dark/org/org-chart-add-agent.png index f5ee023..d845ca9 100644 Binary files a/docs/user-guides/screenshots/dark/org/org-chart-add-agent.png and b/docs/user-guides/screenshots/dark/org/org-chart-add-agent.png differ diff --git a/docs/user-guides/screenshots/dark/org/org-chart-manager-assigned.png b/docs/user-guides/screenshots/dark/org/org-chart-manager-assigned.png index f5ee023..4a06fcc 100644 Binary files a/docs/user-guides/screenshots/dark/org/org-chart-manager-assigned.png and b/docs/user-guides/screenshots/dark/org/org-chart-manager-assigned.png differ diff --git a/docs/user-guides/screenshots/dark/org/org-chart-small-team.png b/docs/user-guides/screenshots/dark/org/org-chart-small-team.png index 123515e..fd2df03 100644 Binary files a/docs/user-guides/screenshots/dark/org/org-chart-small-team.png and b/docs/user-guides/screenshots/dark/org/org-chart-small-team.png differ diff --git a/docs/user-guides/screenshots/dark/org/org-chart-view.png b/docs/user-guides/screenshots/dark/org/org-chart-view.png index 123515e..fa4b6d1 100644 Binary files a/docs/user-guides/screenshots/dark/org/org-chart-view.png and b/docs/user-guides/screenshots/dark/org/org-chart-view.png differ diff --git a/docs/user-guides/screenshots/dark/org/reassign.png b/docs/user-guides/screenshots/dark/org/reassign.png index 07e895c..b206695 100644 Binary files a/docs/user-guides/screenshots/dark/org/reassign.png and b/docs/user-guides/screenshots/dark/org/reassign.png differ diff --git a/docs/user-guides/screenshots/dark/org/skills-list.png b/docs/user-guides/screenshots/dark/org/skills-list.png index 8c227b2..144ee90 100644 Binary files a/docs/user-guides/screenshots/dark/org/skills-list.png and b/docs/user-guides/screenshots/dark/org/skills-list.png differ diff --git a/docs/user-guides/screenshots/dark/org/workspace-modes.png b/docs/user-guides/screenshots/dark/org/workspace-modes.png index f5ee023..e4dd457 100644 Binary files a/docs/user-guides/screenshots/dark/org/workspace-modes.png and b/docs/user-guides/screenshots/dark/org/workspace-modes.png differ diff --git a/docs/user-guides/screenshots/dark/plugins/detail.png b/docs/user-guides/screenshots/dark/plugins/detail.png index 51eb7d8..86366a8 100644 Binary files a/docs/user-guides/screenshots/dark/plugins/detail.png and b/docs/user-guides/screenshots/dark/plugins/detail.png differ diff --git a/docs/user-guides/screenshots/dark/plugins/install.png b/docs/user-guides/screenshots/dark/plugins/install.png index 51eb7d8..1e59f51 100644 Binary files a/docs/user-guides/screenshots/dark/plugins/install.png and b/docs/user-guides/screenshots/dark/plugins/install.png differ diff --git a/docs/user-guides/screenshots/dark/plugins/jobs-log.png b/docs/user-guides/screenshots/dark/plugins/jobs-log.png index 51eb7d8..7dc5953 100644 Binary files a/docs/user-guides/screenshots/dark/plugins/jobs-log.png and b/docs/user-guides/screenshots/dark/plugins/jobs-log.png differ diff --git a/docs/user-guides/screenshots/dark/plugins/list.png b/docs/user-guides/screenshots/dark/plugins/list.png index 51eb7d8..7dc5953 100644 Binary files a/docs/user-guides/screenshots/dark/plugins/list.png and b/docs/user-guides/screenshots/dark/plugins/list.png differ diff --git a/docs/user-guides/screenshots/dark/plugins/settings.png b/docs/user-guides/screenshots/dark/plugins/settings.png index 51eb7d8..aa5ef31 100644 Binary files a/docs/user-guides/screenshots/dark/plugins/settings.png and b/docs/user-guides/screenshots/dark/plugins/settings.png differ diff --git a/docs/user-guides/screenshots/dark/projects/budget.png b/docs/user-guides/screenshots/dark/projects/budget.png index 520470f..ece5091 100644 Binary files a/docs/user-guides/screenshots/dark/projects/budget.png and b/docs/user-guides/screenshots/dark/projects/budget.png differ diff --git a/docs/user-guides/screenshots/dark/projects/configuration.png b/docs/user-guides/screenshots/dark/projects/configuration.png index c149f64..d88bc60 100644 Binary files a/docs/user-guides/screenshots/dark/projects/configuration.png and b/docs/user-guides/screenshots/dark/projects/configuration.png differ diff --git a/docs/user-guides/screenshots/dark/projects/issues.png b/docs/user-guides/screenshots/dark/projects/issues.png index 92c61d4..fc04b9f 100644 Binary files a/docs/user-guides/screenshots/dark/projects/issues.png and b/docs/user-guides/screenshots/dark/projects/issues.png differ diff --git a/docs/user-guides/screenshots/dark/projects/list.png b/docs/user-guides/screenshots/dark/projects/list.png index 40da1e5..9daa101 100644 Binary files a/docs/user-guides/screenshots/dark/projects/list.png and b/docs/user-guides/screenshots/dark/projects/list.png differ diff --git a/docs/user-guides/screenshots/dark/projects/overview.png b/docs/user-guides/screenshots/dark/projects/overview.png index 2f9a8b9..80d4f49 100644 Binary files a/docs/user-guides/screenshots/dark/projects/overview.png and b/docs/user-guides/screenshots/dark/projects/overview.png differ diff --git a/docs/user-guides/screenshots/dark/projects/workspaces.png b/docs/user-guides/screenshots/dark/projects/workspaces.png index b2fb097..b3d2dff 100644 Binary files a/docs/user-guides/screenshots/dark/projects/workspaces.png and b/docs/user-guides/screenshots/dark/projects/workspaces.png differ diff --git a/docs/user-guides/screenshots/dark/routines/cron-picker.png b/docs/user-guides/screenshots/dark/routines/cron-picker.png index b065500..673889a 100644 Binary files a/docs/user-guides/screenshots/dark/routines/cron-picker.png and b/docs/user-guides/screenshots/dark/routines/cron-picker.png differ diff --git a/docs/user-guides/screenshots/dark/routines/detail.png b/docs/user-guides/screenshots/dark/routines/detail.png index 6c64f64..8726513 100644 Binary files a/docs/user-guides/screenshots/dark/routines/detail.png and b/docs/user-guides/screenshots/dark/routines/detail.png differ diff --git a/docs/user-guides/screenshots/dark/routines/list.png b/docs/user-guides/screenshots/dark/routines/list.png index 3399a72..8d2cec7 100644 Binary files a/docs/user-guides/screenshots/dark/routines/list.png and b/docs/user-guides/screenshots/dark/routines/list.png differ diff --git a/docs/user-guides/screenshots/dark/routines/run-history.png b/docs/user-guides/screenshots/dark/routines/run-history.png index d7dd8c4..8345f5d 100644 Binary files a/docs/user-guides/screenshots/dark/routines/run-history.png and b/docs/user-guides/screenshots/dark/routines/run-history.png differ diff --git a/docs/user-guides/screenshots/dark/secrets/dispatch-check.png b/docs/user-guides/screenshots/dark/secrets/dispatch-check.png index d6833c6..1546c66 100644 Binary files a/docs/user-guides/screenshots/dark/secrets/dispatch-check.png and b/docs/user-guides/screenshots/dark/secrets/dispatch-check.png differ diff --git a/docs/user-guides/screenshots/dark/secrets/per-user-value-entry.png b/docs/user-guides/screenshots/dark/secrets/per-user-value-entry.png index d6833c6..543aab5 100644 Binary files a/docs/user-guides/screenshots/dark/secrets/per-user-value-entry.png and b/docs/user-guides/screenshots/dark/secrets/per-user-value-entry.png differ diff --git a/docs/user-guides/screenshots/dark/secrets/proposals-tab.png b/docs/user-guides/screenshots/dark/secrets/proposals-tab.png new file mode 100644 index 0000000..169bde1 Binary files /dev/null and b/docs/user-guides/screenshots/dark/secrets/proposals-tab.png differ diff --git a/docs/user-guides/screenshots/dark/secrets/user-secret-definition.png b/docs/user-guides/screenshots/dark/secrets/user-secret-definition.png index 698c67b..ebd6d48 100644 Binary files a/docs/user-guides/screenshots/dark/secrets/user-secret-definition.png and b/docs/user-guides/screenshots/dark/secrets/user-secret-definition.png differ diff --git a/docs/user-guides/screenshots/dark/settings/experimental.png b/docs/user-guides/screenshots/dark/settings/experimental.png index d04ca6b..4f294e2 100644 Binary files a/docs/user-guides/screenshots/dark/settings/experimental.png and b/docs/user-guides/screenshots/dark/settings/experimental.png differ diff --git a/docs/user-guides/screenshots/dark/settings/instance-access.png b/docs/user-guides/screenshots/dark/settings/instance-access.png index 501486b..b0bc399 100644 Binary files a/docs/user-guides/screenshots/dark/settings/instance-access.png and b/docs/user-guides/screenshots/dark/settings/instance-access.png differ diff --git a/docs/user-guides/screenshots/dark/settings/instance-adapters.png b/docs/user-guides/screenshots/dark/settings/instance-adapters.png index dda2d26..7fee51d 100644 Binary files a/docs/user-guides/screenshots/dark/settings/instance-adapters.png and b/docs/user-guides/screenshots/dark/settings/instance-adapters.png differ diff --git a/docs/user-guides/screenshots/dark/settings/instance-general.png b/docs/user-guides/screenshots/dark/settings/instance-general.png index d62b36d..7b93dc5 100644 Binary files a/docs/user-guides/screenshots/dark/settings/instance-general.png and b/docs/user-guides/screenshots/dark/settings/instance-general.png differ diff --git a/docs/user-guides/screenshots/dark/settings/profile.png b/docs/user-guides/screenshots/dark/settings/profile.png index 6cac566..5287a89 100644 Binary files a/docs/user-guides/screenshots/dark/settings/profile.png and b/docs/user-guides/screenshots/dark/settings/profile.png differ diff --git a/docs/user-guides/screenshots/dark/settings/scheduler-heartbeats.png b/docs/user-guides/screenshots/dark/settings/scheduler-heartbeats.png index 2497e93..60917e2 100644 Binary files a/docs/user-guides/screenshots/dark/settings/scheduler-heartbeats.png and b/docs/user-guides/screenshots/dark/settings/scheduler-heartbeats.png differ diff --git a/docs/user-guides/screenshots/dark/skills/add-skill-dialog-confirm.png b/docs/user-guides/screenshots/dark/skills/add-skill-dialog-confirm.png index dd8305f..f870d96 100644 Binary files a/docs/user-guides/screenshots/dark/skills/add-skill-dialog-confirm.png and b/docs/user-guides/screenshots/dark/skills/add-skill-dialog-confirm.png differ diff --git a/docs/user-guides/screenshots/dark/skills/add-skill-dialog.png b/docs/user-guides/screenshots/dark/skills/add-skill-dialog.png index dd8305f..87ec9a6 100644 Binary files a/docs/user-guides/screenshots/dark/skills/add-skill-dialog.png and b/docs/user-guides/screenshots/dark/skills/add-skill-dialog.png differ diff --git a/docs/user-guides/screenshots/dark/skills/assign-to-agent.png b/docs/user-guides/screenshots/dark/skills/assign-to-agent.png index 651e692..0d1bea9 100644 Binary files a/docs/user-guides/screenshots/dark/skills/assign-to-agent.png and b/docs/user-guides/screenshots/dark/skills/assign-to-agent.png differ diff --git a/docs/user-guides/screenshots/dark/skills/file-inventory.png b/docs/user-guides/screenshots/dark/skills/file-inventory.png index dd8305f..05651ed 100644 Binary files a/docs/user-guides/screenshots/dark/skills/file-inventory.png and b/docs/user-guides/screenshots/dark/skills/file-inventory.png differ diff --git a/docs/user-guides/screenshots/dark/skills/list.png b/docs/user-guides/screenshots/dark/skills/list.png index dd8305f..0d8ac44 100644 Binary files a/docs/user-guides/screenshots/dark/skills/list.png and b/docs/user-guides/screenshots/dark/skills/list.png differ diff --git a/docs/user-guides/screenshots/dark/skills/skill-detail.png b/docs/user-guides/screenshots/dark/skills/skill-detail.png index 92844d5..09219ba 100644 Binary files a/docs/user-guides/screenshots/dark/skills/skill-detail.png and b/docs/user-guides/screenshots/dark/skills/skill-detail.png differ diff --git a/docs/user-guides/screenshots/dark/skills/skills-list.png b/docs/user-guides/screenshots/dark/skills/skills-list.png index 92844d5..b5a740f 100644 Binary files a/docs/user-guides/screenshots/dark/skills/skills-list.png and b/docs/user-guides/screenshots/dark/skills/skills-list.png differ diff --git a/docs/user-guides/screenshots/dark/status-cards/board.png b/docs/user-guides/screenshots/dark/status-cards/board.png new file mode 100644 index 0000000..50d8506 Binary files /dev/null and b/docs/user-guides/screenshots/dark/status-cards/board.png differ diff --git a/docs/user-guides/screenshots/dark/status-cards/detail-history.png b/docs/user-guides/screenshots/dark/status-cards/detail-history.png new file mode 100644 index 0000000..85e19bd Binary files /dev/null and b/docs/user-guides/screenshots/dark/status-cards/detail-history.png differ diff --git a/docs/user-guides/screenshots/dark/status-cards/detail-settings.png b/docs/user-guides/screenshots/dark/status-cards/detail-settings.png new file mode 100644 index 0000000..254b516 Binary files /dev/null and b/docs/user-guides/screenshots/dark/status-cards/detail-settings.png differ diff --git a/docs/user-guides/screenshots/dark/status-cards/detail-summary.png b/docs/user-guides/screenshots/dark/status-cards/detail-summary.png new file mode 100644 index 0000000..87dc00d Binary files /dev/null and b/docs/user-guides/screenshots/dark/status-cards/detail-summary.png differ diff --git a/docs/user-guides/screenshots/dark/status-cards/detail-watched-issues.png b/docs/user-guides/screenshots/dark/status-cards/detail-watched-issues.png new file mode 100644 index 0000000..93e203e Binary files /dev/null and b/docs/user-guides/screenshots/dark/status-cards/detail-watched-issues.png differ diff --git a/docs/user-guides/screenshots/dark/status-cards/new-card-dialog.png b/docs/user-guides/screenshots/dark/status-cards/new-card-dialog.png new file mode 100644 index 0000000..53447d6 Binary files /dev/null and b/docs/user-guides/screenshots/dark/status-cards/new-card-dialog.png differ diff --git a/docs/user-guides/screenshots/dark/task-chat/composer-modes.png b/docs/user-guides/screenshots/dark/task-chat/composer-modes.png new file mode 100644 index 0000000..39f17cd Binary files /dev/null and b/docs/user-guides/screenshots/dark/task-chat/composer-modes.png differ diff --git a/docs/user-guides/screenshots/dark/task-chat/side-pane.png b/docs/user-guides/screenshots/dark/task-chat/side-pane.png new file mode 100644 index 0000000..e896f08 Binary files /dev/null and b/docs/user-guides/screenshots/dark/task-chat/side-pane.png differ diff --git a/docs/user-guides/screenshots/dark/task-chat/thread.png b/docs/user-guides/screenshots/dark/task-chat/thread.png new file mode 100644 index 0000000..26f1802 Binary files /dev/null and b/docs/user-guides/screenshots/dark/task-chat/thread.png differ diff --git a/docs/user-guides/screenshots/dark/tasks/comment-input-box.png b/docs/user-guides/screenshots/dark/tasks/comment-input-box.png index c6a5b33..1c21bdd 100644 Binary files a/docs/user-guides/screenshots/dark/tasks/comment-input-box.png and b/docs/user-guides/screenshots/dark/tasks/comment-input-box.png differ diff --git a/docs/user-guides/screenshots/dark/tasks/inbox-newly-created-tasks.png b/docs/user-guides/screenshots/dark/tasks/inbox-newly-created-tasks.png index 622ce17..1bdb8df 100644 Binary files a/docs/user-guides/screenshots/dark/tasks/inbox-newly-created-tasks.png and b/docs/user-guides/screenshots/dark/tasks/inbox-newly-created-tasks.png differ diff --git a/docs/user-guides/screenshots/dark/tasks/inbox-view-with-filters.png b/docs/user-guides/screenshots/dark/tasks/inbox-view-with-filters.png index d95ae76..267b508 100644 Binary files a/docs/user-guides/screenshots/dark/tasks/inbox-view-with-filters.png and b/docs/user-guides/screenshots/dark/tasks/inbox-view-with-filters.png differ diff --git a/docs/user-guides/screenshots/dark/tasks/inbox-view.png b/docs/user-guides/screenshots/dark/tasks/inbox-view.png index d996689..651c766 100644 Binary files a/docs/user-guides/screenshots/dark/tasks/inbox-view.png and b/docs/user-guides/screenshots/dark/tasks/inbox-view.png differ diff --git a/docs/user-guides/screenshots/dark/tasks/new-task-form-empty.png b/docs/user-guides/screenshots/dark/tasks/new-task-form-empty.png index c6a5b33..1dd86a6 100644 Binary files a/docs/user-guides/screenshots/dark/tasks/new-task-form-empty.png and b/docs/user-guides/screenshots/dark/tasks/new-task-form-empty.png differ diff --git a/docs/user-guides/screenshots/dark/tasks/new-task-form-filled.png b/docs/user-guides/screenshots/dark/tasks/new-task-form-filled.png index c6a5b33..4e0c1de 100644 Binary files a/docs/user-guides/screenshots/dark/tasks/new-task-form-filled.png and b/docs/user-guides/screenshots/dark/tasks/new-task-form-filled.png differ diff --git a/docs/user-guides/screenshots/dark/tasks/task-detail-in-progress.png b/docs/user-guides/screenshots/dark/tasks/task-detail-in-progress.png index c6a5b33..88ebce3 100644 Binary files a/docs/user-guides/screenshots/dark/tasks/task-detail-in-progress.png and b/docs/user-guides/screenshots/dark/tasks/task-detail-in-progress.png differ diff --git a/docs/user-guides/screenshots/dark/tasks/task-detail-with-comments.png b/docs/user-guides/screenshots/dark/tasks/task-detail-with-comments.png index d847eb7..f4b0a98 100644 Binary files a/docs/user-guides/screenshots/dark/tasks/task-detail-with-comments.png and b/docs/user-guides/screenshots/dark/tasks/task-detail-with-comments.png differ diff --git a/docs/user-guides/screenshots/dark/tasks/task-done-status.png b/docs/user-guides/screenshots/dark/tasks/task-done-status.png index c6a5b33..9e65e81 100644 Binary files a/docs/user-guides/screenshots/dark/tasks/task-done-status.png and b/docs/user-guides/screenshots/dark/tasks/task-done-status.png differ diff --git a/docs/user-guides/screenshots/dark/watchdogs/watchdog-thread-outcome.png b/docs/user-guides/screenshots/dark/watchdogs/watchdog-thread-outcome.png index 8576bbd..6b60e39 100644 Binary files a/docs/user-guides/screenshots/dark/watchdogs/watchdog-thread-outcome.png and b/docs/user-guides/screenshots/dark/watchdogs/watchdog-thread-outcome.png differ diff --git a/docs/user-guides/screenshots/dark/work-modes/ask-mode-answer.png b/docs/user-guides/screenshots/dark/work-modes/ask-mode-answer.png index 8aac5f4..b879e55 100644 Binary files a/docs/user-guides/screenshots/dark/work-modes/ask-mode-answer.png and b/docs/user-guides/screenshots/dark/work-modes/ask-mode-answer.png differ diff --git a/docs/user-guides/screenshots/dark/work-modes/plan-mode-plan.png b/docs/user-guides/screenshots/dark/work-modes/plan-mode-plan.png index a8133aa..5965004 100644 Binary files a/docs/user-guides/screenshots/dark/work-modes/plan-mode-plan.png and b/docs/user-guides/screenshots/dark/work-modes/plan-mode-plan.png differ diff --git a/docs/user-guides/screenshots/dark/work-modes/standard-mode-run.png b/docs/user-guides/screenshots/dark/work-modes/standard-mode-run.png index 5375986..49d0257 100644 Binary files a/docs/user-guides/screenshots/dark/work-modes/standard-mode-run.png and b/docs/user-guides/screenshots/dark/work-modes/standard-mode-run.png differ diff --git a/docs/user-guides/screenshots/dark/work-modes/work-mode-picker.png b/docs/user-guides/screenshots/dark/work-modes/work-mode-picker.png index 628c79c..b11a79a 100644 Binary files a/docs/user-guides/screenshots/dark/work-modes/work-mode-picker.png and b/docs/user-guides/screenshots/dark/work-modes/work-mode-picker.png differ diff --git a/docs/user-guides/screenshots/dark/work-timeline/work-timeline-handoff.png b/docs/user-guides/screenshots/dark/work-timeline/work-timeline-handoff.png index d59b6eb..b0d4d6c 100644 Binary files a/docs/user-guides/screenshots/dark/work-timeline/work-timeline-handoff.png and b/docs/user-guides/screenshots/dark/work-timeline/work-timeline-handoff.png differ diff --git a/docs/user-guides/screenshots/dark/work-timeline/work-timeline-overview.png b/docs/user-guides/screenshots/dark/work-timeline/work-timeline-overview.png index 10ef815..abd671d 100644 Binary files a/docs/user-guides/screenshots/dark/work-timeline/work-timeline-overview.png and b/docs/user-guides/screenshots/dark/work-timeline/work-timeline-overview.png differ diff --git a/docs/user-guides/screenshots/dark/workspaces/configuration.png b/docs/user-guides/screenshots/dark/workspaces/configuration.png index 38ff3a6..2e8267f 100644 Binary files a/docs/user-guides/screenshots/dark/workspaces/configuration.png and b/docs/user-guides/screenshots/dark/workspaces/configuration.png differ diff --git a/docs/user-guides/screenshots/dark/workspaces/issues.png b/docs/user-guides/screenshots/dark/workspaces/issues.png index e4e0422..2b301b9 100644 Binary files a/docs/user-guides/screenshots/dark/workspaces/issues.png and b/docs/user-guides/screenshots/dark/workspaces/issues.png differ diff --git a/docs/user-guides/screenshots/dark/workspaces/list.png b/docs/user-guides/screenshots/dark/workspaces/list.png index f772f92..1e08bed 100644 Binary files a/docs/user-guides/screenshots/dark/workspaces/list.png and b/docs/user-guides/screenshots/dark/workspaces/list.png differ diff --git a/docs/user-guides/screenshots/dark/workspaces/runtime-logs.png b/docs/user-guides/screenshots/dark/workspaces/runtime-logs.png index cbc0075..73d0d69 100644 Binary files a/docs/user-guides/screenshots/dark/workspaces/runtime-logs.png and b/docs/user-guides/screenshots/dark/workspaces/runtime-logs.png differ diff --git a/docs/user-guides/screenshots/dark/workspaces/services.png b/docs/user-guides/screenshots/dark/workspaces/services.png index 60a948b..90f601a 100644 Binary files a/docs/user-guides/screenshots/dark/workspaces/services.png and b/docs/user-guides/screenshots/dark/workspaces/services.png differ diff --git a/docs/user-guides/screenshots/light/activity/activity-filtered-by-agent.png b/docs/user-guides/screenshots/light/activity/activity-filtered-by-agent.png index 4b196a2..4fe50aa 100644 Binary files a/docs/user-guides/screenshots/light/activity/activity-filtered-by-agent.png and b/docs/user-guides/screenshots/light/activity/activity-filtered-by-agent.png differ diff --git a/docs/user-guides/screenshots/light/activity/activity-filters.png b/docs/user-guides/screenshots/light/activity/activity-filters.png index 582e5f5..59b5bd4 100644 Binary files a/docs/user-guides/screenshots/light/activity/activity-filters.png and b/docs/user-guides/screenshots/light/activity/activity-filters.png differ diff --git a/docs/user-guides/screenshots/light/activity/activity-log-full.png b/docs/user-guides/screenshots/light/activity/activity-log-full.png index 784a7c6..76ae85c 100644 Binary files a/docs/user-guides/screenshots/light/activity/activity-log-full.png and b/docs/user-guides/screenshots/light/activity/activity-log-full.png differ diff --git a/docs/user-guides/screenshots/light/adapters/detail.png b/docs/user-guides/screenshots/light/adapters/detail.png index bec2cf5..5560c56 100644 Binary files a/docs/user-guides/screenshots/light/adapters/detail.png and b/docs/user-guides/screenshots/light/adapters/detail.png differ diff --git a/docs/user-guides/screenshots/light/adapters/health.png b/docs/user-guides/screenshots/light/adapters/health.png index bec2cf5..177a1c0 100644 Binary files a/docs/user-guides/screenshots/light/adapters/health.png and b/docs/user-guides/screenshots/light/adapters/health.png differ diff --git a/docs/user-guides/screenshots/light/adapters/install.png b/docs/user-guides/screenshots/light/adapters/install.png index bec2cf5..48ed615 100644 Binary files a/docs/user-guides/screenshots/light/adapters/install.png and b/docs/user-guides/screenshots/light/adapters/install.png differ diff --git a/docs/user-guides/screenshots/light/adapters/list.png b/docs/user-guides/screenshots/light/adapters/list.png index bec2cf5..48ed615 100644 Binary files a/docs/user-guides/screenshots/light/adapters/list.png and b/docs/user-guides/screenshots/light/adapters/list.png differ diff --git a/docs/user-guides/screenshots/light/agents/adapter-type-dropdown.png b/docs/user-guides/screenshots/light/agents/adapter-type-dropdown.png index 90117bd..70487ca 100644 Binary files a/docs/user-guides/screenshots/light/agents/adapter-type-dropdown.png and b/docs/user-guides/screenshots/light/agents/adapter-type-dropdown.png differ diff --git a/docs/user-guides/screenshots/light/agents/agent-detail-idle.png b/docs/user-guides/screenshots/light/agents/agent-detail-idle.png index 5522fa6..9155585 100644 Binary files a/docs/user-guides/screenshots/light/agents/agent-detail-idle.png and b/docs/user-guides/screenshots/light/agents/agent-detail-idle.png differ diff --git a/docs/user-guides/screenshots/light/agents/agent-detail-run-history.png b/docs/user-guides/screenshots/light/agents/agent-detail-run-history.png index b63e470..99d43b7 100644 Binary files a/docs/user-guides/screenshots/light/agents/agent-detail-run-history.png and b/docs/user-guides/screenshots/light/agents/agent-detail-run-history.png differ diff --git a/docs/user-guides/screenshots/light/agents/agent-status-running.png b/docs/user-guides/screenshots/light/agents/agent-status-running.png index 1d7133d..6f61f70 100644 Binary files a/docs/user-guides/screenshots/light/agents/agent-status-running.png and b/docs/user-guides/screenshots/light/agents/agent-status-running.png differ diff --git a/docs/user-guides/screenshots/light/agents/agents-list-empty.png b/docs/user-guides/screenshots/light/agents/agents-list-empty.png index 28a4429..966e240 100644 Binary files a/docs/user-guides/screenshots/light/agents/agents-list-empty.png and b/docs/user-guides/screenshots/light/agents/agents-list-empty.png differ diff --git a/docs/user-guides/screenshots/light/agents/budget-and-heartbeat-fields.png b/docs/user-guides/screenshots/light/agents/budget-and-heartbeat-fields.png index 9d039a6..efc1bc5 100644 Binary files a/docs/user-guides/screenshots/light/agents/budget-and-heartbeat-fields.png and b/docs/user-guides/screenshots/light/agents/budget-and-heartbeat-fields.png differ diff --git a/docs/user-guides/screenshots/light/agents/budget.png b/docs/user-guides/screenshots/light/agents/budget.png index 70a8b41..1065bbb 100644 Binary files a/docs/user-guides/screenshots/light/agents/budget.png and b/docs/user-guides/screenshots/light/agents/budget.png differ diff --git a/docs/user-guides/screenshots/light/agents/claude-local-config-filled.png b/docs/user-guides/screenshots/light/agents/claude-local-config-filled.png index 2bcd467..c1f49cb 100644 Binary files a/docs/user-guides/screenshots/light/agents/claude-local-config-filled.png and b/docs/user-guides/screenshots/light/agents/claude-local-config-filled.png differ diff --git a/docs/user-guides/screenshots/light/agents/codex-local-config.png b/docs/user-guides/screenshots/light/agents/codex-local-config.png index 9578926..8e07ec3 100644 Binary files a/docs/user-guides/screenshots/light/agents/codex-local-config.png and b/docs/user-guides/screenshots/light/agents/codex-local-config.png differ diff --git a/docs/user-guides/screenshots/light/agents/configuration.png b/docs/user-guides/screenshots/light/agents/configuration.png index 099002a..fdda6bd 100644 Binary files a/docs/user-guides/screenshots/light/agents/configuration.png and b/docs/user-guides/screenshots/light/agents/configuration.png differ diff --git a/docs/user-guides/screenshots/light/agents/dashboard.png b/docs/user-guides/screenshots/light/agents/dashboard.png index 5858505..cd6d20a 100644 Binary files a/docs/user-guides/screenshots/light/agents/dashboard.png and b/docs/user-guides/screenshots/light/agents/dashboard.png differ diff --git a/docs/user-guides/screenshots/light/agents/heartbeat-toggle-disabled.png b/docs/user-guides/screenshots/light/agents/heartbeat-toggle-disabled.png index c7c7a5b..20a2dc3 100644 Binary files a/docs/user-guides/screenshots/light/agents/heartbeat-toggle-disabled.png and b/docs/user-guides/screenshots/light/agents/heartbeat-toggle-disabled.png differ diff --git a/docs/user-guides/screenshots/light/agents/heartbeat-toggle-enabled.png b/docs/user-guides/screenshots/light/agents/heartbeat-toggle-enabled.png index 51a4afc..7e2bc2e 100644 Binary files a/docs/user-guides/screenshots/light/agents/heartbeat-toggle-enabled.png and b/docs/user-guides/screenshots/light/agents/heartbeat-toggle-enabled.png differ diff --git a/docs/user-guides/screenshots/light/agents/http-adapter-config.png b/docs/user-guides/screenshots/light/agents/http-adapter-config.png index e73226a..1a3e7b1 100644 Binary files a/docs/user-guides/screenshots/light/agents/http-adapter-config.png and b/docs/user-guides/screenshots/light/agents/http-adapter-config.png differ diff --git a/docs/user-guides/screenshots/light/agents/instructions.png b/docs/user-guides/screenshots/light/agents/instructions.png index be0b048..5467c0f 100644 Binary files a/docs/user-guides/screenshots/light/agents/instructions.png and b/docs/user-guides/screenshots/light/agents/instructions.png differ diff --git a/docs/user-guides/screenshots/light/agents/list.png b/docs/user-guides/screenshots/light/agents/list.png index 47b3892..20466bc 100644 Binary files a/docs/user-guides/screenshots/light/agents/list.png and b/docs/user-guides/screenshots/light/agents/list.png differ diff --git a/docs/user-guides/screenshots/light/agents/new-agent-name-role.png b/docs/user-guides/screenshots/light/agents/new-agent-name-role.png index 17de532..f058d54 100644 Binary files a/docs/user-guides/screenshots/light/agents/new-agent-name-role.png and b/docs/user-guides/screenshots/light/agents/new-agent-name-role.png differ diff --git a/docs/user-guides/screenshots/light/agents/new-agent-reports-to-field.png b/docs/user-guides/screenshots/light/agents/new-agent-reports-to-field.png index 202e7c3..a06df47 100644 Binary files a/docs/user-guides/screenshots/light/agents/new-agent-reports-to-field.png and b/docs/user-guides/screenshots/light/agents/new-agent-reports-to-field.png differ diff --git a/docs/user-guides/screenshots/light/agents/new.png b/docs/user-guides/screenshots/light/agents/new.png index 9d039a6..7dc8b74 100644 Binary files a/docs/user-guides/screenshots/light/agents/new.png and b/docs/user-guides/screenshots/light/agents/new.png differ diff --git a/docs/user-guides/screenshots/light/agents/run-history-in-progress.png b/docs/user-guides/screenshots/light/agents/run-history-in-progress.png index f5074f3..e28b10e 100644 Binary files a/docs/user-guides/screenshots/light/agents/run-history-in-progress.png and b/docs/user-guides/screenshots/light/agents/run-history-in-progress.png differ diff --git a/docs/user-guides/screenshots/light/agents/run-transcript-view.png b/docs/user-guides/screenshots/light/agents/run-transcript-view.png index 4d2fb06..3b564ff 100644 Binary files a/docs/user-guides/screenshots/light/agents/run-transcript-view.png and b/docs/user-guides/screenshots/light/agents/run-transcript-view.png differ diff --git a/docs/user-guides/screenshots/light/agents/runs.png b/docs/user-guides/screenshots/light/agents/runs.png index 9688d73..94c0838 100644 Binary files a/docs/user-guides/screenshots/light/agents/runs.png and b/docs/user-guides/screenshots/light/agents/runs.png differ diff --git a/docs/user-guides/screenshots/light/agents/skills.png b/docs/user-guides/screenshots/light/agents/skills.png index 3db1ddb..65c5662 100644 Binary files a/docs/user-guides/screenshots/light/agents/skills.png and b/docs/user-guides/screenshots/light/agents/skills.png differ diff --git a/docs/user-guides/screenshots/light/agents/test-environment-fail.png b/docs/user-guides/screenshots/light/agents/test-environment-fail.png index caa9e53..015bc43 100644 Binary files a/docs/user-guides/screenshots/light/agents/test-environment-fail.png and b/docs/user-guides/screenshots/light/agents/test-environment-fail.png differ diff --git a/docs/user-guides/screenshots/light/agents/test-environment-success.png b/docs/user-guides/screenshots/light/agents/test-environment-success.png index b09759e..795606a 100644 Binary files a/docs/user-guides/screenshots/light/agents/test-environment-success.png and b/docs/user-guides/screenshots/light/agents/test-environment-success.png differ diff --git a/docs/user-guides/screenshots/light/approvals/approvals-list.png b/docs/user-guides/screenshots/light/approvals/approvals-list.png index 4388e97..41893a9 100644 Binary files a/docs/user-guides/screenshots/light/approvals/approvals-list.png and b/docs/user-guides/screenshots/light/approvals/approvals-list.png differ diff --git a/docs/user-guides/screenshots/light/approvals/approvals-queue-strategy.png b/docs/user-guides/screenshots/light/approvals/approvals-queue-strategy.png index a56c74b..36f2728 100644 Binary files a/docs/user-guides/screenshots/light/approvals/approvals-queue-strategy.png and b/docs/user-guides/screenshots/light/approvals/approvals-queue-strategy.png differ diff --git a/docs/user-guides/screenshots/light/approvals/approve-reject-buttons.png b/docs/user-guides/screenshots/light/approvals/approve-reject-buttons.png index 434ce96..4507921 100644 Binary files a/docs/user-guides/screenshots/light/approvals/approve-reject-buttons.png and b/docs/user-guides/screenshots/light/approvals/approve-reject-buttons.png differ diff --git a/docs/user-guides/screenshots/light/approvals/approve-reject-revision-buttons.png b/docs/user-guides/screenshots/light/approvals/approve-reject-revision-buttons.png index 7a3ab21..81aea9c 100644 Binary files a/docs/user-guides/screenshots/light/approvals/approve-reject-revision-buttons.png and b/docs/user-guides/screenshots/light/approvals/approve-reject-revision-buttons.png differ diff --git a/docs/user-guides/screenshots/light/approvals/approved-approval.png b/docs/user-guides/screenshots/light/approvals/approved-approval.png index 1a24504..fa35cd3 100644 Binary files a/docs/user-guides/screenshots/light/approvals/approved-approval.png and b/docs/user-guides/screenshots/light/approvals/approved-approval.png differ diff --git a/docs/user-guides/screenshots/light/approvals/hire-approval-detail.png b/docs/user-guides/screenshots/light/approvals/hire-approval-detail.png index 7a3ab21..cc1af4a 100644 Binary files a/docs/user-guides/screenshots/light/approvals/hire-approval-detail.png and b/docs/user-guides/screenshots/light/approvals/hire-approval-detail.png differ diff --git a/docs/user-guides/screenshots/light/approvals/queue-filters.png b/docs/user-guides/screenshots/light/approvals/queue-filters.png index a56c74b..8b5c452 100644 Binary files a/docs/user-guides/screenshots/light/approvals/queue-filters.png and b/docs/user-guides/screenshots/light/approvals/queue-filters.png differ diff --git a/docs/user-guides/screenshots/light/approvals/revision-request-input.png b/docs/user-guides/screenshots/light/approvals/revision-request-input.png index fa8dd28..ef1c723 100644 Binary files a/docs/user-guides/screenshots/light/approvals/revision-request-input.png and b/docs/user-guides/screenshots/light/approvals/revision-request-input.png differ diff --git a/docs/user-guides/screenshots/light/approvals/strategy-approval-detail.png b/docs/user-guides/screenshots/light/approvals/strategy-approval-detail.png index dd836f8..571bbb0 100644 Binary files a/docs/user-guides/screenshots/light/approvals/strategy-approval-detail.png and b/docs/user-guides/screenshots/light/approvals/strategy-approval-detail.png differ diff --git a/docs/user-guides/screenshots/light/company/access.png b/docs/user-guides/screenshots/light/company/access.png index 35c5088..fd206a5 100644 Binary files a/docs/user-guides/screenshots/light/company/access.png and b/docs/user-guides/screenshots/light/company/access.png differ diff --git a/docs/user-guides/screenshots/light/company/company-budget-setting.png b/docs/user-guides/screenshots/light/company/company-budget-setting.png index 9ab242a..3466690 100644 Binary files a/docs/user-guides/screenshots/light/company/company-budget-setting.png and b/docs/user-guides/screenshots/light/company/company-budget-setting.png differ diff --git a/docs/user-guides/screenshots/light/company/company-goal-field.png b/docs/user-guides/screenshots/light/company/company-goal-field.png index 65d3406..3c1b0b3 100644 Binary files a/docs/user-guides/screenshots/light/company/company-goal-field.png and b/docs/user-guides/screenshots/light/company/company-goal-field.png differ diff --git a/docs/user-guides/screenshots/light/company/company-saved.png b/docs/user-guides/screenshots/light/company/company-saved.png index fcf7795..e101f7b 100644 Binary files a/docs/user-guides/screenshots/light/company/company-saved.png and b/docs/user-guides/screenshots/light/company/company-saved.png differ diff --git a/docs/user-guides/screenshots/light/company/export.png b/docs/user-guides/screenshots/light/company/export.png index 1c13eb6..ade2aec 100644 Binary files a/docs/user-guides/screenshots/light/company/export.png and b/docs/user-guides/screenshots/light/company/export.png differ diff --git a/docs/user-guides/screenshots/light/company/import.png b/docs/user-guides/screenshots/light/company/import.png index 0e246c5..c59f0f3 100644 Binary files a/docs/user-guides/screenshots/light/company/import.png and b/docs/user-guides/screenshots/light/company/import.png differ diff --git a/docs/user-guides/screenshots/light/company/invites.png b/docs/user-guides/screenshots/light/company/invites.png index baa1252..e587a12 100644 Binary files a/docs/user-guides/screenshots/light/company/invites.png and b/docs/user-guides/screenshots/light/company/invites.png differ diff --git a/docs/user-guides/screenshots/light/company/join-requests.png b/docs/user-guides/screenshots/light/company/join-requests.png index c6e7ab4..98ad1b3 100644 Binary files a/docs/user-guides/screenshots/light/company/join-requests.png and b/docs/user-guides/screenshots/light/company/join-requests.png differ diff --git a/docs/user-guides/screenshots/light/company/new-company-form.png b/docs/user-guides/screenshots/light/company/new-company-form.png index 4fd460c..c6be259 100644 Binary files a/docs/user-guides/screenshots/light/company/new-company-form.png and b/docs/user-guides/screenshots/light/company/new-company-form.png differ diff --git a/docs/user-guides/screenshots/light/company/settings.png b/docs/user-guides/screenshots/light/company/settings.png index 7710eba..8caee98 100644 Binary files a/docs/user-guides/screenshots/light/company/settings.png and b/docs/user-guides/screenshots/light/company/settings.png differ diff --git a/docs/user-guides/screenshots/light/costs/agent-budget-100pct-paused.png b/docs/user-guides/screenshots/light/costs/agent-budget-100pct-paused.png index e621689..6719e65 100644 Binary files a/docs/user-guides/screenshots/light/costs/agent-budget-100pct-paused.png and b/docs/user-guides/screenshots/light/costs/agent-budget-100pct-paused.png differ diff --git a/docs/user-guides/screenshots/light/costs/agent-budget-50pct.png b/docs/user-guides/screenshots/light/costs/agent-budget-50pct.png index 1ce4cd7..5acc201 100644 Binary files a/docs/user-guides/screenshots/light/costs/agent-budget-50pct.png and b/docs/user-guides/screenshots/light/costs/agent-budget-50pct.png differ diff --git a/docs/user-guides/screenshots/light/costs/agent-budget-80pct-warning.png b/docs/user-guides/screenshots/light/costs/agent-budget-80pct-warning.png index 83e04c9..a4cc327 100644 Binary files a/docs/user-guides/screenshots/light/costs/agent-budget-80pct-warning.png and b/docs/user-guides/screenshots/light/costs/agent-budget-80pct-warning.png differ diff --git a/docs/user-guides/screenshots/light/costs/agent-budget-bar.png b/docs/user-guides/screenshots/light/costs/agent-budget-bar.png index 1db3ff5..bbf7be7 100644 Binary files a/docs/user-guides/screenshots/light/costs/agent-budget-bar.png and b/docs/user-guides/screenshots/light/costs/agent-budget-bar.png differ diff --git a/docs/user-guides/screenshots/light/costs/agent-budget-field.png b/docs/user-guides/screenshots/light/costs/agent-budget-field.png index 03a83d0..7fbf4da 100644 Binary files a/docs/user-guides/screenshots/light/costs/agent-budget-field.png and b/docs/user-guides/screenshots/light/costs/agent-budget-field.png differ diff --git a/docs/user-guides/screenshots/light/costs/billers.png b/docs/user-guides/screenshots/light/costs/billers.png index 4aeea44..2022b1e 100644 Binary files a/docs/user-guides/screenshots/light/costs/billers.png and b/docs/user-guides/screenshots/light/costs/billers.png differ diff --git a/docs/user-guides/screenshots/light/costs/budgets.png b/docs/user-guides/screenshots/light/costs/budgets.png index 4aeea44..e7b2d75 100644 Binary files a/docs/user-guides/screenshots/light/costs/budgets.png and b/docs/user-guides/screenshots/light/costs/budgets.png differ diff --git a/docs/user-guides/screenshots/light/costs/company-budget-field.png b/docs/user-guides/screenshots/light/costs/company-budget-field.png index b29cd25..32a3f7d 100644 Binary files a/docs/user-guides/screenshots/light/costs/company-budget-field.png and b/docs/user-guides/screenshots/light/costs/company-budget-field.png differ diff --git a/docs/user-guides/screenshots/light/costs/costs-dashboard-overview.png b/docs/user-guides/screenshots/light/costs/costs-dashboard-overview.png index c0f378a..588867d 100644 Binary files a/docs/user-guides/screenshots/light/costs/costs-dashboard-overview.png and b/docs/user-guides/screenshots/light/costs/costs-dashboard-overview.png differ diff --git a/docs/user-guides/screenshots/light/costs/finance.png b/docs/user-guides/screenshots/light/costs/finance.png index 23d707d..2022b1e 100644 Binary files a/docs/user-guides/screenshots/light/costs/finance.png and b/docs/user-guides/screenshots/light/costs/finance.png differ diff --git a/docs/user-guides/screenshots/light/costs/overview.png b/docs/user-guides/screenshots/light/costs/overview.png index dcc636e..d418f24 100644 Binary files a/docs/user-guides/screenshots/light/costs/overview.png and b/docs/user-guides/screenshots/light/costs/overview.png differ diff --git a/docs/user-guides/screenshots/light/costs/per-run-cost-detail.png b/docs/user-guides/screenshots/light/costs/per-run-cost-detail.png index de0bcf1..63b16e0 100644 Binary files a/docs/user-guides/screenshots/light/costs/per-run-cost-detail.png and b/docs/user-guides/screenshots/light/costs/per-run-cost-detail.png differ diff --git a/docs/user-guides/screenshots/light/costs/providers.png b/docs/user-guides/screenshots/light/costs/providers.png index 23d707d..ca75faf 100644 Binary files a/docs/user-guides/screenshots/light/costs/providers.png and b/docs/user-guides/screenshots/light/costs/providers.png differ diff --git a/docs/user-guides/screenshots/light/dashboard/activity-feed.png b/docs/user-guides/screenshots/light/dashboard/activity-feed.png index 75ee16f..0994106 100644 Binary files a/docs/user-guides/screenshots/light/dashboard/activity-feed.png and b/docs/user-guides/screenshots/light/dashboard/activity-feed.png differ diff --git a/docs/user-guides/screenshots/light/dashboard/agent-status-panel.png b/docs/user-guides/screenshots/light/dashboard/agent-status-panel.png index 4a39f1c..abd01ef 100644 Binary files a/docs/user-guides/screenshots/light/dashboard/agent-status-panel.png and b/docs/user-guides/screenshots/light/dashboard/agent-status-panel.png differ diff --git a/docs/user-guides/screenshots/light/dashboard/cost-summary-panel.png b/docs/user-guides/screenshots/light/dashboard/cost-summary-panel.png index a1088dc..a081562 100644 Binary files a/docs/user-guides/screenshots/light/dashboard/cost-summary-panel.png and b/docs/user-guides/screenshots/light/dashboard/cost-summary-panel.png differ diff --git a/docs/user-guides/screenshots/light/dashboard/dashboard-overview.png b/docs/user-guides/screenshots/light/dashboard/dashboard-overview.png index f1c4455..005a213 100644 Binary files a/docs/user-guides/screenshots/light/dashboard/dashboard-overview.png and b/docs/user-guides/screenshots/light/dashboard/dashboard-overview.png differ diff --git a/docs/user-guides/screenshots/light/dashboard/stale-tasks-panel.png b/docs/user-guides/screenshots/light/dashboard/stale-tasks-panel.png deleted file mode 100644 index 4c06215..0000000 Binary files a/docs/user-guides/screenshots/light/dashboard/stale-tasks-panel.png and /dev/null differ diff --git a/docs/user-guides/screenshots/light/dashboard/task-breakdown-panel.png b/docs/user-guides/screenshots/light/dashboard/task-breakdown-panel.png index 201b119..0bb6b78 100644 Binary files a/docs/user-guides/screenshots/light/dashboard/task-breakdown-panel.png and b/docs/user-guides/screenshots/light/dashboard/task-breakdown-panel.png differ diff --git a/docs/user-guides/screenshots/light/decisions/decision-card.png b/docs/user-guides/screenshots/light/decisions/decision-card.png new file mode 100644 index 0000000..56a0590 Binary files /dev/null and b/docs/user-guides/screenshots/light/decisions/decision-card.png differ diff --git a/docs/user-guides/screenshots/light/decisions/empty-state.png b/docs/user-guides/screenshots/light/decisions/empty-state.png new file mode 100644 index 0000000..111d14c Binary files /dev/null and b/docs/user-guides/screenshots/light/decisions/empty-state.png differ diff --git a/docs/user-guides/screenshots/light/decisions/history-curtains.png b/docs/user-guides/screenshots/light/decisions/history-curtains.png new file mode 100644 index 0000000..1aa7a1f Binary files /dev/null and b/docs/user-guides/screenshots/light/decisions/history-curtains.png differ diff --git a/docs/user-guides/screenshots/light/decisions/named-queue.png b/docs/user-guides/screenshots/light/decisions/named-queue.png new file mode 100644 index 0000000..887a8a8 Binary files /dev/null and b/docs/user-guides/screenshots/light/decisions/named-queue.png differ diff --git a/docs/user-guides/screenshots/light/decisions/queue-overview.png b/docs/user-guides/screenshots/light/decisions/queue-overview.png new file mode 100644 index 0000000..0121550 Binary files /dev/null and b/docs/user-guides/screenshots/light/decisions/queue-overview.png differ diff --git a/docs/user-guides/screenshots/light/decisions/toolbar-filters.png b/docs/user-guides/screenshots/light/decisions/toolbar-filters.png new file mode 100644 index 0000000..950d7d6 Binary files /dev/null and b/docs/user-guides/screenshots/light/decisions/toolbar-filters.png differ diff --git a/docs/user-guides/screenshots/light/decisions/triage-chips.png b/docs/user-guides/screenshots/light/decisions/triage-chips.png new file mode 100644 index 0000000..f9e761a Binary files /dev/null and b/docs/user-guides/screenshots/light/decisions/triage-chips.png differ diff --git a/docs/user-guides/screenshots/light/experimental/cloud-upstream.png b/docs/user-guides/screenshots/light/experimental/cloud-upstream.png index d64a1fd..92b88f8 100644 Binary files a/docs/user-guides/screenshots/light/experimental/cloud-upstream.png and b/docs/user-guides/screenshots/light/experimental/cloud-upstream.png differ diff --git a/docs/user-guides/screenshots/light/experimental/environments.png b/docs/user-guides/screenshots/light/experimental/environments.png index 7cf3c70..9297b76 100644 Binary files a/docs/user-guides/screenshots/light/experimental/environments.png and b/docs/user-guides/screenshots/light/experimental/environments.png differ diff --git a/docs/user-guides/screenshots/light/experimental/external-objects.png b/docs/user-guides/screenshots/light/experimental/external-objects.png index e718b6d..067c0b1 100644 Binary files a/docs/user-guides/screenshots/light/experimental/external-objects.png and b/docs/user-guides/screenshots/light/experimental/external-objects.png differ diff --git a/docs/user-guides/screenshots/light/experimental/file-viewer-browse.png b/docs/user-guides/screenshots/light/experimental/file-viewer-browse.png index b8189be..3216e83 100644 Binary files a/docs/user-guides/screenshots/light/experimental/file-viewer-browse.png and b/docs/user-guides/screenshots/light/experimental/file-viewer-browse.png differ diff --git a/docs/user-guides/screenshots/light/experimental/recovery-preview.png b/docs/user-guides/screenshots/light/experimental/recovery-preview.png index e120b38..8052487 100644 Binary files a/docs/user-guides/screenshots/light/experimental/recovery-preview.png and b/docs/user-guides/screenshots/light/experimental/recovery-preview.png differ diff --git a/docs/user-guides/screenshots/light/experimental/server-info.png b/docs/user-guides/screenshots/light/experimental/server-info.png index 03aeb0f..3515391 100644 Binary files a/docs/user-guides/screenshots/light/experimental/server-info.png and b/docs/user-guides/screenshots/light/experimental/server-info.png differ diff --git a/docs/user-guides/screenshots/light/export-import/export-dialog.png b/docs/user-guides/screenshots/light/export-import/export-dialog.png index 5bafc5d..4b0e6ab 100644 Binary files a/docs/user-guides/screenshots/light/export-import/export-dialog.png and b/docs/user-guides/screenshots/light/export-import/export-dialog.png differ diff --git a/docs/user-guides/screenshots/light/export-import/import-dialog.png b/docs/user-guides/screenshots/light/export-import/import-dialog.png index a844fea..72809cc 100644 Binary files a/docs/user-guides/screenshots/light/export-import/import-dialog.png and b/docs/user-guides/screenshots/light/export-import/import-dialog.png differ diff --git a/docs/user-guides/screenshots/light/goals/detail.png b/docs/user-guides/screenshots/light/goals/detail.png index c5ac320..853c8f2 100644 Binary files a/docs/user-guides/screenshots/light/goals/detail.png and b/docs/user-guides/screenshots/light/goals/detail.png differ diff --git a/docs/user-guides/screenshots/light/goals/list.png b/docs/user-guides/screenshots/light/goals/list.png index 1c17e68..6f00aa0 100644 Binary files a/docs/user-guides/screenshots/light/goals/list.png and b/docs/user-guides/screenshots/light/goals/list.png differ diff --git a/docs/user-guides/screenshots/light/interactions/checkbox-confirmation-card.png b/docs/user-guides/screenshots/light/interactions/checkbox-confirmation-card.png index 43f4388..aed9856 100644 Binary files a/docs/user-guides/screenshots/light/interactions/checkbox-confirmation-card.png and b/docs/user-guides/screenshots/light/interactions/checkbox-confirmation-card.png differ diff --git a/docs/user-guides/screenshots/light/issues/detail-activity.png b/docs/user-guides/screenshots/light/issues/detail-activity.png index 851218a..a5b113b 100644 Binary files a/docs/user-guides/screenshots/light/issues/detail-activity.png and b/docs/user-guides/screenshots/light/issues/detail-activity.png differ diff --git a/docs/user-guides/screenshots/light/issues/detail-chat.png b/docs/user-guides/screenshots/light/issues/detail-chat.png index aa2cf9b..3fab880 100644 Binary files a/docs/user-guides/screenshots/light/issues/detail-chat.png and b/docs/user-guides/screenshots/light/issues/detail-chat.png differ diff --git a/docs/user-guides/screenshots/light/issues/detail-sidebar.png b/docs/user-guides/screenshots/light/issues/detail-sidebar.png index 99ef25b..0447cfc 100644 Binary files a/docs/user-guides/screenshots/light/issues/detail-sidebar.png and b/docs/user-guides/screenshots/light/issues/detail-sidebar.png differ diff --git a/docs/user-guides/screenshots/light/issues/inbox-requests.png b/docs/user-guides/screenshots/light/issues/inbox-requests.png index 8fa5880..7b89b28 100644 Binary files a/docs/user-guides/screenshots/light/issues/inbox-requests.png and b/docs/user-guides/screenshots/light/issues/inbox-requests.png differ diff --git a/docs/user-guides/screenshots/light/issues/inbox-unread.png b/docs/user-guides/screenshots/light/issues/inbox-unread.png index b935fd4..3afcec5 100644 Binary files a/docs/user-guides/screenshots/light/issues/inbox-unread.png and b/docs/user-guides/screenshots/light/issues/inbox-unread.png differ diff --git a/docs/user-guides/screenshots/light/issues/inbox.png b/docs/user-guides/screenshots/light/issues/inbox.png index 3f3684e..8ae28d0 100644 Binary files a/docs/user-guides/screenshots/light/issues/inbox.png and b/docs/user-guides/screenshots/light/issues/inbox.png differ diff --git a/docs/user-guides/screenshots/light/issues/list.png b/docs/user-guides/screenshots/light/issues/list.png index 267d3fd..ebe7351 100644 Binary files a/docs/user-guides/screenshots/light/issues/list.png and b/docs/user-guides/screenshots/light/issues/list.png differ diff --git a/docs/user-guides/screenshots/light/issues/my-issues.png b/docs/user-guides/screenshots/light/issues/my-issues.png index ac34799..10209b0 100644 Binary files a/docs/user-guides/screenshots/light/issues/my-issues.png and b/docs/user-guides/screenshots/light/issues/my-issues.png differ diff --git a/docs/user-guides/screenshots/light/onboarding/budget-field.png b/docs/user-guides/screenshots/light/onboarding/budget-field.png index 075b412..4c07c0c 100644 Binary files a/docs/user-guides/screenshots/light/onboarding/budget-field.png and b/docs/user-guides/screenshots/light/onboarding/budget-field.png differ diff --git a/docs/user-guides/screenshots/light/onboarding/empty-dashboard.png b/docs/user-guides/screenshots/light/onboarding/empty-dashboard.png index d7d8f42..4784188 100644 Binary files a/docs/user-guides/screenshots/light/onboarding/empty-dashboard.png and b/docs/user-guides/screenshots/light/onboarding/empty-dashboard.png differ diff --git a/docs/user-guides/screenshots/light/onboarding/goal-field.png b/docs/user-guides/screenshots/light/onboarding/goal-field.png index b496c5d..8dfd02b 100644 Binary files a/docs/user-guides/screenshots/light/onboarding/goal-field.png and b/docs/user-guides/screenshots/light/onboarding/goal-field.png differ diff --git a/docs/user-guides/screenshots/light/onboarding/new-company-modal-empty.png b/docs/user-guides/screenshots/light/onboarding/new-company-modal-empty.png index f3c1896..5aab0db 100644 Binary files a/docs/user-guides/screenshots/light/onboarding/new-company-modal-empty.png and b/docs/user-guides/screenshots/light/onboarding/new-company-modal-empty.png differ diff --git a/docs/user-guides/screenshots/light/onboarding/sidebar-new-company-button.png b/docs/user-guides/screenshots/light/onboarding/sidebar-new-company-button.png index 06b7bc9..afd45e5 100644 Binary files a/docs/user-guides/screenshots/light/onboarding/sidebar-new-company-button.png and b/docs/user-guides/screenshots/light/onboarding/sidebar-new-company-button.png differ diff --git a/docs/user-guides/screenshots/light/org/chart.png b/docs/user-guides/screenshots/light/org/chart.png index b05cf3d..79b361a 100644 Binary files a/docs/user-guides/screenshots/light/org/chart.png and b/docs/user-guides/screenshots/light/org/chart.png differ diff --git a/docs/user-guides/screenshots/light/org/org-chart-add-agent.png b/docs/user-guides/screenshots/light/org/org-chart-add-agent.png index 0f8cf64..09d6b14 100644 Binary files a/docs/user-guides/screenshots/light/org/org-chart-add-agent.png and b/docs/user-guides/screenshots/light/org/org-chart-add-agent.png differ diff --git a/docs/user-guides/screenshots/light/org/org-chart-manager-assigned.png b/docs/user-guides/screenshots/light/org/org-chart-manager-assigned.png index 0f8cf64..a521f44 100644 Binary files a/docs/user-guides/screenshots/light/org/org-chart-manager-assigned.png and b/docs/user-guides/screenshots/light/org/org-chart-manager-assigned.png differ diff --git a/docs/user-guides/screenshots/light/org/org-chart-small-team.png b/docs/user-guides/screenshots/light/org/org-chart-small-team.png index f1ad6b6..9b317de 100644 Binary files a/docs/user-guides/screenshots/light/org/org-chart-small-team.png and b/docs/user-guides/screenshots/light/org/org-chart-small-team.png differ diff --git a/docs/user-guides/screenshots/light/org/org-chart-view.png b/docs/user-guides/screenshots/light/org/org-chart-view.png index f1ad6b6..d6a117e 100644 Binary files a/docs/user-guides/screenshots/light/org/org-chart-view.png and b/docs/user-guides/screenshots/light/org/org-chart-view.png differ diff --git a/docs/user-guides/screenshots/light/org/reassign.png b/docs/user-guides/screenshots/light/org/reassign.png index dfeee9b..eb8fe66 100644 Binary files a/docs/user-guides/screenshots/light/org/reassign.png and b/docs/user-guides/screenshots/light/org/reassign.png differ diff --git a/docs/user-guides/screenshots/light/org/skills-list.png b/docs/user-guides/screenshots/light/org/skills-list.png index b96b5c0..df16fac 100644 Binary files a/docs/user-guides/screenshots/light/org/skills-list.png and b/docs/user-guides/screenshots/light/org/skills-list.png differ diff --git a/docs/user-guides/screenshots/light/org/workspace-modes.png b/docs/user-guides/screenshots/light/org/workspace-modes.png index c779187..2647155 100644 Binary files a/docs/user-guides/screenshots/light/org/workspace-modes.png and b/docs/user-guides/screenshots/light/org/workspace-modes.png differ diff --git a/docs/user-guides/screenshots/light/plugins/detail.png b/docs/user-guides/screenshots/light/plugins/detail.png index 5774452..69f5914 100644 Binary files a/docs/user-guides/screenshots/light/plugins/detail.png and b/docs/user-guides/screenshots/light/plugins/detail.png differ diff --git a/docs/user-guides/screenshots/light/plugins/install.png b/docs/user-guides/screenshots/light/plugins/install.png index 5774452..dc63118 100644 Binary files a/docs/user-guides/screenshots/light/plugins/install.png and b/docs/user-guides/screenshots/light/plugins/install.png differ diff --git a/docs/user-guides/screenshots/light/plugins/jobs-log.png b/docs/user-guides/screenshots/light/plugins/jobs-log.png index 5774452..82dc6e1 100644 Binary files a/docs/user-guides/screenshots/light/plugins/jobs-log.png and b/docs/user-guides/screenshots/light/plugins/jobs-log.png differ diff --git a/docs/user-guides/screenshots/light/plugins/list.png b/docs/user-guides/screenshots/light/plugins/list.png index 5774452..f8f12e8 100644 Binary files a/docs/user-guides/screenshots/light/plugins/list.png and b/docs/user-guides/screenshots/light/plugins/list.png differ diff --git a/docs/user-guides/screenshots/light/plugins/settings.png b/docs/user-guides/screenshots/light/plugins/settings.png index 5774452..58de6fb 100644 Binary files a/docs/user-guides/screenshots/light/plugins/settings.png and b/docs/user-guides/screenshots/light/plugins/settings.png differ diff --git a/docs/user-guides/screenshots/light/projects/budget.png b/docs/user-guides/screenshots/light/projects/budget.png index 088876d..230d02b 100644 Binary files a/docs/user-guides/screenshots/light/projects/budget.png and b/docs/user-guides/screenshots/light/projects/budget.png differ diff --git a/docs/user-guides/screenshots/light/projects/configuration.png b/docs/user-guides/screenshots/light/projects/configuration.png index 31ded2b..519520c 100644 Binary files a/docs/user-guides/screenshots/light/projects/configuration.png and b/docs/user-guides/screenshots/light/projects/configuration.png differ diff --git a/docs/user-guides/screenshots/light/projects/issues.png b/docs/user-guides/screenshots/light/projects/issues.png index 8f62a04..872c122 100644 Binary files a/docs/user-guides/screenshots/light/projects/issues.png and b/docs/user-guides/screenshots/light/projects/issues.png differ diff --git a/docs/user-guides/screenshots/light/projects/list.png b/docs/user-guides/screenshots/light/projects/list.png index 654cdf6..14e010e 100644 Binary files a/docs/user-guides/screenshots/light/projects/list.png and b/docs/user-guides/screenshots/light/projects/list.png differ diff --git a/docs/user-guides/screenshots/light/projects/overview.png b/docs/user-guides/screenshots/light/projects/overview.png index b307886..85b5e42 100644 Binary files a/docs/user-guides/screenshots/light/projects/overview.png and b/docs/user-guides/screenshots/light/projects/overview.png differ diff --git a/docs/user-guides/screenshots/light/projects/workspaces.png b/docs/user-guides/screenshots/light/projects/workspaces.png index 282267c..baf985d 100644 Binary files a/docs/user-guides/screenshots/light/projects/workspaces.png and b/docs/user-guides/screenshots/light/projects/workspaces.png differ diff --git a/docs/user-guides/screenshots/light/routines/cron-picker.png b/docs/user-guides/screenshots/light/routines/cron-picker.png index 3bebf8e..0bf3860 100644 Binary files a/docs/user-guides/screenshots/light/routines/cron-picker.png and b/docs/user-guides/screenshots/light/routines/cron-picker.png differ diff --git a/docs/user-guides/screenshots/light/routines/detail.png b/docs/user-guides/screenshots/light/routines/detail.png index d3b81a7..98803dd 100644 Binary files a/docs/user-guides/screenshots/light/routines/detail.png and b/docs/user-guides/screenshots/light/routines/detail.png differ diff --git a/docs/user-guides/screenshots/light/routines/list.png b/docs/user-guides/screenshots/light/routines/list.png index 3e5a350..da7dfb2 100644 Binary files a/docs/user-guides/screenshots/light/routines/list.png and b/docs/user-guides/screenshots/light/routines/list.png differ diff --git a/docs/user-guides/screenshots/light/routines/run-history.png b/docs/user-guides/screenshots/light/routines/run-history.png index ebcb09d..0958b7b 100644 Binary files a/docs/user-guides/screenshots/light/routines/run-history.png and b/docs/user-guides/screenshots/light/routines/run-history.png differ diff --git a/docs/user-guides/screenshots/light/secrets/dispatch-check.png b/docs/user-guides/screenshots/light/secrets/dispatch-check.png index 7fd6c6e..f6fcf1e 100644 Binary files a/docs/user-guides/screenshots/light/secrets/dispatch-check.png and b/docs/user-guides/screenshots/light/secrets/dispatch-check.png differ diff --git a/docs/user-guides/screenshots/light/secrets/per-user-value-entry.png b/docs/user-guides/screenshots/light/secrets/per-user-value-entry.png index 0fa7a3b..fd36615 100644 Binary files a/docs/user-guides/screenshots/light/secrets/per-user-value-entry.png and b/docs/user-guides/screenshots/light/secrets/per-user-value-entry.png differ diff --git a/docs/user-guides/screenshots/light/secrets/proposals-tab.png b/docs/user-guides/screenshots/light/secrets/proposals-tab.png new file mode 100644 index 0000000..3f50b61 Binary files /dev/null and b/docs/user-guides/screenshots/light/secrets/proposals-tab.png differ diff --git a/docs/user-guides/screenshots/light/secrets/user-secret-definition.png b/docs/user-guides/screenshots/light/secrets/user-secret-definition.png index 7fd6c6e..a4f2d19 100644 Binary files a/docs/user-guides/screenshots/light/secrets/user-secret-definition.png and b/docs/user-guides/screenshots/light/secrets/user-secret-definition.png differ diff --git a/docs/user-guides/screenshots/light/settings/experimental.png b/docs/user-guides/screenshots/light/settings/experimental.png index d67601d..c19e0aa 100644 Binary files a/docs/user-guides/screenshots/light/settings/experimental.png and b/docs/user-guides/screenshots/light/settings/experimental.png differ diff --git a/docs/user-guides/screenshots/light/settings/instance-access.png b/docs/user-guides/screenshots/light/settings/instance-access.png index bfb1dd7..853477c 100644 Binary files a/docs/user-guides/screenshots/light/settings/instance-access.png and b/docs/user-guides/screenshots/light/settings/instance-access.png differ diff --git a/docs/user-guides/screenshots/light/settings/instance-adapters.png b/docs/user-guides/screenshots/light/settings/instance-adapters.png index bec2cf5..48ed615 100644 Binary files a/docs/user-guides/screenshots/light/settings/instance-adapters.png and b/docs/user-guides/screenshots/light/settings/instance-adapters.png differ diff --git a/docs/user-guides/screenshots/light/settings/instance-general.png b/docs/user-guides/screenshots/light/settings/instance-general.png index 7a83478..0c4ffbd 100644 Binary files a/docs/user-guides/screenshots/light/settings/instance-general.png and b/docs/user-guides/screenshots/light/settings/instance-general.png differ diff --git a/docs/user-guides/screenshots/light/settings/profile.png b/docs/user-guides/screenshots/light/settings/profile.png index 5801495..3d23d84 100644 Binary files a/docs/user-guides/screenshots/light/settings/profile.png and b/docs/user-guides/screenshots/light/settings/profile.png differ diff --git a/docs/user-guides/screenshots/light/settings/scheduler-heartbeats.png b/docs/user-guides/screenshots/light/settings/scheduler-heartbeats.png index e485205..777984d 100644 Binary files a/docs/user-guides/screenshots/light/settings/scheduler-heartbeats.png and b/docs/user-guides/screenshots/light/settings/scheduler-heartbeats.png differ diff --git a/docs/user-guides/screenshots/light/skills/add-skill-dialog-confirm.png b/docs/user-guides/screenshots/light/skills/add-skill-dialog-confirm.png index e455494..c9427a6 100644 Binary files a/docs/user-guides/screenshots/light/skills/add-skill-dialog-confirm.png and b/docs/user-guides/screenshots/light/skills/add-skill-dialog-confirm.png differ diff --git a/docs/user-guides/screenshots/light/skills/add-skill-dialog.png b/docs/user-guides/screenshots/light/skills/add-skill-dialog.png index 96c91f3..9828334 100644 Binary files a/docs/user-guides/screenshots/light/skills/add-skill-dialog.png and b/docs/user-guides/screenshots/light/skills/add-skill-dialog.png differ diff --git a/docs/user-guides/screenshots/light/skills/assign-to-agent.png b/docs/user-guides/screenshots/light/skills/assign-to-agent.png index f840b28..21ab464 100644 Binary files a/docs/user-guides/screenshots/light/skills/assign-to-agent.png and b/docs/user-guides/screenshots/light/skills/assign-to-agent.png differ diff --git a/docs/user-guides/screenshots/light/skills/file-inventory.png b/docs/user-guides/screenshots/light/skills/file-inventory.png index 2e0fa0c..8992199 100644 Binary files a/docs/user-guides/screenshots/light/skills/file-inventory.png and b/docs/user-guides/screenshots/light/skills/file-inventory.png differ diff --git a/docs/user-guides/screenshots/light/skills/list.png b/docs/user-guides/screenshots/light/skills/list.png index 2e0fa0c..df16fac 100644 Binary files a/docs/user-guides/screenshots/light/skills/list.png and b/docs/user-guides/screenshots/light/skills/list.png differ diff --git a/docs/user-guides/screenshots/light/skills/skill-detail.png b/docs/user-guides/screenshots/light/skills/skill-detail.png index 37b4dc1..079101d 100644 Binary files a/docs/user-guides/screenshots/light/skills/skill-detail.png and b/docs/user-guides/screenshots/light/skills/skill-detail.png differ diff --git a/docs/user-guides/screenshots/light/skills/skills-list.png b/docs/user-guides/screenshots/light/skills/skills-list.png index 47d53ce..8992199 100644 Binary files a/docs/user-guides/screenshots/light/skills/skills-list.png and b/docs/user-guides/screenshots/light/skills/skills-list.png differ diff --git a/docs/user-guides/screenshots/light/status-cards/board.png b/docs/user-guides/screenshots/light/status-cards/board.png new file mode 100644 index 0000000..c797241 Binary files /dev/null and b/docs/user-guides/screenshots/light/status-cards/board.png differ diff --git a/docs/user-guides/screenshots/light/status-cards/detail-history.png b/docs/user-guides/screenshots/light/status-cards/detail-history.png new file mode 100644 index 0000000..6015ff8 Binary files /dev/null and b/docs/user-guides/screenshots/light/status-cards/detail-history.png differ diff --git a/docs/user-guides/screenshots/light/status-cards/detail-settings.png b/docs/user-guides/screenshots/light/status-cards/detail-settings.png new file mode 100644 index 0000000..ede27d9 Binary files /dev/null and b/docs/user-guides/screenshots/light/status-cards/detail-settings.png differ diff --git a/docs/user-guides/screenshots/light/status-cards/detail-summary.png b/docs/user-guides/screenshots/light/status-cards/detail-summary.png new file mode 100644 index 0000000..bebb172 Binary files /dev/null and b/docs/user-guides/screenshots/light/status-cards/detail-summary.png differ diff --git a/docs/user-guides/screenshots/light/status-cards/detail-watched-issues.png b/docs/user-guides/screenshots/light/status-cards/detail-watched-issues.png new file mode 100644 index 0000000..c8bd066 Binary files /dev/null and b/docs/user-guides/screenshots/light/status-cards/detail-watched-issues.png differ diff --git a/docs/user-guides/screenshots/light/status-cards/new-card-dialog.png b/docs/user-guides/screenshots/light/status-cards/new-card-dialog.png new file mode 100644 index 0000000..22168ec Binary files /dev/null and b/docs/user-guides/screenshots/light/status-cards/new-card-dialog.png differ diff --git a/docs/user-guides/screenshots/light/task-chat/composer-modes.png b/docs/user-guides/screenshots/light/task-chat/composer-modes.png new file mode 100644 index 0000000..c11672c Binary files /dev/null and b/docs/user-guides/screenshots/light/task-chat/composer-modes.png differ diff --git a/docs/user-guides/screenshots/light/task-chat/side-pane.png b/docs/user-guides/screenshots/light/task-chat/side-pane.png new file mode 100644 index 0000000..b311905 Binary files /dev/null and b/docs/user-guides/screenshots/light/task-chat/side-pane.png differ diff --git a/docs/user-guides/screenshots/light/task-chat/thread.png b/docs/user-guides/screenshots/light/task-chat/thread.png new file mode 100644 index 0000000..b736e7d Binary files /dev/null and b/docs/user-guides/screenshots/light/task-chat/thread.png differ diff --git a/docs/user-guides/screenshots/light/tasks/comment-input-box.png b/docs/user-guides/screenshots/light/tasks/comment-input-box.png index 4c83fd4..703a293 100644 Binary files a/docs/user-guides/screenshots/light/tasks/comment-input-box.png and b/docs/user-guides/screenshots/light/tasks/comment-input-box.png differ diff --git a/docs/user-guides/screenshots/light/tasks/inbox-newly-created-tasks.png b/docs/user-guides/screenshots/light/tasks/inbox-newly-created-tasks.png index 6a9a7d6..a7893a3 100644 Binary files a/docs/user-guides/screenshots/light/tasks/inbox-newly-created-tasks.png and b/docs/user-guides/screenshots/light/tasks/inbox-newly-created-tasks.png differ diff --git a/docs/user-guides/screenshots/light/tasks/inbox-view-with-filters.png b/docs/user-guides/screenshots/light/tasks/inbox-view-with-filters.png index c9a021e..2ea6b61 100644 Binary files a/docs/user-guides/screenshots/light/tasks/inbox-view-with-filters.png and b/docs/user-guides/screenshots/light/tasks/inbox-view-with-filters.png differ diff --git a/docs/user-guides/screenshots/light/tasks/inbox-view.png b/docs/user-guides/screenshots/light/tasks/inbox-view.png index e920acb..83426f0 100644 Binary files a/docs/user-guides/screenshots/light/tasks/inbox-view.png and b/docs/user-guides/screenshots/light/tasks/inbox-view.png differ diff --git a/docs/user-guides/screenshots/light/tasks/new-task-form-empty.png b/docs/user-guides/screenshots/light/tasks/new-task-form-empty.png index 555f6a9..3d47027 100644 Binary files a/docs/user-guides/screenshots/light/tasks/new-task-form-empty.png and b/docs/user-guides/screenshots/light/tasks/new-task-form-empty.png differ diff --git a/docs/user-guides/screenshots/light/tasks/new-task-form-filled.png b/docs/user-guides/screenshots/light/tasks/new-task-form-filled.png index e541af5..a8c5344 100644 Binary files a/docs/user-guides/screenshots/light/tasks/new-task-form-filled.png and b/docs/user-guides/screenshots/light/tasks/new-task-form-filled.png differ diff --git a/docs/user-guides/screenshots/light/tasks/task-detail-in-progress.png b/docs/user-guides/screenshots/light/tasks/task-detail-in-progress.png index d372c4c..d2333d6 100644 Binary files a/docs/user-guides/screenshots/light/tasks/task-detail-in-progress.png and b/docs/user-guides/screenshots/light/tasks/task-detail-in-progress.png differ diff --git a/docs/user-guides/screenshots/light/tasks/task-detail-with-comments.png b/docs/user-guides/screenshots/light/tasks/task-detail-with-comments.png index 1d81560..80243e7 100644 Binary files a/docs/user-guides/screenshots/light/tasks/task-detail-with-comments.png and b/docs/user-guides/screenshots/light/tasks/task-detail-with-comments.png differ diff --git a/docs/user-guides/screenshots/light/tasks/task-done-status.png b/docs/user-guides/screenshots/light/tasks/task-done-status.png index 412fa19..04f1225 100644 Binary files a/docs/user-guides/screenshots/light/tasks/task-done-status.png and b/docs/user-guides/screenshots/light/tasks/task-done-status.png differ diff --git a/docs/user-guides/screenshots/light/watchdogs/watchdog-thread-outcome.png b/docs/user-guides/screenshots/light/watchdogs/watchdog-thread-outcome.png index bc99645..970ef87 100644 Binary files a/docs/user-guides/screenshots/light/watchdogs/watchdog-thread-outcome.png and b/docs/user-guides/screenshots/light/watchdogs/watchdog-thread-outcome.png differ diff --git a/docs/user-guides/screenshots/light/work-modes/ask-mode-answer.png b/docs/user-guides/screenshots/light/work-modes/ask-mode-answer.png index 1aa3747..2f9e959 100644 Binary files a/docs/user-guides/screenshots/light/work-modes/ask-mode-answer.png and b/docs/user-guides/screenshots/light/work-modes/ask-mode-answer.png differ diff --git a/docs/user-guides/screenshots/light/work-modes/plan-mode-plan.png b/docs/user-guides/screenshots/light/work-modes/plan-mode-plan.png index 43f4388..94a0ef1 100644 Binary files a/docs/user-guides/screenshots/light/work-modes/plan-mode-plan.png and b/docs/user-guides/screenshots/light/work-modes/plan-mode-plan.png differ diff --git a/docs/user-guides/screenshots/light/work-modes/standard-mode-run.png b/docs/user-guides/screenshots/light/work-modes/standard-mode-run.png index cb05420..0dbaec8 100644 Binary files a/docs/user-guides/screenshots/light/work-modes/standard-mode-run.png and b/docs/user-guides/screenshots/light/work-modes/standard-mode-run.png differ diff --git a/docs/user-guides/screenshots/light/work-modes/work-mode-picker.png b/docs/user-guides/screenshots/light/work-modes/work-mode-picker.png index bec0af8..7ab2e14 100644 Binary files a/docs/user-guides/screenshots/light/work-modes/work-mode-picker.png and b/docs/user-guides/screenshots/light/work-modes/work-mode-picker.png differ diff --git a/docs/user-guides/screenshots/light/work-timeline/work-timeline-handoff.png b/docs/user-guides/screenshots/light/work-timeline/work-timeline-handoff.png index b621bed..1970aeb 100644 Binary files a/docs/user-guides/screenshots/light/work-timeline/work-timeline-handoff.png and b/docs/user-guides/screenshots/light/work-timeline/work-timeline-handoff.png differ diff --git a/docs/user-guides/screenshots/light/work-timeline/work-timeline-overview.png b/docs/user-guides/screenshots/light/work-timeline/work-timeline-overview.png index c4a8449..7e64a51 100644 Binary files a/docs/user-guides/screenshots/light/work-timeline/work-timeline-overview.png and b/docs/user-guides/screenshots/light/work-timeline/work-timeline-overview.png differ diff --git a/docs/user-guides/screenshots/light/workspaces/configuration.png b/docs/user-guides/screenshots/light/workspaces/configuration.png index 54ccf3e..64fac33 100644 Binary files a/docs/user-guides/screenshots/light/workspaces/configuration.png and b/docs/user-guides/screenshots/light/workspaces/configuration.png differ diff --git a/docs/user-guides/screenshots/light/workspaces/issues.png b/docs/user-guides/screenshots/light/workspaces/issues.png index 026ddf1..6971e36 100644 Binary files a/docs/user-guides/screenshots/light/workspaces/issues.png and b/docs/user-guides/screenshots/light/workspaces/issues.png differ diff --git a/docs/user-guides/screenshots/light/workspaces/list.png b/docs/user-guides/screenshots/light/workspaces/list.png index 6e11a3a..19dd51e 100644 Binary files a/docs/user-guides/screenshots/light/workspaces/list.png and b/docs/user-guides/screenshots/light/workspaces/list.png differ diff --git a/docs/user-guides/screenshots/light/workspaces/runtime-logs.png b/docs/user-guides/screenshots/light/workspaces/runtime-logs.png index 4293b4e..3dbca61 100644 Binary files a/docs/user-guides/screenshots/light/workspaces/runtime-logs.png and b/docs/user-guides/screenshots/light/workspaces/runtime-logs.png differ diff --git a/docs/user-guides/screenshots/light/workspaces/services.png b/docs/user-guides/screenshots/light/workspaces/services.png index d97c375..ffedcf0 100644 Binary files a/docs/user-guides/screenshots/light/workspaces/services.png and b/docs/user-guides/screenshots/light/workspaces/services.png differ diff --git a/docs/user-guides/screenshots/registry.json b/docs/user-guides/screenshots/registry.json index 3ebef45..e962314 100644 --- a/docs/user-guides/screenshots/registry.json +++ b/docs/user-guides/screenshots/registry.json @@ -10,10 +10,11 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/activity", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ - "ui/src/pages/Activity.tsx" + "ui/src/pages/audit/CompanyActivity.tsx", + "ui/src/pages/audit/AuditFeed.tsx" ] }, { @@ -21,10 +22,11 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/activity", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ - "ui/src/pages/Activity.tsx" + "ui/src/pages/audit/CompanyActivity.tsx", + "ui/src/pages/audit/AuditFeed.tsx" ] }, { @@ -32,19 +34,20 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/activity", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ - "ui/src/pages/Activity.tsx" + "ui/src/pages/audit/CompanyActivity.tsx", + "ui/src/pages/audit/AuditFeed.tsx" ] }, { "file": "dark/adapters/detail.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/adapters", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/adapters", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AdapterManager.tsx" ] @@ -53,9 +56,9 @@ "file": "dark/adapters/health.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/adapters", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/adapters", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AdapterManager.tsx" ] @@ -64,9 +67,9 @@ "file": "dark/adapters/install.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/adapters", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/adapters", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AdapterManager.tsx" ] @@ -75,9 +78,9 @@ "file": "dark/adapters/list.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/adapters", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/adapters", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AdapterManager.tsx" ] @@ -87,8 +90,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -98,8 +101,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{agentId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -109,8 +112,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/runs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -120,8 +123,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{longRunnerAgentId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -131,8 +134,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/{emptyPrefix}/agents/all", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Agents.tsx" ] @@ -142,8 +145,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -153,8 +156,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/budget", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -164,8 +167,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -175,8 +178,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -186,8 +189,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -197,8 +200,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/dashboard", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -208,8 +211,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -219,8 +222,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -230,8 +233,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{httpAgentId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -241,8 +244,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/instructions", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -252,8 +255,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/all", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Agents.tsx" ] @@ -263,8 +266,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/NewAgent.tsx" ] @@ -274,8 +277,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/NewAgent.tsx", "ui/src/components/ReportsToPicker.tsx" @@ -286,8 +289,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Agents.tsx" ] @@ -297,8 +300,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{longRunnerAgentId}/runs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -308,8 +311,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{runnerAgentId}/runs/{runnerRunId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/transcript/RunTranscriptView.tsx", "ui/src/pages/AgentDetail.tsx" @@ -320,8 +323,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/runs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -331,8 +334,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/skills", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -342,8 +345,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -353,8 +356,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{runnerAgentId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -364,8 +367,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/approvals/pending", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Approvals.tsx", "ui/src/components/ApprovalCard.tsx" @@ -376,8 +379,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/approvals/pending", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Approvals.tsx", "ui/src/components/ApprovalPayload.tsx" @@ -388,8 +391,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/approvals/{hireApprovalId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ApprovalDetail.tsx" ] @@ -399,8 +402,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/approvals/{hireApprovalId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ApprovalDetail.tsx" ] @@ -410,8 +413,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/approvals/{approvedApprovalId}?resolved=approved", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ApprovalDetail.tsx" ] @@ -421,8 +424,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/approvals/{hireApprovalId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/ApprovalPayload.tsx", "ui/src/pages/ApprovalDetail.tsx" @@ -433,8 +436,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/approvals/pending", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Approvals.tsx" ] @@ -444,8 +447,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/approvals/{strategyApprovalId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ApprovalDetail.tsx" ] @@ -455,8 +458,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/approvals/{strategyApprovalId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/ApprovalPayload.tsx", "ui/src/pages/ApprovalDetail.tsx" @@ -467,8 +470,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/company/settings/access", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanyAccess.tsx" ] @@ -478,8 +481,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/configuration", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -489,8 +492,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/onboarding", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/OnboardingWizard.tsx" ] @@ -500,8 +503,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/company/settings", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySettings.tsx" ] @@ -511,8 +514,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/company/export", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanyExport.tsx" ] @@ -522,8 +525,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/company/import", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanyImport.tsx" ] @@ -533,8 +536,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/company/settings/invites", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanyInvites.tsx" ] @@ -544,8 +547,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/inbox/requests", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -555,8 +558,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/onboarding", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/OnboardingWizard.tsx" ] @@ -566,8 +569,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/company/settings", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySettings.tsx" ] @@ -577,8 +580,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{budgetMaxAgentId}/budget", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/BudgetPolicyCard.tsx" ] @@ -588,8 +591,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{budgetHalfAgentId}/budget", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/BudgetPolicyCard.tsx" ] @@ -599,8 +602,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{budgetWarnAgentId}/budget", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/BudgetPolicyCard.tsx" ] @@ -610,8 +613,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{budgetHalfAgentId}/budget", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/BudgetPolicyCard.tsx" ] @@ -621,8 +624,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/budget", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx", "ui/src/components/BudgetPolicyCard.tsx" @@ -633,8 +636,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx" ] @@ -644,8 +647,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx" ] @@ -655,8 +658,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx", "ui/src/components/BudgetPolicyCard.tsx" @@ -667,8 +670,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx" ] @@ -678,8 +681,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx" ] @@ -689,8 +692,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx" ] @@ -700,8 +703,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx" ] @@ -711,8 +714,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx" ] @@ -722,8 +725,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/dashboard", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Dashboard.tsx" ] @@ -733,8 +736,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/dashboard", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Dashboard.tsx" ] @@ -744,8 +747,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/dashboard", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Dashboard.tsx" ] @@ -765,19 +768,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/dashboard", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", - "depends_on": [ - "ui/src/pages/Dashboard.tsx" - ] - }, - { - "file": "dark/dashboard/stale-tasks-panel.png", - "theme": "dark", - "viewport": "1440x900", - "route": "/ACME/dashboard", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Dashboard.tsx" ] @@ -787,30 +779,115 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/dashboard", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Dashboard.tsx" ] }, + { + "file": "dark/decisions/decision-card.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/decisions", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/components/DecisionCard.tsx", + "ui/src/components/DecisionResolver.tsx", + "ui/src/components/DecisionTriageStrip.tsx" + ] + }, + { + "file": "dark/decisions/empty-state.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/{emptyPrefix}/decisions", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/WhatNeedsMe.tsx" + ] + }, + { + "file": "dark/decisions/history-curtains.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/decisions", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/components/DecisionShelf.tsx", + "ui/src/pages/WhatNeedsMe.tsx" + ] + }, + { + "file": "dark/decisions/named-queue.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/decisions/queues/{decisionQueueKey}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/DecisionQueuePage.tsx", + "ui/src/components/DecisionQueueRail.tsx" + ] + }, + { + "file": "dark/decisions/queue-overview.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/decisions", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/WhatNeedsMe.tsx", + "ui/src/components/AttentionQueueRow.tsx" + ] + }, + { + "file": "dark/decisions/toolbar-filters.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/decisions", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/components/DecisionsToolbar.tsx", + "ui/src/components/DecisionDateChips.tsx" + ] + }, + { + "file": "dark/decisions/triage-chips.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/decisions", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/components/DecisionTriageStrip.tsx", + "ui/src/components/DecisionDateChips.tsx" + ] + }, { "file": "dark/experimental/cloud-upstream.png", "theme": "dark", "viewport": "1440x900", - "route": "/ACME/company/settings/cloud-upstream", - "captured_against": "master", - "captured_sha": "59eee4829cae581d9dd462712c34d91942f07a8c", + "route": "/ACME/company/export", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ - "ui/src/pages/CloudUpstream.tsx" + "ui/src/pages/CompanyExport.tsx", + "ui/src/api/cloud.ts" ] }, { "file": "dark/experimental/environments.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/environments", - "captured_against": "master", - "captured_sha": "59eee4829cae581d9dd462712c34d91942f07a8c", + "route": "/ACME/company/settings/instance/environments", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanyEnvironments.tsx" ] @@ -820,8 +897,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/issues/{externalObjectIssueId}", - "captured_against": "master", - "captured_sha": "59eee4829cae581d9dd462712c34d91942f07a8c", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/ExternalObjectPill.tsx", "ui/src/components/IssueRelatedWorkPanel.tsx" @@ -832,8 +909,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/issues/{issueId}?browse=1", - "captured_against": "master", - "captured_sha": "59eee4829cae581d9dd462712c34d91942f07a8c", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/FileViewerSheet.tsx", "ui/src/components/WorkspaceFileBrowser.tsx" @@ -843,9 +920,9 @@ "file": "dark/experimental/recovery-preview.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/experimental", - "captured_against": "master", - "captured_sha": "59eee4829cae581d9dd462712c34d91942f07a8c", + "route": "/ACME/company/settings/instance/experimental", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/InstanceExperimentalSettings.tsx" ] @@ -855,8 +932,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/dashboard", - "captured_against": "master", - "captured_sha": "59eee4829cae581d9dd462712c34d91942f07a8c", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/SidebarServerInfo.tsx", "ui/src/components/SidebarAccountMenu.tsx" @@ -867,8 +944,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/company/export", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanyExport.tsx" ] @@ -878,8 +955,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/company/import", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanyImport.tsx" ] @@ -889,8 +966,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/goals/{goalId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/GoalDetail.tsx" ] @@ -900,8 +977,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/goals", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Goals.tsx" ] @@ -911,8 +988,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx", "ui/src/components/IssueThreadInteractionCard.tsx" @@ -923,8 +1000,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx" ] @@ -934,8 +1011,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx" ] @@ -945,8 +1022,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx" ] @@ -956,8 +1033,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/inbox/requests", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -967,8 +1044,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/inbox/unread", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -978,8 +1055,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/inbox/mine", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -989,8 +1066,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/issues", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx" ] @@ -1000,8 +1077,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/inbox/mine", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -1011,8 +1088,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -1022,8 +1099,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/{emptyPrefix}/dashboard", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Dashboard.tsx" ] @@ -1033,8 +1110,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/onboarding", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/OnboardingWizard.tsx" ] @@ -1044,8 +1121,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/onboarding", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/OnboardingWizard.tsx" ] @@ -1055,8 +1132,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/dashboard", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/SidebarCompanyMenu.tsx" ] @@ -1066,8 +1143,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/org", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/OrgChart.tsx" ] @@ -1077,8 +1154,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/org", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/OrgChart.tsx" ] @@ -1088,8 +1165,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/org", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/OrgChart.tsx" ] @@ -1099,8 +1176,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/org", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/OrgChart.tsx" ] @@ -1110,8 +1187,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/org", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/OrgChart.tsx" ] @@ -1121,8 +1198,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -1132,8 +1209,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/skills", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] @@ -1143,8 +1220,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/workspaces", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Workspaces.tsx" ] @@ -1153,9 +1230,9 @@ "file": "dark/plugins/detail.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/plugins", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/plugins", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/PluginPage.tsx" ] @@ -1164,9 +1241,9 @@ "file": "dark/plugins/install.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/plugins", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/plugins", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/PluginManager.tsx" ] @@ -1175,9 +1252,9 @@ "file": "dark/plugins/jobs-log.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/plugins", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/plugins", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/PluginSettings.tsx" ] @@ -1186,9 +1263,9 @@ "file": "dark/plugins/list.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/plugins", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/plugins", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/PluginManager.tsx" ] @@ -1197,9 +1274,9 @@ "file": "dark/plugins/settings.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/plugins", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/plugins", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/PluginSettings.tsx" ] @@ -1209,8 +1286,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/projects/{projectId}/budget", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ProjectDetail.tsx" ] @@ -1220,8 +1297,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/projects/{projectId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ProjectDetail.tsx" ] @@ -1231,8 +1308,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/projects/{projectId}/issues", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ProjectDetail.tsx" ] @@ -1242,8 +1319,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/projects", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Projects.tsx" ] @@ -1253,8 +1330,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/projects/{projectId}/overview", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ProjectDetail.tsx" ] @@ -1264,8 +1341,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/projects/{projectId}/workspaces", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ProjectDetail.tsx" ] @@ -1274,12 +1351,11 @@ "file": "dark/routines/cron-picker.png", "theme": "dark", "viewport": "1440x900", - "route": "/ACME/routines/{routineId}", - "captured_against": "master", - "captured_sha": "4c6c0c6ad048838dda4a67e1aca43aa37a6fcf0d", + "route": "/ACME/routines/{routineId}/triggers", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ - "ui/src/pages/RoutineDetail.tsx", - "ui/src/components/ScheduleEditor.tsx" + "ui/src/pages/RoutineDetail.tsx" ] }, { @@ -1287,8 +1363,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/routines/{routineId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/RoutineDetail.tsx" ] @@ -1298,8 +1374,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/routines", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Routines.tsx" ] @@ -1309,8 +1385,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/routines/{routineId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/RoutineDetail.tsx" ] @@ -1320,8 +1396,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/company/settings/secrets", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/secrets/MissingUserSecretsBanner.tsx", "server/src/services/secrets.ts" @@ -1332,21 +1408,32 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/company/settings/secrets", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Secrets.tsx", "ui/src/pages/secrets/SetMyUserSecretDialog.tsx", "ui/src/pages/secrets/MyUserSecretsTab.tsx" ] }, + { + "file": "dark/secrets/proposals-tab.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/company/settings/secrets", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/secrets/ProposalsTab.tsx" + ] + }, { "file": "dark/secrets/user-secret-definition.png", "theme": "dark", "viewport": "1440x900", "route": "/ACME/company/settings/secrets", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Secrets.tsx", "ui/src/pages/secrets/UserSecretDefinitionsTab.tsx" @@ -1356,9 +1443,9 @@ "file": "dark/settings/experimental.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/experimental", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/experimental", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/InstanceExperimentalSettings.tsx" ] @@ -1367,9 +1454,9 @@ "file": "dark/settings/instance-access.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/access", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/access", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/InstanceAccess.tsx" ] @@ -1378,9 +1465,9 @@ "file": "dark/settings/instance-adapters.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/adapters", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/adapters", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AdapterManager.tsx" ] @@ -1389,9 +1476,9 @@ "file": "dark/settings/instance-general.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/general", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/general", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/InstanceGeneralSettings.tsx" ] @@ -1400,9 +1487,9 @@ "file": "dark/settings/profile.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/profile", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/profile", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ProfileSettings.tsx" ] @@ -1411,9 +1498,9 @@ "file": "dark/settings/scheduler-heartbeats.png", "theme": "dark", "viewport": "1440x900", - "route": "/instance/settings/heartbeats", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/heartbeats", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/InstanceSettings.tsx" ] @@ -1423,8 +1510,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/skills", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] @@ -1434,8 +1521,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/skills", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] @@ -1445,8 +1532,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/skills/{skillId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] @@ -1456,8 +1543,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/skills", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] @@ -1467,8 +1554,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/skills", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] @@ -1478,8 +1565,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/skills/{skillId}", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] @@ -1489,19 +1576,122 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/skills", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] }, + { + "file": "dark/status-cards/board.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/status", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/StatusCards/index.tsx", + "ui/src/pages/StatusCards/StatusCardTile.tsx" + ] + }, + { + "file": "dark/status-cards/detail-history.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/status/{statusCardId}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/StatusCards/StatusCardDetailDrawer.tsx" + ] + }, + { + "file": "dark/status-cards/detail-settings.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/status/{statusCardId}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/StatusCards/StatusCardSettingsForm.tsx" + ] + }, + { + "file": "dark/status-cards/detail-summary.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/status/{statusCardId}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/StatusCards/StatusCardDetailDrawer.tsx" + ] + }, + { + "file": "dark/status-cards/detail-watched-issues.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/status/{statusCardId}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/StatusCards/StatusCardDetailDrawer.tsx" + ] + }, + { + "file": "dark/status-cards/new-card-dialog.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/status", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/StatusCards/CreateStatusCardDialog.tsx" + ] + }, + { + "file": "dark/task-chat/composer-modes.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/issues/{issueId}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/components/task-chat/TaskChatComposer.tsx" + ] + }, + { + "file": "dark/task-chat/side-pane.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/issues/{issueId}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/components/issue-properties/IssuePropertiesPlansTab.tsx", + "ui/src/components/issue-properties/IssuePropertiesArtifactsTab.tsx" + ] + }, + { + "file": "dark/task-chat/thread.png", + "theme": "dark", + "viewport": "1440x900", + "route": "/ACME/issues/{issueId}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/components/TaskChatThread.tsx", + "ui/src/components/task-chat/TaskChatBubble.tsx", + "ui/src/components/task-chat/TaskChatTurn.tsx" + ] + }, { "file": "dark/tasks/comment-input-box.png", "theme": "dark", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx", "ui/src/components/CommentThread.tsx" @@ -1512,8 +1702,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/inbox/recent", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -1523,8 +1713,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/inbox/all", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -1534,8 +1724,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/inbox/mine", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -1545,8 +1735,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/inbox/mine", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -1556,8 +1746,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/inbox/mine", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -1567,8 +1757,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx" ] @@ -1578,8 +1768,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx" ] @@ -1589,8 +1779,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/issues/done", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -1600,8 +1790,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx", "server/src/services/task-watchdogs.ts" @@ -1612,8 +1802,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx", "ui/src/components/IssueChatThread.tsx" @@ -1624,8 +1814,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx", "ui/src/components/IssueDocumentsSection.tsx" @@ -1636,8 +1826,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx", "ui/src/components/IssueChatThread.tsx" @@ -1648,8 +1838,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/inbox/mine", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/NewIssueDialog.tsx", "ui/src/lib/work-mode-meta.ts" @@ -1660,8 +1850,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/timeline", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Timeline.tsx", "server/src/services/work-timeline.ts" @@ -1672,8 +1862,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/timeline", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Timeline.tsx", "ui/src/api/workTimeline.ts", @@ -1685,8 +1875,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/execution-workspaces/{executionWorkspaceId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ExecutionWorkspaceDetail.tsx" ] @@ -1696,8 +1886,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/execution-workspaces/{executionWorkspaceId}/issues", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ExecutionWorkspaceDetail.tsx" ] @@ -1707,8 +1897,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/workspaces", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Workspaces.tsx" ] @@ -1718,8 +1908,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/execution-workspaces/{executionWorkspaceId}/runtime-logs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ExecutionWorkspaceDetail.tsx" ] @@ -1729,8 +1919,8 @@ "theme": "dark", "viewport": "1440x900", "route": "/ACME/execution-workspaces/{executionWorkspaceId}/services", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ExecutionWorkspaceDetail.tsx" ] @@ -1740,10 +1930,11 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/activity", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ - "ui/src/pages/Activity.tsx" + "ui/src/pages/audit/CompanyActivity.tsx", + "ui/src/pages/audit/AuditFeed.tsx" ] }, { @@ -1751,10 +1942,11 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/activity", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ - "ui/src/pages/Activity.tsx" + "ui/src/pages/audit/CompanyActivity.tsx", + "ui/src/pages/audit/AuditFeed.tsx" ] }, { @@ -1762,19 +1954,20 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/activity", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ - "ui/src/pages/Activity.tsx" + "ui/src/pages/audit/CompanyActivity.tsx", + "ui/src/pages/audit/AuditFeed.tsx" ] }, { "file": "light/adapters/detail.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/adapters", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/adapters", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AdapterManager.tsx" ] @@ -1783,9 +1976,9 @@ "file": "light/adapters/health.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/adapters", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/adapters", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AdapterManager.tsx" ] @@ -1794,9 +1987,9 @@ "file": "light/adapters/install.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/adapters", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/adapters", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AdapterManager.tsx" ] @@ -1805,9 +1998,9 @@ "file": "light/adapters/list.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/adapters", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/adapters", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AdapterManager.tsx" ] @@ -1817,8 +2010,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -1828,8 +2021,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{agentId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -1839,8 +2032,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/runs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -1850,8 +2043,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{longRunnerAgentId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -1861,8 +2054,8 @@ "theme": "light", "viewport": "1440x900", "route": "/{emptyPrefix}/agents/all", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Agents.tsx" ] @@ -1872,8 +2065,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -1883,8 +2076,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/budget", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -1894,8 +2087,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -1905,8 +2098,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -1916,8 +2109,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -1927,8 +2120,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/dashboard", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -1938,8 +2131,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -1949,8 +2142,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -1960,8 +2153,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{httpAgentId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -1971,8 +2164,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/instructions", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -1982,8 +2175,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/all", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Agents.tsx" ] @@ -1993,8 +2186,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/NewAgent.tsx" ] @@ -2004,8 +2197,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/NewAgent.tsx", "ui/src/components/ReportsToPicker.tsx" @@ -2016,8 +2209,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Agents.tsx" ] @@ -2027,8 +2220,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{longRunnerAgentId}/runs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -2038,8 +2231,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{runnerAgentId}/runs/{runnerRunId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/transcript/RunTranscriptView.tsx", "ui/src/pages/AgentDetail.tsx" @@ -2050,8 +2243,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/runs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -2061,8 +2254,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/skills", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx" ] @@ -2072,8 +2265,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/new", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -2083,8 +2276,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{runnerAgentId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -2094,8 +2287,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/approvals/pending", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Approvals.tsx", "ui/src/components/ApprovalCard.tsx" @@ -2106,8 +2299,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/approvals/pending", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Approvals.tsx", "ui/src/components/ApprovalPayload.tsx" @@ -2118,8 +2311,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/approvals/{hireApprovalId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ApprovalDetail.tsx" ] @@ -2129,8 +2322,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/approvals/{hireApprovalId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ApprovalDetail.tsx" ] @@ -2140,8 +2333,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/approvals/{approvedApprovalId}?resolved=approved", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ApprovalDetail.tsx" ] @@ -2151,8 +2344,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/approvals/{hireApprovalId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/ApprovalPayload.tsx", "ui/src/pages/ApprovalDetail.tsx" @@ -2163,8 +2356,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/approvals/pending", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Approvals.tsx" ] @@ -2174,8 +2367,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/approvals/{strategyApprovalId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ApprovalDetail.tsx" ] @@ -2185,8 +2378,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/approvals/{strategyApprovalId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/ApprovalPayload.tsx", "ui/src/pages/ApprovalDetail.tsx" @@ -2197,8 +2390,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/company/settings/access", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanyAccess.tsx" ] @@ -2208,8 +2401,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/configuration", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -2219,8 +2412,8 @@ "theme": "light", "viewport": "1440x900", "route": "/onboarding", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/OnboardingWizard.tsx" ] @@ -2230,8 +2423,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/company/settings", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySettings.tsx" ] @@ -2241,8 +2434,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/company/export", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanyExport.tsx" ] @@ -2252,8 +2445,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/company/import", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanyImport.tsx" ] @@ -2263,8 +2456,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/company/settings/invites", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanyInvites.tsx" ] @@ -2274,8 +2467,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/inbox/requests", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -2285,8 +2478,8 @@ "theme": "light", "viewport": "1440x900", "route": "/onboarding", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/OnboardingWizard.tsx" ] @@ -2296,8 +2489,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/company/settings", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySettings.tsx" ] @@ -2307,8 +2500,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{budgetMaxAgentId}/budget", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/BudgetPolicyCard.tsx" ] @@ -2318,8 +2511,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{budgetHalfAgentId}/budget", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/BudgetPolicyCard.tsx" ] @@ -2329,8 +2522,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{budgetWarnAgentId}/budget", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/BudgetPolicyCard.tsx" ] @@ -2340,8 +2533,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{budgetHalfAgentId}/budget", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/BudgetPolicyCard.tsx" ] @@ -2351,8 +2544,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/budget", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AgentDetail.tsx", "ui/src/components/BudgetPolicyCard.tsx" @@ -2363,8 +2556,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx" ] @@ -2374,8 +2567,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx" ] @@ -2385,8 +2578,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx", "ui/src/components/BudgetPolicyCard.tsx" @@ -2397,8 +2590,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx" ] @@ -2408,8 +2601,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx" ] @@ -2419,8 +2612,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx" ] @@ -2430,8 +2623,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx" ] @@ -2441,8 +2634,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/costs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Costs.tsx" ] @@ -2452,8 +2645,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/dashboard", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Dashboard.tsx" ] @@ -2463,8 +2656,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/dashboard", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Dashboard.tsx" ] @@ -2474,8 +2667,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/dashboard", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Dashboard.tsx" ] @@ -2495,19 +2688,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/dashboard", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", - "depends_on": [ - "ui/src/pages/Dashboard.tsx" - ] - }, - { - "file": "light/dashboard/stale-tasks-panel.png", - "theme": "light", - "viewport": "1440x900", - "route": "/ACME/dashboard", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Dashboard.tsx" ] @@ -2517,30 +2699,115 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/dashboard", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Dashboard.tsx" ] }, + { + "file": "light/decisions/decision-card.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/decisions", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/components/DecisionCard.tsx", + "ui/src/components/DecisionResolver.tsx", + "ui/src/components/DecisionTriageStrip.tsx" + ] + }, + { + "file": "light/decisions/empty-state.png", + "theme": "light", + "viewport": "1440x900", + "route": "/{emptyPrefix}/decisions", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/WhatNeedsMe.tsx" + ] + }, + { + "file": "light/decisions/history-curtains.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/decisions", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/components/DecisionShelf.tsx", + "ui/src/pages/WhatNeedsMe.tsx" + ] + }, + { + "file": "light/decisions/named-queue.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/decisions/queues/{decisionQueueKey}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/DecisionQueuePage.tsx", + "ui/src/components/DecisionQueueRail.tsx" + ] + }, + { + "file": "light/decisions/queue-overview.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/decisions", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/WhatNeedsMe.tsx", + "ui/src/components/AttentionQueueRow.tsx" + ] + }, + { + "file": "light/decisions/toolbar-filters.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/decisions", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/components/DecisionsToolbar.tsx", + "ui/src/components/DecisionDateChips.tsx" + ] + }, + { + "file": "light/decisions/triage-chips.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/decisions", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/components/DecisionTriageStrip.tsx", + "ui/src/components/DecisionDateChips.tsx" + ] + }, { "file": "light/experimental/cloud-upstream.png", "theme": "light", "viewport": "1440x900", - "route": "/ACME/company/settings/cloud-upstream", - "captured_against": "master", - "captured_sha": "59eee4829cae581d9dd462712c34d91942f07a8c", + "route": "/ACME/company/export", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ - "ui/src/pages/CloudUpstream.tsx" + "ui/src/pages/CompanyExport.tsx", + "ui/src/api/cloud.ts" ] }, { "file": "light/experimental/environments.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/environments", - "captured_against": "master", - "captured_sha": "59eee4829cae581d9dd462712c34d91942f07a8c", + "route": "/ACME/company/settings/instance/environments", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanyEnvironments.tsx" ] @@ -2550,8 +2817,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/issues/{externalObjectIssueId}", - "captured_against": "master", - "captured_sha": "59eee4829cae581d9dd462712c34d91942f07a8c", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/ExternalObjectPill.tsx", "ui/src/components/IssueRelatedWorkPanel.tsx" @@ -2562,8 +2829,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/issues/{issueId}?browse=1", - "captured_against": "master", - "captured_sha": "59eee4829cae581d9dd462712c34d91942f07a8c", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/FileViewerSheet.tsx", "ui/src/components/WorkspaceFileBrowser.tsx" @@ -2573,9 +2840,9 @@ "file": "light/experimental/recovery-preview.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/experimental", - "captured_against": "master", - "captured_sha": "59eee4829cae581d9dd462712c34d91942f07a8c", + "route": "/ACME/company/settings/instance/experimental", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/InstanceExperimentalSettings.tsx" ] @@ -2585,8 +2852,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/dashboard", - "captured_against": "master", - "captured_sha": "59eee4829cae581d9dd462712c34d91942f07a8c", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/SidebarServerInfo.tsx", "ui/src/components/SidebarAccountMenu.tsx" @@ -2597,8 +2864,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/company/export", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanyExport.tsx" ] @@ -2608,8 +2875,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/company/import", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanyImport.tsx" ] @@ -2619,8 +2886,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/goals/{goalId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/GoalDetail.tsx" ] @@ -2630,8 +2897,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/goals", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Goals.tsx" ] @@ -2641,8 +2908,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx", "ui/src/components/IssueThreadInteractionCard.tsx" @@ -2653,8 +2920,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx" ] @@ -2664,8 +2931,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx" ] @@ -2675,8 +2942,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx" ] @@ -2686,8 +2953,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/inbox/requests", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -2697,8 +2964,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/inbox/unread", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -2708,8 +2975,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/inbox/mine", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -2719,8 +2986,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/issues", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx" ] @@ -2730,8 +2997,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/inbox/mine", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -2741,8 +3008,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -2752,8 +3019,8 @@ "theme": "light", "viewport": "1440x900", "route": "/{emptyPrefix}/dashboard", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Dashboard.tsx" ] @@ -2763,8 +3030,8 @@ "theme": "light", "viewport": "1440x900", "route": "/onboarding", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/OnboardingWizard.tsx" ] @@ -2774,8 +3041,8 @@ "theme": "light", "viewport": "1440x900", "route": "/onboarding", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/OnboardingWizard.tsx" ] @@ -2785,8 +3052,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/dashboard", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/SidebarCompanyMenu.tsx" ] @@ -2796,8 +3063,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/org", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/OrgChart.tsx" ] @@ -2807,8 +3074,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/org", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/OrgChart.tsx" ] @@ -2818,8 +3085,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/org", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/OrgChart.tsx" ] @@ -2829,8 +3096,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/org", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/OrgChart.tsx" ] @@ -2840,8 +3107,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/org", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/OrgChart.tsx" ] @@ -2851,8 +3118,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/agents/{agentId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/AgentConfigForm.tsx" ] @@ -2862,8 +3129,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/skills", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] @@ -2873,8 +3140,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/workspaces", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Workspaces.tsx" ] @@ -2883,9 +3150,9 @@ "file": "light/plugins/detail.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/plugins", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/plugins", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/PluginPage.tsx" ] @@ -2894,9 +3161,9 @@ "file": "light/plugins/install.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/plugins", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/plugins", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/PluginManager.tsx" ] @@ -2905,9 +3172,9 @@ "file": "light/plugins/jobs-log.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/plugins", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/plugins", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/PluginSettings.tsx" ] @@ -2916,9 +3183,9 @@ "file": "light/plugins/list.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/plugins", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/plugins", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/PluginManager.tsx" ] @@ -2927,9 +3194,9 @@ "file": "light/plugins/settings.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/plugins", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/plugins", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/PluginSettings.tsx" ] @@ -2939,8 +3206,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/projects/{projectId}/budget", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ProjectDetail.tsx" ] @@ -2950,8 +3217,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/projects/{projectId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ProjectDetail.tsx" ] @@ -2961,8 +3228,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/projects/{projectId}/issues", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ProjectDetail.tsx" ] @@ -2972,8 +3239,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/projects", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Projects.tsx" ] @@ -2983,8 +3250,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/projects/{projectId}/overview", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ProjectDetail.tsx" ] @@ -2994,8 +3261,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/projects/{projectId}/workspaces", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ProjectDetail.tsx" ] @@ -3004,12 +3271,11 @@ "file": "light/routines/cron-picker.png", "theme": "light", "viewport": "1440x900", - "route": "/ACME/routines/{routineId}", - "captured_against": "master", - "captured_sha": "4c6c0c6ad048838dda4a67e1aca43aa37a6fcf0d", + "route": "/ACME/routines/{routineId}/triggers", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ - "ui/src/pages/RoutineDetail.tsx", - "ui/src/components/ScheduleEditor.tsx" + "ui/src/pages/RoutineDetail.tsx" ] }, { @@ -3017,8 +3283,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/routines/{routineId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/RoutineDetail.tsx" ] @@ -3028,8 +3294,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/routines", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Routines.tsx" ] @@ -3039,8 +3305,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/routines/{routineId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/RoutineDetail.tsx" ] @@ -3050,8 +3316,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/company/settings/secrets", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/secrets/MissingUserSecretsBanner.tsx", "server/src/services/secrets.ts" @@ -3062,21 +3328,32 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/company/settings/secrets", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Secrets.tsx", "ui/src/pages/secrets/SetMyUserSecretDialog.tsx", "ui/src/pages/secrets/MyUserSecretsTab.tsx" ] }, + { + "file": "light/secrets/proposals-tab.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/company/settings/secrets", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/secrets/ProposalsTab.tsx" + ] + }, { "file": "light/secrets/user-secret-definition.png", "theme": "light", "viewport": "1440x900", "route": "/ACME/company/settings/secrets", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Secrets.tsx", "ui/src/pages/secrets/UserSecretDefinitionsTab.tsx" @@ -3086,9 +3363,9 @@ "file": "light/settings/experimental.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/experimental", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/experimental", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/InstanceExperimentalSettings.tsx" ] @@ -3097,9 +3374,9 @@ "file": "light/settings/instance-access.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/access", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/access", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/InstanceAccess.tsx" ] @@ -3108,9 +3385,9 @@ "file": "light/settings/instance-adapters.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/adapters", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/adapters", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/AdapterManager.tsx" ] @@ -3119,9 +3396,9 @@ "file": "light/settings/instance-general.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/general", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/general", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/InstanceGeneralSettings.tsx" ] @@ -3130,9 +3407,9 @@ "file": "light/settings/profile.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/profile", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/profile", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ProfileSettings.tsx" ] @@ -3141,9 +3418,9 @@ "file": "light/settings/scheduler-heartbeats.png", "theme": "light", "viewport": "1440x900", - "route": "/instance/settings/heartbeats", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "route": "/ACME/company/settings/instance/heartbeats", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/InstanceSettings.tsx" ] @@ -3153,8 +3430,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/skills", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] @@ -3164,8 +3441,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/skills", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] @@ -3175,8 +3452,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/skills/{skillId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] @@ -3186,8 +3463,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/skills", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] @@ -3197,8 +3474,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/skills", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] @@ -3208,8 +3485,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/skills/{skillId}", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] @@ -3219,19 +3496,122 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/skills", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/CompanySkills.tsx" ] }, + { + "file": "light/status-cards/board.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/status", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/StatusCards/index.tsx", + "ui/src/pages/StatusCards/StatusCardTile.tsx" + ] + }, + { + "file": "light/status-cards/detail-history.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/status/{statusCardId}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/StatusCards/StatusCardDetailDrawer.tsx" + ] + }, + { + "file": "light/status-cards/detail-settings.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/status/{statusCardId}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/StatusCards/StatusCardSettingsForm.tsx" + ] + }, + { + "file": "light/status-cards/detail-summary.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/status/{statusCardId}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/StatusCards/StatusCardDetailDrawer.tsx" + ] + }, + { + "file": "light/status-cards/detail-watched-issues.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/status/{statusCardId}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/StatusCards/StatusCardDetailDrawer.tsx" + ] + }, + { + "file": "light/status-cards/new-card-dialog.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/status", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/pages/StatusCards/CreateStatusCardDialog.tsx" + ] + }, + { + "file": "light/task-chat/composer-modes.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/issues/{issueId}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/components/task-chat/TaskChatComposer.tsx" + ] + }, + { + "file": "light/task-chat/side-pane.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/issues/{issueId}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/components/issue-properties/IssuePropertiesPlansTab.tsx", + "ui/src/components/issue-properties/IssuePropertiesArtifactsTab.tsx" + ] + }, + { + "file": "light/task-chat/thread.png", + "theme": "light", + "viewport": "1440x900", + "route": "/ACME/issues/{issueId}", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", + "depends_on": [ + "ui/src/components/TaskChatThread.tsx", + "ui/src/components/task-chat/TaskChatBubble.tsx", + "ui/src/components/task-chat/TaskChatTurn.tsx" + ] + }, { "file": "light/tasks/comment-input-box.png", "theme": "light", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx", "ui/src/components/CommentThread.tsx" @@ -3242,8 +3622,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/inbox/recent", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -3253,8 +3633,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/inbox/all", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -3264,8 +3644,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/inbox/mine", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -3275,8 +3655,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/inbox/mine", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -3286,8 +3666,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/inbox/mine", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -3297,8 +3677,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx" ] @@ -3308,8 +3688,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx" ] @@ -3319,8 +3699,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/issues/done", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Inbox.tsx" ] @@ -3330,8 +3710,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx", "server/src/services/task-watchdogs.ts" @@ -3342,8 +3722,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx", "ui/src/components/IssueChatThread.tsx" @@ -3354,8 +3734,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx", "ui/src/components/IssueDocumentsSection.tsx" @@ -3366,8 +3746,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/issues/{issueId}", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/IssueDetail.tsx", "ui/src/components/IssueChatThread.tsx" @@ -3378,8 +3758,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/inbox/mine", - "captured_against": null, - "captured_sha": null, + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/components/NewIssueDialog.tsx", "ui/src/lib/work-mode-meta.ts" @@ -3390,8 +3770,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/timeline", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Timeline.tsx", "server/src/services/work-timeline.ts" @@ -3402,8 +3782,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/timeline", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Timeline.tsx", "ui/src/api/workTimeline.ts", @@ -3415,8 +3795,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/execution-workspaces/{executionWorkspaceId}/configuration", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ExecutionWorkspaceDetail.tsx" ] @@ -3426,8 +3806,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/execution-workspaces/{executionWorkspaceId}/issues", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ExecutionWorkspaceDetail.tsx" ] @@ -3437,8 +3817,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/workspaces", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/Workspaces.tsx" ] @@ -3448,8 +3828,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/execution-workspaces/{executionWorkspaceId}/runtime-logs", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ExecutionWorkspaceDetail.tsx" ] @@ -3459,8 +3839,8 @@ "theme": "light", "viewport": "1440x900", "route": "/ACME/execution-workspaces/{executionWorkspaceId}/services", - "captured_against": "HEAD", - "captured_sha": "903bd157c22a39c1b90813b8d2a7e63cfd10fa65", + "captured_against": "upstream/candidate/release-2026.817.0", + "captured_sha": "213dabab4f8e1f3bb1803a2924c0fea1289fcd4c", "depends_on": [ "ui/src/pages/ExecutionWorkspaceDetail.tsx" ] diff --git a/package.json b/package.json index 30562db..c770494 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "screenshots:capture": "node scripts/screenshots/capture.mjs", "screenshots:sync-registry": "node scripts/screenshots/sync-registry.mjs", "screenshots:refresh": "node scripts/screenshots/run.mjs", + "screenshots:serve": "node scripts/screenshots/run.mjs --serve", "screenshots:refresh:all": "node scripts/screenshots/run.mjs --all" }, "devDependencies": { diff --git a/scripts/screenshots/capture.mjs b/scripts/screenshots/capture.mjs index 2b7dd50..87b591f 100644 --- a/scripts/screenshots/capture.mjs +++ b/scripts/screenshots/capture.mjs @@ -63,22 +63,59 @@ async function parentHeadSha() { } } -/** Get the current branch name of the parent repo. */ -async function parentRefName() { +/** Run a git command in the parent repo; returns trimmed stdout, or "" on failure. */ +async function parentGit(args) { try { - const { stdout } = await execFileAsync("git", [ - "-C", - PARENT_REPO, - "rev-parse", - "--abbrev-ref", - "HEAD", - ]); + const { stdout } = await execFileAsync("git", ["-C", PARENT_REPO, ...args]); return stdout.trim(); } catch { - return null; + return ""; } } +/** + * Human-meaningful name for the parent revision being captured. + * + * Release captures run against a *detached* checkout of the release commit, and + * `rev-parse --abbrev-ref HEAD` answers the literal string "HEAD" in that state — + * which is what every pre-2026.817 registry entry recorded, making it impossible + * to tell from the registry alone which release a shot belongs to. So: use the + * branch name when we are on one, and otherwise resolve the most descriptive ref + * that contains the commit (tag → describe → containing branch), falling back to + * the short SHA. `captured_sha` still pins the exact commit either way. + */ +async function parentRefName() { + const branch = await parentGit(["symbolic-ref", "--quiet", "--short", "HEAD"]); + if (branch) return branch; + + // Detached. A tag pointing exactly at HEAD is the best answer. + const tag = (await parentGit(["tag", "--points-at", "HEAD"])).split("\n")[0]; + if (tag) return tag; + + // Otherwise the nearest tag-ish description (e.g. "beta/v2026.811.0-beta.0-4-g213dabab4"). + const described = await parentGit(["describe", "--tags", "--always"]); + + // A release candidate usually lives only on a release branch — prefer that name + // when one contains the commit, since it identifies the release. + const containing = await parentGit([ + "branch", + "--all", + "--contains", + "HEAD", + "--format=%(refname:short)", + ]) + .then((out) => + out + .split("\n") + .map((line) => line.trim()) + .filter((line) => line && !line.includes("HEAD detached")), + ); + const release = containing.find((ref) => /release|candidate/.test(ref)) ?? containing[0]; + if (release) return release; + + return described || (await parentGit(["rev-parse", "--short", "HEAD"])) || null; +} + /** * Playwright addInitScript source that seeds localStorage and the dark class * before the page-level inline script runs. This exactly mirrors the @@ -127,7 +164,10 @@ function locate(page, sel) { * is logged and skipped so we still screenshot whatever state was reached * (a partial capture beats no capture, and surfaces selector drift in the log). * - * Step shapes: { click }, { fill, value }, { waitFor }, { waitMs }, { press }. + * Step shapes: { click }, { fill, value }, { waitFor }, { waitMs }, { press }, + * { scrollTo } (bring an element into view — long pages like issue detail put + * the interesting section below the fold, and a 1440×900 shot of the top of the + * page misses it entirely). * * @param {import('@playwright/test').Page} page * @param {Array} steps @@ -140,6 +180,8 @@ async function runSteps(page, steps, label) { await page.waitForTimeout(stepObj.waitMs); } else if (stepObj.waitFor) { await locate(page, stepObj.waitFor).waitFor({ state: "visible", timeout: 8_000 }); + } else if (stepObj.scrollTo) { + await locate(page, stepObj.scrollTo).scrollIntoViewIfNeeded({ timeout: 8_000 }); } else if (stepObj.click) { await locate(page, stepObj.click).click({ timeout: 8_000 }); } else if (stepObj.fill) { diff --git a/scripts/screenshots/config.mjs b/scripts/screenshots/config.mjs index 155d063..9bf98cd 100644 --- a/scripts/screenshots/config.mjs +++ b/scripts/screenshots/config.mjs @@ -6,6 +6,9 @@ import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; +import { createRequire } from "node:module"; +import { readFileSync } from "node:fs"; +import { createHmac } from "node:crypto"; import os from "node:os"; import net from "node:net"; @@ -186,3 +189,118 @@ export async function findFreeEmbeddedPostgresPort( `No free port found in [${start}, ${start + attempts}) for the screenshot instance's embedded Postgres`, ); } + +// ── Direct instance access (DB + agent JWT) ────────────────────────────────── +// Some demo state has no board-facing REST path (execution workspaces) or is +// deliberately agent-only (decisions, secret proposals). These helpers give the +// seed the same two doors a real agent run has, pointed strictly at the +// throw-away instance. + +/** + * Work out which port the screenshot instance's embedded Postgres is listening + * on. run.mjs pins it to a free port (written into config.json), and the live + * cluster records the same value in `postmaster.pid` (line 4 is the port). We + * read the running value first, then config.json. + * + * There is deliberately NO compiled-in fallback to 54329: that is the default a + * developer's real local Paperclip uses, so guessing it could connect a direct + * write into the real database. If neither source yields a port we throw — a + * loud failure is far safer than a silent write to the wrong cluster. + */ +export function resolvePostgresPort(home = scratchHome()) { + const instanceRoot = resolve(home, "instances", INSTANCE_ID); + + try { + const lines = readFileSync(resolve(instanceRoot, "db", "postmaster.pid"), "utf8").split("\n"); + const port = Number.parseInt(lines[3]?.trim() ?? "", 10); + if (Number.isInteger(port) && port > 0) return port; + } catch { + /* fall through */ + } + + try { + const cfg = JSON.parse(readFileSync(resolve(instanceRoot, "config.json"), "utf8")); + const port = cfg?.database?.embeddedPostgresPort; + if (Number.isInteger(port) && port > 0) return port; + } catch { + /* fall through */ + } + + throw new Error( + `could not resolve the screenshot instance's Postgres port from ` + + `${resolve(instanceRoot, "db", "postmaster.pid")} or ${resolve(instanceRoot, "config.json")}. ` + + "Refusing to guess (54329 may be your real local instance).", + ); +} + +/** + * Open a postgres.js connection to the screenshot instance's embedded database. + * The `postgres` client is resolved from the parent repo's node_modules (it is + * not a dependency of paperclip-docs). Caller must `await sql.end()`. + */ +export function openInstanceDb(home = scratchHome()) { + const port = resolvePostgresPort(home); + // Base the require on a path inside the parent's db package so pnpm's + // node_modules layout resolves `postgres` correctly. + const requireFromParent = createRequire(resolve(PARENT_REPO, "packages/db/package.json")); + const postgres = requireFromParent("postgres"); + return postgres(`postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`, { + max: 1, + idle_timeout: 5, + onnotice: () => {}, + }); +} + +/** + * Mint a local agent JWT for the screenshot instance — the same token the + * server hands a real adapter run. + * + * A handful of endpoints are gated on `actor.source === "agent_jwt"` + * specifically and reject the simpler agent API key (secret proposals, for + * one: "Secret proposals require a verified run-bound agent token"). Rather + * than insert those rows behind the API's back, the seed signs a genuine token + * and drives the real endpoint. + * + * Mirrors `server/src/agent-auth-jwt.ts` → `createLocalAgentJwt()`: HS256 over + * a per-instance, per-company derived key, so a token minted here is valid only + * against this throw-away instance and only for the company it names. The + * master secret is read from the instance's own generated `.env`. + * + * @returns {string|null} the token, or null when the instance has no secret. + */ +export function mintAgentJwt( + { agentId, companyId, runId, adapterType = "process", responsibleUserId = "local-board" }, + home = scratchHome(), +) { + let secret; + try { + const env = readFileSync(resolve(home, "instances", INSTANCE_ID, ".env"), "utf8"); + secret = /^PAPERCLIP_AGENT_JWT_SECRET=(.*)$/m.exec(env)?.[1]?.trim(); + } catch { + return null; + } + if (!secret) return null; + + const b64 = (value) => Buffer.from(value, "utf8").toString("base64url"); + const now = Math.floor(Date.now() / 1000); + const claims = { + sub: agentId, + company_id: companyId, + adapter_type: adapterType, + run_id: runId, + responsible_user_id: responsibleUserId, + iat: now, + exp: now + 3600, + iss: "paperclip", + aud: "paperclip-api", + instance_id: INSTANCE_ID, + }; + const signingInput = `${b64(JSON.stringify({ alg: "HS256", typ: "JWT" }))}.${b64(JSON.stringify(claims))}`; + // Domain-separated per-company/per-instance key derivation — see + // deriveCompanySigningKey() in the parent's agent-auth-jwt.ts. + const signingKey = createHmac("sha256", secret) + .update(`jwt:${INSTANCE_ID}:${companyId}`) + .digest("hex"); + const signature = createHmac("sha256", signingKey).update(signingInput).digest("base64url"); + return `${signingInput}.${signature}`; +} diff --git a/scripts/screenshots/routes.mjs b/scripts/screenshots/routes.mjs index 09d8453..cc22787 100644 --- a/scripts/screenshots/routes.mjs +++ b/scripts/screenshots/routes.mjs @@ -50,6 +50,12 @@ const ID_TOKENS = [ "runnerAgentId", // process agent with a completed run (transcript) "runnerRunId", // that run's id "longRunnerAgentId", // process agent left running during capture + // v2026.817.0 surfaces (written by seed-new-surfaces.mjs) + "darkModeDecisionId", // an open agent-proposed decision with three options + "rolloutDecisionId", // an open decision that collects input fields first + "decisionQueueKey", // a named decision queue's key (route segment, not a uuid) + "statusCardId", // a status card with a compiled query and a written summary + "reliabilityStatusCardId", // a second card, so the board is not a single tile ]; /** @@ -134,31 +140,31 @@ export const CAPTURE_TARGETS = [ // ── Instance settings ────────────────────────────────────────────────────── { name: "settings/profile", - route: "/instance/settings/profile", + route: "/{prefix}/company/settings/instance/profile", dependsOn: ["ui/src/pages/ProfileSettings.tsx"], themes: ["light", "dark"], }, { name: "settings/instance-general", - route: "/instance/settings/general", + route: "/{prefix}/company/settings/instance/general", dependsOn: ["ui/src/pages/InstanceGeneralSettings.tsx"], themes: ["light", "dark"], }, { name: "settings/instance-access", - route: "/instance/settings/access", + route: "/{prefix}/company/settings/instance/access", dependsOn: ["ui/src/pages/InstanceAccess.tsx"], themes: ["light", "dark"], }, { name: "settings/scheduler-heartbeats", - route: "/instance/settings/heartbeats", + route: "/{prefix}/company/settings/instance/heartbeats", dependsOn: ["ui/src/pages/InstanceSettings.tsx"], themes: ["light", "dark"], }, { name: "settings/experimental", - route: "/instance/settings/experimental", + route: "/{prefix}/company/settings/instance/experimental", dependsOn: ["ui/src/pages/InstanceExperimentalSettings.tsx"], themes: ["light", "dark"], }, @@ -166,7 +172,7 @@ export const CAPTURE_TARGETS = [ // ── Plugins ──────────────────────────────────────────────────────────────── { name: "plugins/list", - route: "/instance/settings/plugins", + route: "/{prefix}/company/settings/instance/plugins", dependsOn: ["ui/src/pages/PluginManager.tsx"], themes: ["light", "dark"], }, @@ -174,26 +180,26 @@ export const CAPTURE_TARGETS = [ // of scope for the automated batch, but the base page is captured. { name: "plugins/install", - route: "/instance/settings/plugins", + route: "/{prefix}/company/settings/instance/plugins", dependsOn: ["ui/src/pages/PluginManager.tsx"], themes: ["light", "dark"], wait: 1500, // extra settle to allow plugin list to render }, { name: "plugins/detail", - route: "/instance/settings/plugins", + route: "/{prefix}/company/settings/instance/plugins", dependsOn: ["ui/src/pages/PluginPage.tsx"], themes: ["light", "dark"], }, { name: "plugins/settings", - route: "/instance/settings/plugins", + route: "/{prefix}/company/settings/instance/plugins", dependsOn: ["ui/src/pages/PluginSettings.tsx"], themes: ["light", "dark"], }, { name: "plugins/jobs-log", - route: "/instance/settings/plugins", + route: "/{prefix}/company/settings/instance/plugins", dependsOn: ["ui/src/pages/PluginSettings.tsx"], themes: ["light", "dark"], }, @@ -313,6 +319,15 @@ export const CAPTURE_TARGETS = [ route: "/{prefix}/issues/{issueId}", dependsOn: ["ui/src/pages/IssueDetail.tsx"], themes: ["light", "dark"], + // The issue page opens at the top, where the description and sub-tasks + // table live; the chat thread this shot is *about* sits below the fold and + // was almost entirely cropped out. Scrolling to the Chat tab itself is a + // no-op (it is already just inside a 900px viewport), so scroll to the last + // message in the thread — that lands the conversation on screen. + steps: [ + { scrollTo: { text: "Ship the desktop version" } }, + { waitMs: 900 }, + ], }, // Activity tab — same route, manual tab click needed; captured as a separate target. { @@ -366,10 +381,13 @@ export const CAPTURE_TARGETS = [ "ui/src/components/NewIssueDialog.tsx", "ui/src/lib/work-mode-meta.ts", ], + // The "c" keyboard shortcut no longer opens the composer; the sidebar + // "New Task" button does. The mode chip inside the dialog is unchanged. steps: [ - { press: "c" }, - { waitMs: 600 }, + { click: { role: "button", name: "New Task" } }, + { waitMs: 1200 }, { click: { css: "[data-issue-work-mode-chip]" } }, + { waitMs: 700 }, ], themes: ["light", "dark"], }, @@ -472,19 +490,19 @@ export const CAPTURE_TARGETS = [ // ── Adapters ─────────────────────────────────────────────────────────────── { name: "adapters/list", - route: "/instance/settings/adapters", + route: "/{prefix}/company/settings/instance/adapters", dependsOn: ["ui/src/pages/AdapterManager.tsx"], themes: ["light", "dark"], }, { name: "adapters/install", - route: "/instance/settings/adapters", + route: "/{prefix}/company/settings/instance/adapters", dependsOn: ["ui/src/pages/AdapterManager.tsx"], themes: ["light", "dark"], }, { name: "adapters/detail", - route: "/instance/settings/adapters", + route: "/{prefix}/company/settings/instance/adapters", dependsOn: ["ui/src/pages/AdapterManager.tsx"], themes: ["light", "dark"], }, @@ -541,13 +559,15 @@ export const CAPTURE_TARGETS = [ name: "dashboard/activity-feed", route: "/{prefix}/dashboard", dependsOn: ["ui/src/pages/Dashboard.tsx"], - clip: { css: 'xpath=//h3[contains(.,"Recent Activity")]/ancestor::div[contains(@class,"rounded")][1]' }, + // The heading's Card is a sibling now, not an ancestor — the old + // ancestor-div[rounded] xpath matched nothing and fell back to full page. + clip: { css: 'xpath=//h3[contains(.,"Recent Activity")]/parent::div' }, }, { name: "dashboard/task-breakdown-panel", route: "/{prefix}/dashboard", dependsOn: ["ui/src/pages/Dashboard.tsx"], - clip: { css: 'xpath=//h3[contains(.,"Recent Tasks")]/ancestor::div[contains(@class,"rounded")][1]' }, + clip: { css: 'xpath=//h3[contains(.,"Recent Tasks")]/parent::div' }, }, // ── Tasks / inbox ───────────────────────────────────────────────────────── @@ -561,21 +581,29 @@ export const CAPTURE_TARGETS = [ name: "issues/detail-sidebar", route: "/{prefix}/issues/{issueId}", dependsOn: ["ui/src/pages/IssueDetail.tsx"], - steps: [{ click: { title: "Show properties" } }, { waitMs: 600 }], + // The properties pane is open by default now, so the old "Show properties" + // toggle is hidden and clicking it timed out. Just land on the page. + wait: 2000, }, // ── Activity ──────────────────────────────────────────────────────────────── - { name: "activity/activity-log-full", route: "/{prefix}/activity", dependsOn: ["ui/src/pages/Activity.tsx"] }, + { + name: "activity/activity-log-full", + route: "/{prefix}/activity", + // Activity.tsx was deleted in v2026.817.0; /activity now renders the merged + // audit + activity page (and /audit redirects here with ?mode=agents). + dependsOn: ["ui/src/pages/audit/CompanyActivity.tsx", "ui/src/pages/audit/AuditFeed.tsx"], + }, { name: "activity/activity-filters", route: "/{prefix}/activity", - dependsOn: ["ui/src/pages/Activity.tsx"], + dependsOn: ["ui/src/pages/audit/CompanyActivity.tsx", "ui/src/pages/audit/AuditFeed.tsx"], steps: [{ click: { role: "combobox" } }, { waitMs: 500 }], }, { name: "activity/activity-filtered-by-agent", route: "/{prefix}/activity", - dependsOn: ["ui/src/pages/Activity.tsx"], + dependsOn: ["ui/src/pages/audit/CompanyActivity.tsx", "ui/src/pages/audit/AuditFeed.tsx"], steps: [{ click: { role: "combobox" } }, { waitMs: 400 }, { click: { role: "option", name: "Agent" } }, { waitMs: 600 }], }, @@ -601,9 +629,13 @@ export const CAPTURE_TARGETS = [ }, { name: "routines/cron-picker", - route: "/{prefix}/routines/{routineId}", - dependsOn: ["ui/src/pages/RoutineDetail.tsx", "ui/src/components/ScheduleEditor.tsx"], - steps: [{ click: { role: "combobox" } }, { waitMs: 500 }], + // RoutineDetail is tabbed now (Overview / Triggers / Variables / …) and the + // schedule editor lives under Triggers, which has its own URL. Navigating + // straight there beats clicking a combobox that no longer exists. + route: "/{prefix}/routines/{routineId}/triggers", + dependsOn: ["ui/src/pages/RoutineDetail.tsx"], + themes: ["light", "dark"], + wait: 2000, }, // ── Skills ────────────────────────────────────────────────────────────────── @@ -611,14 +643,15 @@ export const CAPTURE_TARGETS = [ name: "skills/assign-to-agent", route: "/{prefix}/skills/{skillId}", dependsOn: ["ui/src/pages/CompanySkills.tsx"], - steps: [{ click: { role: "button", name: "Attach to agents" } }, { waitMs: 600 }], + // Renamed from "Attach to agents" to "Add to agent". + steps: [{ click: { role: "button", name: "Add to agent" } }, { waitMs: 800 }], }, // ── Workspaces ────────────────────────────────────────────────────────────── { name: "workspaces/list", route: "/{prefix}/workspaces", dependsOn: ["ui/src/pages/Workspaces.tsx"] }, // ── Settings ──────────────────────────────────────────────────────────────── - { name: "settings/instance-adapters", route: "/instance/settings/adapters", dependsOn: ["ui/src/pages/AdapterManager.tsx"] }, + { name: "settings/instance-adapters", route: "/{prefix}/company/settings/instance/adapters", dependsOn: ["ui/src/pages/AdapterManager.tsx"] }, // ── Costs ───────────────────────────────────────────────────────────────── { name: "costs/costs-dashboard-overview", route: "/{prefix}/costs", dependsOn: ["ui/src/pages/Costs.tsx"] }, @@ -697,7 +730,8 @@ export const CAPTURE_TARGETS = [ name: "onboarding/sidebar-new-company-button", route: "/{prefix}/dashboard", dependsOn: ["ui/src/components/SidebarCompanyMenu.tsx"], - steps: [{ click: { role: "button", name: /workspace switcher/i } }, { waitMs: 500 }], + // The switcher is labelled "Open company switcher". + steps: [{ click: { role: "button", name: /company switcher/i } }, { waitMs: 700 }], }, { // Captured pre-seed: the company-less instance redirects to /onboarding, @@ -739,7 +773,7 @@ export const CAPTURE_TARGETS = [ }, // ── Adapter "health" — the adapter rows (type · package · N models) ───────── - { name: "adapters/health", route: "/instance/settings/adapters", dependsOn: ["ui/src/pages/AdapterManager.tsx"] }, + { name: "adapters/health", route: "/{prefix}/company/settings/instance/adapters", dependsOn: ["ui/src/pages/AdapterManager.tsx"] }, // ── Onboarding goal field (wizard step 1) ─────────────────────────────────── { @@ -866,12 +900,6 @@ export const CAPTURE_TARGETS = [ dependsOn: ["ui/src/pages/Dashboard.tsx"], clip: { css: 'xpath=(//*[contains(text(),"Cost") or contains(text(),"Spend")]/ancestor::div[contains(@class,"rounded")])[1]' }, }, - { - name: "dashboard/stale-tasks-panel", - route: "/{prefix}/dashboard", - dependsOn: ["ui/src/pages/Dashboard.tsx"], - clip: { css: 'xpath=(//*[contains(text(),"Stale")]/ancestor::div[contains(@class,"rounded")])[1]' }, - }, // Export / import — full pages double as the dialog surfaces. { @@ -890,7 +918,8 @@ export const CAPTURE_TARGETS = [ name: "org/org-chart-add-agent", route: "/{prefix}/org", dependsOn: ["ui/src/pages/OrgChart.tsx"], - steps: [{ click: { role: "button", name: /Add agent/i } }, { waitMs: 600 }], + // The org chart's create button is labelled "New agent". + steps: [{ click: { role: "button", name: /New agent/i } }, { waitMs: 800 }], }, { // Seed assigns Bob → Ada (manager), so the plain chart shows the assignment. @@ -962,13 +991,19 @@ export const CAPTURE_TARGETS = [ // their real content rather than the disabled stubs. { name: "experimental/environments", - route: "/instance/settings/environments", + route: "/{prefix}/company/settings/instance/environments", dependsOn: ["ui/src/pages/CompanyEnvironments.tsx"], }, { + // PAP-era rename: `ui/src/pages/CloudUpstream.tsx` was deleted in + // v2026.817.0 and `/company/settings/cloud-upstream` is now a redirect to + // `/company/export`, where cloud sync lives. Pointing at the old route + // would have captured the export page under the wrong name; point at the + // real surface instead so the shot and its depends_on agree. name: "experimental/cloud-upstream", - route: "/{prefix}/company/settings/cloud-upstream", - dependsOn: ["ui/src/pages/CloudUpstream.tsx"], + route: "/{prefix}/company/export", + dependsOn: ["ui/src/pages/CompanyExport.tsx", "ui/src/api/cloud.ts"], + wait: 2000, }, { name: "experimental/file-viewer-browse", @@ -990,10 +1025,172 @@ export const CAPTURE_TARGETS = [ }, { name: "experimental/recovery-preview", - route: "/instance/settings/experimental", + route: "/{prefix}/company/settings/instance/experimental", dependsOn: ["ui/src/pages/InstanceExperimentalSettings.tsx"], steps: [{ click: { role: "button", name: "Preview" } }, { waitMs: 1500 }], }, + + // ── Decisions (docs/guides/day-to-day/decisions.md) ───────────────────────── + // New in v2026.817.0. The whole surface is gated on `enableDecisions`, which + // seed.mjs turns on, and the queue only has content because + // seed-new-surfaces.mjs proposes decisions through a real agent run. + // + // These pages resolve their gate from an async experimental-settings fetch, + // and the sidebar/feed paint in two passes — a 1200 ms settle reliably caught + // the pre-flag render. Hence the longer `wait` on every target here. + { + name: "decisions/queue-overview", + route: "/{prefix}/decisions", + dependsOn: ["ui/src/pages/WhatNeedsMe.tsx", "ui/src/components/AttentionQueueRow.tsx"], + wait: 4000, + }, + { + name: "decisions/decision-card", + route: "/{prefix}/decisions", + dependsOn: [ + "ui/src/components/DecisionCard.tsx", + "ui/src/components/DecisionResolver.tsx", + "ui/src/components/DecisionTriageStrip.tsx", + ], + // The feed selects and expands its first row, which is the newest decision. + // Click it explicitly anyway so the shot does not depend on that default. + wait: 4000, + steps: [{ click: { text: "Ship dark mode with the system-preference default?" } }, { waitMs: 1500 }], + }, + { + name: "decisions/triage-chips", + route: "/{prefix}/decisions", + dependsOn: ["ui/src/components/DecisionTriageStrip.tsx", "ui/src/components/DecisionDateChips.tsx"], + wait: 4000, + steps: [{ click: { text: "Ship dark mode with the system-preference default?" } }, { waitMs: 1500 }], + // Just the triage strip — the decide-by chips, queue picker and snooze — + // rather than the whole row, which the decision-card shot already covers. + // ancestor::div[2] is the strip's own bordered panel; [3] is the row and + // pulls the whole decision card in with it. + clip: { css: 'xpath=//*[contains(text(),"When to decide")]/ancestor::div[2]' }, + }, + { + name: "decisions/toolbar-filters", + route: "/{prefix}/decisions", + dependsOn: ["ui/src/components/DecisionsToolbar.tsx", "ui/src/components/DecisionDateChips.tsx"], + wait: 4000, + steps: [{ click: { role: "button", name: "Filter" } }, { waitMs: 900 }], + }, + { + name: "decisions/named-queue", + route: "/{prefix}/decisions/queues/{decisionQueueKey}", + dependsOn: ["ui/src/pages/DecisionQueuePage.tsx", "ui/src/components/DecisionQueueRail.tsx"], + wait: 4000, + }, + { + name: "decisions/history-curtains", + route: "/{prefix}/decisions", + dependsOn: ["ui/src/components/DecisionShelf.tsx", "ui/src/pages/WhatNeedsMe.tsx"], + // "Decided" and "Expired" are collapsed by default; open Decided so the + // curtain shows the decision seed-new-surfaces.mjs resolved. + wait: 4000, + steps: [{ click: { text: "Decided" } }, { waitMs: 1200 }], + }, + { + name: "decisions/empty-state", + // The second, empty company has no attention items at all — the only place + // "You're all caught up" is reachable without emptying the demo board. + route: "/{emptyPrefix}/decisions", + dependsOn: ["ui/src/pages/WhatNeedsMe.tsx"], + wait: 4000, + }, + + // ── Status cards (docs/experimental/status-cards.md) ──────────────────────── + // Gated on `enableStatusCards` + a `ready` built-in Summarizer; both are set + // up by the seed. Cards carry a real compiled query and a written summary, so + // the tiles render Fresh rather than an endless "Setting up". + { + name: "status-cards/board", + route: "/{prefix}/status", + dependsOn: ["ui/src/pages/StatusCards/index.tsx", "ui/src/pages/StatusCards/StatusCardTile.tsx"], + wait: 4000, + }, + { + name: "status-cards/new-card-dialog", + route: "/{prefix}/status", + dependsOn: ["ui/src/pages/StatusCards/CreateStatusCardDialog.tsx"], + wait: 4000, + steps: [{ click: { role: "button", name: "New card" } }, { waitMs: 1200 }], + }, + { + name: "status-cards/detail-summary", + route: "/{prefix}/status/{statusCardId}", + dependsOn: ["ui/src/pages/StatusCards/StatusCardDetailDrawer.tsx"], + wait: 4000, + }, + { + name: "status-cards/detail-settings", + route: "/{prefix}/status/{statusCardId}", + dependsOn: ["ui/src/pages/StatusCards/StatusCardSettingsForm.tsx"], + wait: 4000, + steps: [{ click: { role: "tab", name: "Settings" } }, { waitMs: 1200 }], + }, + { + name: "status-cards/detail-watched-issues", + route: "/{prefix}/status/{statusCardId}", + dependsOn: ["ui/src/pages/StatusCards/StatusCardDetailDrawer.tsx"], + wait: 4000, + steps: [{ click: { role: "tab", name: "Watched issues" } }, { waitMs: 1500 }], + }, + { + name: "status-cards/detail-history", + route: "/{prefix}/status/{statusCardId}", + dependsOn: ["ui/src/pages/StatusCards/StatusCardDetailDrawer.tsx"], + wait: 4000, + steps: [{ click: { role: "tab", name: "History" } }, { waitMs: 1500 }], + }, + + // ── Agent secret proposals (docs/administration/secret-scopes.md) ─────────── + // The Proposals tab is local component state, not a URL param, so it can only + // be reached with a click. + { + name: "secrets/proposals-tab", + route: "/{prefix}/company/settings/secrets", + dependsOn: ["ui/src/pages/secrets/ProposalsTab.tsx"], + wait: 3000, + steps: [{ click: { role: "tab", name: "Proposals" } }, { waitMs: 1500 }], + }, + + // ── Chat-style tasks (docs/experimental/task-chat.md) ─────────────────────── + // `phase: "task-chat"` — captured in a separate pass. `enableTaskChatRedesign` + // is instance-wide and replaces the ordinary task detail page, so if it were + // on during the main pass every classic issue/task/work-mode shot would come + // back as the redesigned chat. run.mjs turns the flag on for this phase only + // and turns it back off afterwards. + { + name: "task-chat/thread", + route: "/{prefix}/issues/{issueId}", + dependsOn: [ + "ui/src/components/TaskChatThread.tsx", + "ui/src/components/task-chat/TaskChatBubble.tsx", + "ui/src/components/task-chat/TaskChatTurn.tsx", + ], + phase: "task-chat", + wait: 4000, + }, + { + name: "task-chat/composer-modes", + route: "/{prefix}/issues/{issueId}", + dependsOn: ["ui/src/components/task-chat/TaskChatComposer.tsx"], + phase: "task-chat", + wait: 4000, + steps: [{ click: { role: "button", name: "Agent mode" } }, { waitMs: 1000 }], + }, + { + name: "task-chat/side-pane", + route: "/{prefix}/issues/{issueId}", + dependsOn: [ + "ui/src/components/issue-properties/IssuePropertiesPlansTab.tsx", + "ui/src/components/issue-properties/IssuePropertiesArtifactsTab.tsx", + ], + phase: "task-chat", + wait: 4000, + }, ]; // Hand-authored diagrams that live under the screenshots tree so they deploy with diff --git a/scripts/screenshots/run.mjs b/scripts/screenshots/run.mjs index f29c61c..cd9d0ec 100644 --- a/scripts/screenshots/run.mjs +++ b/scripts/screenshots/run.mjs @@ -25,9 +25,12 @@ * --stale Comma-separated registry file list. * --base-url Override BASE_URL. * --keep Do NOT rm -rf the scratch home after capture. + * --serve Boot + seed, then STAY UP instead of capturing. For iterating on + * seed.mjs and eyeballing screens before committing to a full run. * * Usage: * node scripts/screenshots/run.mjs [--all] [--only ] [--theme light|dark|both] + * node scripts/screenshots/run.mjs --serve # live instance, Ctrl-C to tear down */ import { spawn } from "node:child_process"; @@ -38,6 +41,7 @@ import { BASE_URL, PARENT_REPO, REPO_ROOT, + SEED_IDS_PATH, scratchHome, instanceEnv, instanceConfigPath, @@ -316,7 +320,44 @@ async function main() { console.warn("run: sync-registry warning:", err.message, "(continuing)"); } + // ── 5.5. Serve mode — hand the live instance over and stop here ────────── + // Seeding a screen correctly is iterative: extend seed.mjs, look at the page, + // adjust. Booting the whole instance for each pass costs minutes, so --serve + // keeps it up. `npm run screenshots:seed` re-runs the seed against it in + // place, and `npm run screenshots:capture -- --only ` shoots one target. + if (hasFlag(args, "--serve")) { + const base = getFlag(args, "--base-url") ?? BASE_URL; + console.log(""); + console.log(`run: serving at ${base} — instance is live and seeded.`); + console.log("run: re-seed: npm run screenshots:seed"); + console.log(`run: capture: npm run screenshots:capture -- --only `); + console.log("run: stop: Ctrl-C (tears down the server and scratch home)"); + console.log(""); + // Park forever; the SIGINT/SIGTERM handlers registered above run cleanup(). + await new Promise(() => {}); + return; + } + // ── 6. Capture screenshots ─────────────────────────────────────────────── + // Keep the demo board's issue statuses correct for the whole capture. The + // demo `process` agents finish their runs without declaring a disposition, so + // Paperclip's stalled-work sweep keeps moving those issues to `blocked` with + // an error notice — every few minutes, over a capture that runs much longer + // than that. See reassertIssueStatuses() in seed.mjs. + const seedIds = JSON.parse(await readFile(SEED_IDS_PATH, "utf8").catch(() => "{}")); + let statusKeeper = null; + if (seedIds.companyId) { + const { reassertIssueStatuses } = await import("./seed.mjs"); + statusKeeper = setInterval(() => { + reassertIssueStatuses({ + companyId: seedIds.companyId, + baseUrl: getFlag(args, "--base-url") ?? BASE_URL, + }).catch((err) => console.warn("run: status keeper:", err.message)); + }, 45_000); + // Never hold the process open on this timer alone. + statusKeeper.unref?.(); + } + console.log("run: capturing screenshots…"); try { const capture = await importCapture(); @@ -333,7 +374,38 @@ async function main() { await capture(captureOpts); console.log("run: capture complete."); + + // ── 6b. Task-chat phase ──────────────────────────────────────────────── + // `enableTaskChatRedesign` replaces the ordinary task detail page for the + // whole instance, so it cannot be on during the main pass — every classic + // issue/task/work-mode shot would come back as the redesigned chat. Flip it + // on, shoot only the `phase: "task-chat"` targets, flip it back. + const base = getFlag(args, "--base-url") ?? BASE_URL; + const setTaskChat = (enabled) => + fetch(`${base}/api/instance/settings/experimental`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enableTaskChatRedesign: enabled }), + }); + + try { + const res = await setTaskChat(true); + if (!res.ok) throw new Error(`PATCH experimental → HTTP ${res.status}`); + console.log("run: capturing task-chat screenshots (enableTaskChatRedesign on)…"); + await capture({ ...captureOpts, phase: "task-chat" }); + } catch (err) { + console.error("run: task-chat capture failed (continuing):", err.message); + } finally { + // Always restore, even if the phase threw — leaving the flag on would + // silently poison any later capture against this instance. + await setTaskChat(false).catch((err) => + console.warn("run: could not restore enableTaskChatRedesign:", err.message), + ); + } + + if (statusKeeper) clearInterval(statusKeeper); } catch (err) { + if (statusKeeper) clearInterval(statusKeeper); console.error("run: capture failed:", err); await cleanup(); process.exit(1); diff --git a/scripts/screenshots/seed-new-surfaces.mjs b/scripts/screenshots/seed-new-surfaces.mjs new file mode 100644 index 0000000..d93b070 --- /dev/null +++ b/scripts/screenshots/seed-new-surfaces.mjs @@ -0,0 +1,565 @@ +/** + * seed-new-surfaces.mjs — populates the surfaces that shipped in v2026.817.0: + * Decisions (feed, queues, triage, history), Status Cards, and agent secret + * proposals. + * + * ── Why this is not just more REST calls in seed.mjs ───────────────────────── + * + * All three surfaces are *agent-authored by design*. The seed talks to the + * instance as the local board user, and the board is explicitly not allowed to + * author any of them: + * + * • `POST /companies/:id/decisions` → 403 "Agent run context required" unless + * the actor is an agent WITH a run id, and that run's contextSnapshot must + * carry an `issueId` ("Origin run is not issue-scoped"). + * • `PUT /status-cards/:id/{query,summary}` are the agent's half of card + * generation, and are rejected unless the run matches the card's *active* + * generation task. + * • `POST /agents/me/secret-proposals` additionally demands + * `actor.source === "agent_jwt"` — an agent API key is refused with + * "Secret proposals require a verified run-bound agent token". + * + * So instead of faking rows, this module borrows the two doors a real agent run + * already has: a board-minted agent API key (`POST /agents/:id/keys`, which + * returns the token in plaintext exactly once) for the first two, and a + * locally-signed agent JWT for the third. Every write then goes through the + * same code path production uses, which is the point — a screenshot of a + * hand-inserted row is a screenshot of something users can never see. + * + * Runs are not invented either: assigning issues to agents in seed.mjs already + * produces genuine issue-scoped runs, so `findIssueScopedRun()` just looks one + * up. + * + * ── Endpoint evidence (verified against the v2026.817.0 release commit) ────── + * POST /api/agents/:id/keys agents.ts:3513 → { token } + * POST /api/companies/:id/decisions decisions.ts:138 createSchema + * POST /api/decisions/:id/decide decisions.ts:181 + * POST /api/decisions/:id/dismiss decisions.ts:187 + * GET/POST /api/companies/:id/decision-queues decision-queues.ts:77/83 + * POST /api/companies/:id/decision-queues/:key/items :113 + * PUT /api/companies/:id/decision-triage/:kind/:id :164 + * POST /api/companies/:id/status-cards status-cards.ts:150 { interestPrompt, agentId? } + * PUT /api/status-cards/:id/query status-cards.ts:271 + * PUT /api/status-cards/:id/summary status-cards.ts:290 + * POST /api/agents/me/secret-proposals secrets.ts:243 (agent JWT only) + * POST /api/companies/:id/secret-proposals/:id/approve|reject secrets.ts:303/319 + */ + +import { BASE_URL, mintAgentJwt, openInstanceDb } from "./config.mjs"; + +// ── HTTP helpers ───────────────────────────────────────────────────────────── + +async function request(method, path, { body, baseUrl = BASE_URL, token, runId } = {}) { + const headers = { "Content-Type": "application/json" }; + if (token) headers.Authorization = `Bearer ${token}`; + if (runId) headers["X-Paperclip-Run-Id"] = runId; + const init = { method, headers }; + if (body !== undefined) init.body = JSON.stringify(body); + const res = await fetch(`${baseUrl}${path}`, init); + if (!res.ok) { + const text = await res.text().catch(() => "(unreadable body)"); + throw new Error(`${method} ${path} → HTTP ${res.status}: ${text.slice(0, 300)}`); + } + const text = await res.text(); + return text ? JSON.parse(text) : {}; +} + +const asList = (v) => (Array.isArray(v) ? v : (v?.items ?? [])); + +/** Run a labelled step, swallowing and logging errors so the seed continues. */ +async function step(label, fn, fallback = null) { + try { + return await fn(); + } catch (err) { + console.warn(`[seed-new] ${label} failed (non-fatal): ${err.message}`); + return fallback; + } +} + +// ── Agent identity helpers ─────────────────────────────────────────────────── + +/** Board-mint an agent API key and return its plaintext token. */ +async function agentToken(agentId, baseUrl, name = "docs-screenshots") { + const key = await request("POST", `/api/agents/${agentId}/keys`, { body: { name }, baseUrl }); + return key.token; +} + +/** + * Find a run belonging to `agentId` whose contextSnapshot names an issue. + * seed.mjs's issue assignments generate these naturally; we only read. + */ +async function findIssueScopedRun(sql, agentId) { + const rows = await sql` + select id, context_snapshot->>'issueId' as issue_id + from heartbeat_runs + where agent_id = ${agentId} and context_snapshot->>'issueId' is not null + order by created_at desc + limit 1`; + return rows[0] ? { runId: rows[0].id, issueId: rows[0].issue_id } : null; +} + +// ── Decisions ──────────────────────────────────────────────────────────────── + +/** + * Agent-proposed decisions in the states docs/guides/day-to-day/decisions.md + * walks through: a plain multi-option proposal, one that collects input first, + * one already decided, and one dismissed — so the feed *and* the "Decided" / + * "Expired" curtains all have real content. + */ +const DECISION_SPECS = [ + { + slug: "darkModeDecisionId", + title: "Ship dark mode with the system-preference default?", + body: + "The portal dark theme is done. Defaulting to the OS preference matches the marketing site, " + + "but it changes the look for every existing user on their next load.", + options: [ + { id: "system", label: "Default to system preference", effects: [] }, + { id: "light", label: "Keep light as the default", effects: [] }, + { id: "ask", label: "Ask each user once on first load", effects: [] }, + ], + resolve: null, + }, + { + slug: "rolloutDecisionId", + title: "How should we stage the hero rollout?", + // Inputs render as the "up to four fields first" affordance the guide + // describes, with the options disabled until the required one is filled. + inputs: [ + { id: "percent", label: "Starting traffic share (%)", type: "text", required: true }, + { id: "notes", label: "Anything the on-call should know?", type: "text", required: false }, + ], + body: + "Desktop hero is approved. A staged rollout lets us watch conversion before committing, " + + "at the cost of a longer window where two layouts are live.", + options: [ + { id: "staged", label: "Stage the rollout", effects: [] }, + { id: "full", label: "Ship to everyone now", effects: [] }, + ], + resolve: null, + }, + { + slug: "decidedDecisionId", + title: "Adopt the upstream sandbox proxy fix?", + body: + "Upstream shipped a fix that covers our proxy case. Taking it now means one fewer local patch " + + "to carry, but it pulls in a minor version bump.", + options: [ + { id: "adopt", label: "Adopt the upstream fix", effects: [] }, + { id: "wait", label: "Stay on the local patch for now", effects: [] }, + ], + resolve: { kind: "decide", optionId: "adopt" }, + }, + { + slug: "dismissedDecisionId", + title: "Rename the 'portal' project to 'customer app'?", + body: "Naming came up in review. Nothing depends on it, but the mismatch confuses new joiners.", + options: [ + { id: "rename", label: "Rename it", effects: [] }, + { id: "keep", label: "Keep the current name", effects: [] }, + ], + resolve: { kind: "dismiss", reason: "Not worth the churn this cycle — revisit after launch." }, + }, +]; + +async function seedDecisions({ companyId, baseUrl, sql, agents }) { + const ids = {}; + + // Prefer a worker agent with a real issue-scoped run; the decision's + // provenance line ("Proposed by Bob while running ACM-5") reads from it. + let origin = null; + let originAgentId = null; + for (const agentId of agents) { + const found = await findIssueScopedRun(sql, agentId); + if (found) { + origin = found; + originAgentId = agentId; + break; + } + } + if (!origin) { + console.warn("[seed-new] no issue-scoped agent run found — skipping decisions"); + return ids; + } + + const token = await step("agent key for decisions", () => agentToken(originAgentId, baseUrl, "docs-decisions")); + if (!token) return ids; + + for (const spec of DECISION_SPECS) { + const created = await step(`decision "${spec.title}"`, () => + request("POST", `/api/companies/${companyId}/decisions`, { + baseUrl, + token, + runId: origin.runId, + body: { + title: spec.title, + body: spec.body, + options: spec.options, + ...(spec.inputs ? { inputs: spec.inputs } : {}), + }, + }), + ); + if (!created?.id) continue; + ids[spec.slug] = created.id; + + if (spec.resolve?.kind === "decide") { + await step(`decide "${spec.title}"`, () => + request("POST", `/api/decisions/${created.id}/decide`, { + baseUrl, + body: { optionId: spec.resolve.optionId }, + }), + ); + } else if (spec.resolve?.kind === "dismiss") { + await step(`dismiss "${spec.title}"`, () => + request("POST", `/api/decisions/${created.id}/dismiss`, { + baseUrl, + body: { reason: spec.resolve.reason }, + }), + ); + } + } + + console.log(`[seed-new] seeded ${Object.keys(ids).length} decision(s)`); + return ids; +} + +/** + * Named queues. Paperclip auto-creates PRs / Plans / Questions the first time + * something matches, which the demo board may never trigger, so make sure at + * least one queue exists and has a route to screenshot. + */ +async function seedDecisionQueues({ companyId, baseUrl, decisionIds }) { + const existing = asList( + await step("list decision queues", () => + request("GET", `/api/companies/${companyId}/decision-queues`, { baseUrl }), []), + ); + + let queue = existing.find((q) => q.key === "launch-blockers") ?? null; + if (!queue) { + queue = await step("create decision queue", () => + request("POST", `/api/companies/${companyId}/decision-queues`, { + baseUrl, + // `key` is URL-safe kebab-case and becomes the /decisions/queues/:key + // route segment; the human label is `title`. + body: { + key: "launch-blockers", + title: "Launch blockers", + description: "Anything standing between the current build and the public launch.", + }, + }), + ); + } + if (!queue?.key) return { decisionQueueKey: existing[0]?.key ?? null }; + + // A queue with no items renders an empty lane, which is not what the guide is + // describing. Route the open decisions into it. Adding an item is a pure + // label — it does not move or modify the underlying work. + for (const decisionId of [decisionIds.darkModeDecisionId, decisionIds.rolloutDecisionId].filter(Boolean)) { + await step(`add decision to queue ${queue.key}`, () => + request("POST", `/api/companies/${companyId}/decision-queues/${queue.key}/items`, { + baseUrl, + body: { sourceKind: "decision", sourceId: decisionId }, + }), + ); + } + + console.log(`[seed-new] decision queue "${queue.key}" ready`); + return { decisionQueueKey: queue.key }; +} + +// ── Status cards ───────────────────────────────────────────────────────────── + +/** + * The built-in Summarizer ships provisioned but *paused* ("disabled until + * explicitly configured"), and status-card creation refuses to run without a + * `ready` one. Two things stand between it and ready: + * + * 1. It defaults to the `claude_local` adapter, and the screenshot instance has + * no LLM provider. Swap it to `process` — which is on the Summarizer's own + * `allowedAdapterTypes` list — running a command that outlives the capture. + * That matters: a card's summary can only be written while its generation + * task is still the *active* one, and a fast-exiting command lets the task + * finish first, which flips the card straight to `error`. + * 2. `deriveBuiltInAgentStatus()` reports "paused" when EITHER `status` is + * paused OR `pausedAt` is set, and `pausedAt` is not clearable over REST. + * So that one column is cleared directly. + */ +async function prepareSummarizer({ companyId, baseUrl, sql }) { + const builtIns = await step("list built-in agents", () => + request("GET", `/api/companies/${companyId}/built-in-agents`, { baseUrl }), []); + const summarizer = asList(builtIns).find((b) => b.definition?.key === "summarizer"); + if (!summarizer?.agentId) { + console.warn("[seed-new] no Summarizer built-in agent — skipping status cards"); + return null; + } + + await step("configure summarizer adapter", () => + request("PATCH", `/api/agents/${summarizer.agentId}`, { + baseUrl, + body: { + adapterType: "process", + adapterConfig: { + command: "sh", + args: ["-c", "echo 'Compiling status card…'; sleep 900; echo done"], + timeoutSec: 1200, + }, + }, + }), + ); + + await step("clear summarizer pause", async () => { + await sql` + update agents set paused_at = null, pause_reason = null, status = 'idle' + where id = ${summarizer.agentId}`; + }); + + const after = asList( + await step("re-read built-in agents", () => + request("GET", `/api/companies/${companyId}/built-in-agents`, { baseUrl }), []), + ).find((b) => b.definition?.key === "summarizer"); + if (after?.status !== "ready") { + console.warn(`[seed-new] Summarizer is "${after?.status}" not "ready" — status cards may not seed`); + } + return summarizer.agentId; +} + +const STATUS_CARD_SPECS = [ + { + slug: "statusCardId", + interestPrompt: + "Keep an eye on the Website launch. Tell me whether the homepage hero is shipped, and if not, " + + "the exact next actions to ship it.", + title: "Website launch readiness", + queries: [{ q: "hero", scope: "issues" }, { q: "dark mode", scope: "issues" }], + changeSummary: "Compiled the interest prompt into two issue queries.", + markdown: + "**Not shipped yet — two things left.**\n\n" + + "- The homepage hero is *in progress* with Cleo. Desktop layout is approved; mobile spacing is the open question.\n" + + "- Dark mode for the portal is *todo*, waiting on a decision about the system-preference default.\n" + + "- The accessibility audit of the checkout flow is *todo* and not blocking launch.\n\n" + + "**Next actions**\n\n" + + "1. Answer the dark-mode default decision so the work can start.\n" + + "2. Ship the approved desktop hero and iterate on mobile spacing separately.", + summaryChange: "First full summary: hero in progress, dark mode blocked on a decision.", + }, + { + slug: "reliabilityStatusCardId", + interestPrompt: + "Everything blocked or in review across the Mobile App project this week, and who is holding each one.", + title: "Mobile App — blocked & in review", + queries: [{ q: "mobile", scope: "issues" }], + changeSummary: "Compiled the interest prompt into one project-scoped query.", + markdown: + "**One blocker, one review.**\n\n" + + "- Battery telemetry spike is *blocked* with Eve — waiting on a hardware trace from the fleet team.\n" + + "- Push notifications is in the *backlog* and unassigned.\n\n" + + "Nothing here is on the launch path.", + summaryChange: "First full summary: one blocker with Eve, nothing launch-critical.", + }, +]; + +async function seedStatusCards({ companyId, baseUrl, sql }) { + const ids = {}; + const summarizerAgentId = await prepareSummarizer({ companyId, baseUrl, sql }); + if (!summarizerAgentId) return ids; + + const token = await step("agent key for status cards", () => + agentToken(summarizerAgentId, baseUrl, "docs-status-cards")); + if (!token) return ids; + + for (const spec of STATUS_CARD_SPECS) { + const card = await step(`status card "${spec.title}"`, () => + request("POST", `/api/companies/${companyId}/status-cards`, { + baseUrl, + body: { interestPrompt: spec.interestPrompt }, + }), + ); + if (!card?.id || !card.generatingIssueId) continue; + ids[spec.slug] = card.id; + + // The generation task's run is what authorizes the writes below. It is + // dispatched asynchronously, so wait for it to appear. + const run = await step(`await generation run for "${spec.title}"`, async () => { + for (let attempt = 0; attempt < 20; attempt++) { + const rows = await sql` + select id from heartbeat_runs + where context_snapshot->>'issueId' = ${card.generatingIssueId} + order by created_at desc limit 1`; + if (rows[0]?.id) return rows[0].id; + await new Promise((r) => setTimeout(r, 500)); + } + return null; + }); + if (!run) { + console.warn(`[seed-new] no generation run for "${spec.title}" — card stays in "Setting up"`); + continue; + } + + await step(`write query for "${spec.title}"`, () => + request("PUT", `/api/status-cards/${card.id}/query`, { + baseUrl, + token, + runId: run, + body: { + title: spec.title, + changeSummary: spec.changeSummary, + generationIssueId: card.generatingIssueId, + queries: spec.queries, + }, + }), + ); + + await step(`write summary for "${spec.title}"`, () => + request("PUT", `/api/status-cards/${card.id}/summary`, { + baseUrl, + token, + runId: run, + body: { + markdown: spec.markdown, + changeSummary: spec.summaryChange, + generationIssueId: card.generatingIssueId, + model: "claude-haiku-4-5", + }, + }), + ); + } + + console.log(`[seed-new] seeded ${Object.keys(ids).length} status card(s)`); + return ids; +} + +// ── Secret proposals ───────────────────────────────────────────────────────── + +const SECRET_PROPOSAL_SPECS = [ + { + name: "website/sentry-dsn", + key: "SENTRY_DSN", + description: "Error reporting endpoint for the portal build.", + value: "https://examplePublicKey@o0.ingest.sentry.io/0", + justification: + "The portal build fails without an error-reporting DSN. This is the project-scoped public DSN " + + "from the Sentry dashboard, safe to store as a company secret.", + resolve: null, // stays pending → drives the amber count badge on the tab + }, + { + name: "website/plausible-api-key", + key: "PLAUSIBLE_API_KEY", + description: "Read-only analytics key for the launch dashboard.", + value: "plausible_readonly_example_key_0000", + justification: + "Needed to pull page-view numbers into the launch readiness summary. Read-only scope, " + + "so it cannot change the analytics configuration.", + resolve: "approve", + }, + { + name: "website/deploy-token", + key: "DEPLOY_TOKEN", + description: "Write-scoped token for pushing production builds.", + value: "deploy_example_token_0000000000", + justification: "Would let me deploy the hero change without waiting for a human to run the release job.", + resolve: "reject", + }, +]; + +async function seedSecretProposals({ companyId, baseUrl, sql, agents }) { + let origin = null; + let originAgentId = null; + for (const agentId of agents) { + const found = await findIssueScopedRun(sql, agentId); + if (found) { + origin = found; + originAgentId = agentId; + break; + } + } + if (!origin) { + console.warn("[seed-new] no agent run available — skipping secret proposals"); + return {}; + } + + // Secret proposals are the one surface that insists on a run-bound agent JWT; + // an agent API key is refused outright. + const jwt = mintAgentJwt({ agentId: originAgentId, companyId, runId: origin.runId }); + if (!jwt) { + console.warn("[seed-new] could not mint an agent JWT — skipping secret proposals"); + return {}; + } + + const ids = {}; + for (const spec of SECRET_PROPOSAL_SPECS) { + const proposal = await step(`secret proposal "${spec.name}"`, () => + request("POST", "/api/agents/me/secret-proposals", { + baseUrl, + token: jwt, + body: { + kind: "secret", + name: spec.name, + key: spec.key, + description: spec.description, + value: spec.value, + justification: spec.justification, + }, + }), + ); + if (!proposal?.id) continue; + if (!spec.resolve) ids.pendingSecretProposalId = proposal.id; + + if (spec.resolve === "approve") { + await step(`approve "${spec.name}"`, () => + request("POST", `/api/companies/${companyId}/secret-proposals/${proposal.id}/approve`, { + baseUrl, + body: {}, + }), + ); + } else if (spec.resolve === "reject") { + await step(`reject "${spec.name}"`, () => + request("POST", `/api/companies/${companyId}/secret-proposals/${proposal.id}/reject`, { + baseUrl, + body: { reason: "Deploys stay behind the release job — ask a human to run it." }, + }), + ); + } + } + + console.log(`[seed-new] seeded ${SECRET_PROPOSAL_SPECS.length} secret proposal(s)`); + return ids; +} + +// ── Entry point ────────────────────────────────────────────────────────────── + +/** + * Seed every v2026.817.0 surface. Best-effort throughout: a failure in one + * surface logs and leaves the others alone, matching seed.mjs's contract that + * capture simply skips any route whose id ends up null. + * + * @param {object} input + * @param {string} input.companyId + * @param {string[]} input.agentIds candidate origin agents, most-preferred first + * @param {string} [input.baseUrl] + * @returns {Promise>} ids to merge into .seed-ids.json + */ +export async function seedNewSurfaces({ companyId, agentIds, baseUrl = BASE_URL }) { + if (!companyId || !agentIds?.length) { + console.warn("[seed-new] missing companyId/agentIds; skipping"); + return {}; + } + + let sql; + try { + sql = openInstanceDb(); + } catch (err) { + console.warn(`[seed-new] could not open the instance database (skipping): ${err.message}`); + return {}; + } + + try { + const agents = agentIds.filter(Boolean); + const decisionIds = await seedDecisions({ companyId, baseUrl, sql, agents }); + const queueIds = await seedDecisionQueues({ companyId, baseUrl, decisionIds }); + const cardIds = await seedStatusCards({ companyId, baseUrl, sql }); + const proposalIds = await seedSecretProposals({ companyId, baseUrl, sql, agents }); + return { ...decisionIds, ...queueIds, ...cardIds, ...proposalIds }; + } finally { + await sql.end().catch(() => {}); + } +} diff --git a/scripts/screenshots/seed.mjs b/scripts/screenshots/seed.mjs index e4288fe..7b3cdde 100644 --- a/scripts/screenshots/seed.mjs +++ b/scripts/screenshots/seed.mjs @@ -37,8 +37,9 @@ */ import { writeFileSync } from "node:fs"; -import { BASE_URL, COMPANY_NAME, SEED_IDS_PATH } from "./config.mjs"; +import { BASE_URL, COMPANY_NAME, SEED_IDS_PATH, mintAgentJwt, openInstanceDb } from "./config.mjs"; import { seedExecutionWorkspace } from "./seed-execution-workspace.mjs"; +import { seedNewSurfaces } from "./seed-new-surfaces.mjs"; // ── HTTP helpers ──────────────────────────────────────────────────────────── @@ -81,6 +82,24 @@ const DEMO_AGENT_CONFIG = { timeoutSec: 30, }; +/** + * The demo board's intended issue statuses. + * + * Module-level because two things need them: the initial seed, and + * `reassertIssueStatuses()` below, which run.mjs calls on a timer while + * capture is in flight. + */ +const ISSUE_SPECS = [ + { title: "Design the homepage hero section", status: "in_progress", priority: "high", assignee: "Cleo", project: "Website", goal: true }, + { title: "Set up CI/CD pipeline", status: "done", priority: "medium", assignee: "Bob", project: "Website" }, + { title: "Write onboarding docs", status: "todo", priority: "low", assignee: "Eve", project: "Website" }, + { title: "Fix flaky integration test", status: "in_review", priority: "high", assignee: "Dax", project: "Website" }, + { title: "Add dark mode to the portal", status: "todo", priority: "medium", assignee: "Bob", project: "Website" }, + { title: "Investigate battery telemetry spike", status: "blocked", priority: "critical", assignee: "Eve", project: "Mobile App" }, + { title: "Mobile: implement push notifications", status: "backlog", priority: "medium", assignee: "Bob", project: "Mobile App" }, + { title: "Accessibility audit of checkout flow", status: "todo", priority: "high", assignee: "Cleo", project: "Website" }, +]; + /** * Run a labelled step, swallowing and logging errors so the seed continues. * Returns the callback's result, or `fallback` (default null) on failure. @@ -382,16 +401,7 @@ export default async function seed({ baseUrl = BASE_URL } = {}) { } // ── 8. Issues (mixed statuses, assignees, a parent with sub-issues) ───────── - const issueSpecs = [ - { title: "Design the homepage hero section", status: "in_progress", priority: "high", assignee: "Cleo", project: "Website", goal: true }, - { title: "Set up CI/CD pipeline", status: "done", priority: "medium", assignee: "Bob", project: "Website" }, - { title: "Write onboarding docs", status: "todo", priority: "low", assignee: "Eve", project: "Website" }, - { title: "Fix flaky integration test", status: "in_review", priority: "high", assignee: "Dax", project: "Website" }, - { title: "Add dark mode to the portal", status: "todo", priority: "medium", assignee: "Bob", project: "Website" }, - { title: "Investigate battery telemetry spike", status: "blocked", priority: "critical", assignee: "Eve", project: "Mobile App" }, - { title: "Mobile: implement push notifications", status: "backlog", priority: "medium", assignee: "Bob", project: "Mobile App" }, - { title: "Accessibility audit of checkout flow", status: "todo", priority: "high", assignee: "Cleo", project: "Website" }, - ]; + const issueSpecs = ISSUE_SPECS; const existingIssues = asList(await step("list issues", () => get(C("/issues"), baseUrl), [])); const issues = {}; for (const spec of issueSpecs) { @@ -449,19 +459,89 @@ export default async function seed({ baseUrl = BASE_URL } = {}) { } // ── 9. Comments / chat on the primary issue (fresh runs only) ─────────────── + // A real two-sided thread. The board actor can only post `authorType: "user"` + // comments — the server rejects "agent" from a human actor — which used to + // make the captured chat a monologue of four board messages, and the + // chat-style task page render every bubble on the same side. The agent's + // replies are therefore posted with a genuine run-bound agent JWT, exactly as + // a real adapter run would, so they carry the agent as author and render as + // agent bubbles with an author header. if (issueId && !companyExisted) { - // The authenticated actor is the local board user, so comments must use - // authorType "user" (the server rejects "agent"/"system" from a human actor). - const comments = [ - { body: "Kicking this off — let's get three headline directions up for review.", authorType: "user" }, - { body: "Keep it under eight words and lead with the autonomy angle.", authorType: "user" }, - { body: "Drafted layouts for desktop and mobile — preview is pushed to the workspace.", authorType: "user" }, - { body: "Looks great. Ship the desktop version and we'll iterate on mobile spacing.", authorType: "user" }, + const assigneeAgentId = agents.Cleo?.id ?? workerAgentId; + const thread = [ + { as: "user", body: "Kicking this off — let's get three headline directions up for review." }, + { + as: "agent", + body: + "Three directions are up in the workspace:\n\n" + + "1. *Build with Acme Robotics* — plain, safe, reads like everyone else.\n" + + "2. *Your warehouse, running itself* — leads with autonomy.\n" + + "3. *Ship faster with autonomous teams* — benefit-first.\n\n" + + "My pick is the second. It fits on one line at 375px, which the third doesn't.", + }, + { as: "user", body: "Agreed on the second. Keep it under eight words and lead with the autonomy angle." }, + { + as: "agent", + body: + "Done — desktop and mobile layouts are pushed to the workspace branch. " + + "Desktop is ready to review; mobile still has a spacing issue between the headline and the CTA at 375px.", + }, + { as: "user", body: "Looks great. Ship the desktop version and we'll iterate on mobile spacing separately." }, ]; - for (const c of comments) { - await step("add issue comment", () => post(`/api/issues/${issueId}/comments`, c, baseUrl)); + + // One JWT for the whole thread — minting needs an issue-scoped run for the + // agent, which its issue assignment above has already produced. + let agentAuth = null; + if (assigneeAgentId) { + agentAuth = await step("resolve agent thread identity", async () => { + const sql = openInstanceDb(); + try { + // The assignment wakeups from step 8 are dispatched asynchronously, so + // the run this token binds to may not exist yet. Poll briefly rather + // than silently falling back to a one-sided thread. + let runId = null; + for (let attempt = 0; attempt < 20 && !runId; attempt++) { + const rows = await sql` + select id from heartbeat_runs + where agent_id = ${assigneeAgentId} and context_snapshot->>'issueId' is not null + order by created_at desc limit 1`; + runId = rows[0]?.id ?? null; + if (!runId) await new Promise((r) => setTimeout(r, 500)); + } + if (!runId) { + console.warn("[seed] no issue-scoped run for the assignee — thread falls back to board-only comments"); + return null; + } + const token = mintAgentJwt({ agentId: assigneeAgentId, companyId, runId }); + return token ? { token } : null; + } finally { + await sql.end().catch(() => {}); + } + }); } - console.log("[seed] added chat comments to primary issue"); + + for (const entry of thread) { + if (entry.as === "agent" && agentAuth) { + await step("add agent reply", async () => { + const res = await fetch(`${baseUrl}/api/issues/${issueId}/comments`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${agentAuth.token}`, + }, + body: JSON.stringify({ body: entry.body, authorType: "agent" }), + }); + if (!res.ok) throw new Error(`agent comment → HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`); + }); + } else { + // Falls back to a board comment when no agent identity is available, so + // the thread still has content rather than gaps. + await step("add issue comment", () => + post(`/api/issues/${issueId}/comments`, { body: entry.body, authorType: "user" }, baseUrl), + ); + } + } + console.log(`[seed] added a ${thread.length}-message thread to the primary issue`); } // ── 10. Routine + an execution-history row ────────────────────────────────── @@ -512,12 +592,24 @@ export default async function seed({ baseUrl = BASE_URL } = {}) { } // ── 12. Plugins (install bundled examples) ────────────────────────────────── + // The "kitchen sink" example is deliberately excluded. It is a developer + // demo that mounts itself into every contextual surface it can find, and on + // the issue detail page it injects a "Task Detail View" panel that dumps the + // raw issue JSON plus Action/Context/Modal buttons. That panel dominated the + // shipped issues/detail-chat.png and tasks/* screenshots, so the guides were + // illustrating plugin debug output instead of the task page. Every other + // example still installs, which is all the plugin-list shots need. + const EXCLUDED_PLUGIN_MATCH = /kitchen-sink/i; await step("install bundled plugins", async () => { const examples = asList(await get("/api/plugins/examples", baseUrl)); const installed = asList(await get("/api/plugins", baseUrl)); const installedKeys = new Set(installed.map((p) => p.pluginKey ?? p.packageName)); let count = 0; for (const ex of examples) { + if (EXCLUDED_PLUGIN_MATCH.test(ex.pluginKey ?? ex.packageName ?? "")) { + console.log(`[seed] skipping plugin ${ex.displayName ?? ex.packageName} (injects debug panels into captured surfaces)`); + continue; + } if (installedKeys.has(ex.pluginKey) || installedKeys.has(ex.packageName)) continue; const ok = await step(`install plugin ${ex.displayName ?? ex.packageName}`, async () => { await post("/api/plugins/install", { packageName: ex.localPath, isLocalPath: true }, baseUrl); @@ -676,6 +768,12 @@ export default async function seed({ baseUrl = BASE_URL } = {}) { // flags documented in docs/experimental/. Auto-recovery is left off so the // seeded board doesn't grow recovery tasks mid-capture. // Endpoint: PATCH /api/instance/settings/experimental. + // `enableTaskChatRedesign` is deliberately NOT set here. It is instance-wide + // and rewrites the ordinary task detail page, so turning it on would make + // every classic issue-detail shot (issues/*, tasks/*, work-modes/*, + // watchdogs/*) capture the redesigned chat instead of the page those guides + // describe. run.mjs flips it on only for the `phase: "task-chat"` targets and + // flips it back afterwards. await step("enable experimental flags", async () => { await api("PATCH", "/api/instance/settings/experimental", { enableIsolatedWorkspaces: true, @@ -683,9 +781,15 @@ export default async function seed({ baseUrl = BASE_URL } = {}) { enableExperimentalFileViewer: true, enableExternalObjects: true, enableTaskWatchdogs: true, - enableCloudSync: true, enableServerInfoDebugView: true, enableIssuePlanDecompositions: true, + // v2026.817.0 surfaces. `enableDecisions` and `enableStatusCards` gate the + // /decisions and /status routes (and the whole status-card API); the + // built-in Summarizer that status cards require is enabled alongside. + enableDecisions: true, + enableStatusCards: true, + enableSummaries: true, + enableBuiltInAgents: true, }, baseUrl); console.log("[seed] enabled experimental flags"); }); @@ -864,6 +968,25 @@ export default async function seed({ baseUrl = BASE_URL } = {}) { } } + // ── 14h. v2026.817.0 surfaces (decisions, status cards, secret proposals) ── + // Runs last: every one of these is agent-authored and needs the issue-scoped + // agent runs that the issue assignments above produce. + let newSurfaceIds = {}; + await step("seed v2026.817.0 surfaces", async () => { + newSurfaceIds = await seedNewSurfaces({ + companyId, + // Most-preferred origin agent first — the decision's provenance line reads + // "Proposed by while running ", so a worker agent reads best. + agentIds: [workerAgentId, agents.Cleo?.id, agents.Eve?.id, managerAgentId], + baseUrl, + }); + }); + + // ── 14i. Re-assert the intended issue statuses ───────────────────────────── + // See reassertIssueStatuses() for why this is needed. Run once here so the + // board is correct the moment capture starts; run.mjs keeps it correct. + await step("re-assert issue statuses", () => reassertIssueStatuses({ companyId, baseUrl })); + // ── 15. Persist ids ───────────────────────────────────────────────────────── const ids = { companyPrefix, @@ -885,6 +1008,7 @@ export default async function seed({ baseUrl = BASE_URL } = {}) { longRunnerAgentId, ...budgetIds, ...approvalIds, + ...newSurfaceIds, }; writeFileSync(SEED_IDS_PATH, JSON.stringify(ids, null, 2) + "\n", "utf-8"); @@ -893,6 +1017,45 @@ export default async function seed({ baseUrl = BASE_URL } = {}) { return ids; } +/** + * Put the demo board's issue statuses back to the ones ISSUE_SPECS declares. + * + * Why this exists: the demo agents are `process` adapters that echo a line and + * exit. They complete *successfully* without declaring what happened to the + * issue they were woken for, and Paperclip treats a successful run with no + * disposition as work that stalled — it moves the issue to `blocked` and posts + * a "Successful run missing issue disposition" notice into the thread + * (server/src/services/issues.ts, heartbeat.ts). That is right for a real agent + * that went quiet, but on the demo board it means the hero task the guides + * point at renders as Blocked under an error banner. + * + * Disabling the agents' timer heartbeats does not stop it: the sweep runs over + * the *existing* dispositionless runs, so it re-blocks within a few minutes and + * keeps doing so. A full capture takes far longer than that, which is why this + * is a keeper rather than a one-shot — run.mjs calls it on an interval for the + * duration of the capture. + * + * @param {{ companyId: string, baseUrl?: string }} input + * @returns {Promise} how many issues were corrected + */ +export async function reassertIssueStatuses({ companyId, baseUrl = BASE_URL }) { + const wanted = new Map(ISSUE_SPECS.map((spec) => [spec.title, spec.status])); + let restored = 0; + const all = asList(await get(`/api/companies/${companyId}/issues`, baseUrl)); + for (const issue of all) { + const target = wanted.get(issue.title); + if (!target || issue.status === target) continue; + try { + await patch(`/api/issues/${issue.id}`, { status: target }, baseUrl); + restored++; + } catch (err) { + console.warn(`[seed] could not restore "${issue.title}" to ${target}: ${err.message}`); + } + } + if (restored) console.log(`[seed] re-asserted ${restored} drifted issue status(es)`); + return restored; +} + // ── CLI entry-point ───────────────────────────────────────────────────────── if (process.argv[1] && new URL(import.meta.url).pathname === process.argv[1]) { diff --git a/scripts/sync/anchor-map.json b/scripts/sync/anchor-map.json index 9bcb1d6..d8fc503 100644 --- a/scripts/sync/anchor-map.json +++ b/scripts/sync/anchor-map.json @@ -140,14 +140,14 @@ { "name": "db-migrations", "parent_paths": [ - "packages/db/migrations/**" + "packages/db/src/migrations/**" ], "docs_paths": [ "docs/administration/**", "docs/reference/deploy/database.md" ], "tier": "context-only", - "detect": "Migrations themselves rarely need user-facing docs, but a sudden burst of migrations often correlates with a schema change watched elsewhere. Use this watcher's hits as context only — never produce edits directly." + "detect": "Migrations themselves rarely need user-facing docs, but a sudden burst of migrations often correlates with a schema change watched elsewhere. Use this watcher's hits as context only — never produce edits directly. Deliberately NOT promoted above context-only: most migrations are columns, indexes, and renames that need no prose, and promoting this would put all of them in front of a human for triage. The gap that would justify revisiting is a migration changing user-visible behaviour WITHOUT touching a packages/**/schema/*.ts file the `schemas` watcher already catches — a data backfill that changes a default, a retention window, or a constraint that starts rejecting input that used to be accepted." }, { "name": "release-notes", @@ -159,9 +159,18 @@ } ], "adapter_doc_sources": { - "$comment": "Maps a doc basename (docs/reference/adapters/.md) to the source paths the verifier should search. Use when the doc name does not match the adapter package directory under packages/adapters/.", + "$comment": "Maps a doc basename (docs/reference/adapters/.md) to the source paths the verifier should search. Use when the doc name does not match the adapter package directory under packages/adapters/. Without an entry here, verify-edit.mjs finds no sources and reports every adapter-config-field on the page as unverified — a silent false negative that fires on every run, not a one-off.", "hermes-local": ["packages/adapters/hermes"], - "hermes-gateway": ["packages/adapters/hermes"] + "hermes-gateway": ["packages/adapters/hermes"], + "claude-code": ["packages/adapters/claude-local"], + "codex": ["packages/adapters/codex-local"], + "gemini-cli": ["packages/adapters/gemini-local"], + "opencode": ["packages/adapters/opencode-local"], + "pi": ["packages/adapters/pi-local"], + "http": ["server/src/adapters/http"], + "process": ["server/src/adapters/process"], + "modal": ["packages/plugins/sandbox-providers/modal"], + "sandbox-providers": ["packages/plugins/sandbox-providers"] }, "screenshot_registry": "docs/user-guides/screenshots/registry.json", "quarantine_hours": 24, diff --git a/site/content.json b/site/content.json index c3fb0da..0215620 100644 --- a/site/content.json +++ b/site/content.json @@ -222,10 +222,18 @@ "title": "Task Plan Decomposition Panel", "file": "../docs/experimental/plan-decomposition-panel.md" }, + { + "title": "Chat-Style Tasks", + "file": "../docs/experimental/task-chat.md" + }, { "title": "Task Watchdogs", "file": "../docs/experimental/task-watchdogs.md" }, + { + "title": "Status Cards", + "file": "../docs/experimental/status-cards.md" + }, { "title": "Cloud Sync", "file": "../docs/experimental/cloud-sync.md" @@ -444,6 +452,10 @@ "title": "Attention", "file": "../docs/reference/api/attention.md" }, + { + "title": "Decisions", + "file": "../docs/reference/api/decisions.md" + }, { "title": "Checkbox Confirmations", "file": "../docs/reference/api/checkbox-confirmations.md" @@ -492,6 +504,10 @@ "title": "Summary Slots", "file": "../docs/reference/api/summary-slots.md" }, + { + "title": "Status Cards", + "file": "../docs/reference/api/status-cards.md" + }, { "title": "Dashboard", "file": "../docs/reference/api/dashboard.md" @@ -548,6 +564,10 @@ "title": "Setup Commands", "file": "../docs/reference/cli/setup-commands.md" }, + { + "title": "Service", + "file": "../docs/reference/cli/service.md" + }, { "title": "Dev Environments", "file": "../docs/reference/cli/dev-environments.md" @@ -687,6 +707,10 @@ "title": "Issue Triage", "file": "../docs/reference/skills/bundled/paperclip-operations/issue-triage.md" }, + { + "title": "Status Card Query", + "file": "../docs/reference/skills/bundled/paperclip-operations/status-card-query.md" + }, { "title": "Task Planning", "file": "../docs/reference/skills/bundled/paperclip-operations/task-planning.md" @@ -744,6 +768,10 @@ { "title": "Release Announcement", "file": "../docs/reference/skills/optional/content/release-announcement.md" + }, + { + "title": "Simplified English", + "file": "../docs/reference/skills/optional/content/simplified-english.md" } ] }, @@ -773,6 +801,15 @@ "file": "../docs/reference/skills/optional/research/last30days.md" } ] + }, + { + "title": "Software Development", + "pages": [ + { + "title": "Prepare MCP Integration", + "file": "../docs/reference/skills/optional/software-development/prepare-mcp-integration.md" + } + ] } ] }