Ships the v2026.817.0 docs to live: the doc content that was authored and verified earlier, plus a full screenshot overhaul. The content half (`cdf81e2` and the nightly commits behind it) has already been reviewed. **The screenshots are the part that needs eyes** — `f24d734`, 342 PNGs. ## Screenshots Everything was recaptured against the release commit `213dabab4f8e1f3bb1803a2924c0fea1289fcd4c` (`upstream/candidate/release-2026.817.0`). The previous shots were 375 parent-commits stale (v2026.720.0), with 228 of 310 entries flagged pending. All 342 registry entries now carry that SHA. > ⚠️ Worth knowing: the release commit is **not** an ancestor of the parent's > `master`, and local `master` was four days *older* than it. Capturing against > `master` would have both missed release UI and leaked 93 post-tag commits' worth > of UI into the release docs. ### New coverage (34 entries) Three guides shipped with **no images at all** despite being 115–136 lines. They now have them: | Guide | Shots | |---|---| | `guides/day-to-day/decisions.md` | queue, an agent-proposed decision with its options, triage strip, filter panel, named queue, Decided curtain, empty state | | `experimental/status-cards.md` | board, New card dialog, and all four drawer tabs | | `experimental/task-chat.md` | thread, composer mode picker, side pane | | `reference/api/secrets.md` | the board-side Proposals tab | Chat-Style Tasks is captured in a **separate pass**: `enableTaskChatRedesign` is instance-wide, so leaving it on would have turned every classic issue/task/work-mode shot into the redesigned chat. ## Two defects that were already live Both were shipping in the docs before this PR: 1. **`issues/detail-chat.png` and the `tasks/*` shots were mostly plugin debug output.** The Kitchen Sink example plugin mounts a panel into the issue page that dumps the raw issue JSON, and the seed installed it. Those guides were illustrating a debug panel instead of the task page. The seed no longer installs it. 2. **The seeded task thread was a monologue** — four board-user messages in a row, because the board actor cannot post agent comments. It is now a real two-sided exchange. ## Targets repointed because the UI moved | Target | Why | |---|---| | `activity/*` | `Activity.tsx` deleted; `/activity` renders `pages/audit/CompanyActivity.tsx` | | `experimental/cloud-upstream` | `CloudUpstream.tsx` deleted; the route now redirects to `/company/export` | | `settings/*`, `plugins/*`, `adapters/*` | `/instance/settings/*` are legacy redirects now | | `dashboard/stale-tasks-panel` | panel removed from `Dashboard.tsx` — target dropped | Plus eight targets with **silent selector drift**. A failed interaction step 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. Full table in `docs/user-guides/screenshots/SCREENSHOTS_PLAN_v2026.817.0.md` §C2. ## Pipeline Decisions, status-card summaries and secret proposals are all **agent-authored**, and the board actor the seed runs as is refused by every one of them (`"Agent run context required"`, `"Secret proposals require a verified run-bound agent token"`). Rather than insert rows behind the API, `seed-new-surfaces.mjs` borrows the two doors a real run already has — a board-minted agent API key, and a locally signed agent JWT — so every write goes through the production code path. Also: - `capture.mjs` records a real `captured_against` on a detached checkout instead of the literal string `"HEAD"` (which is what every pre-2026.817 entry recorded), and gains a `scrollTo` step for content below the fold. - `run.mjs` gains `--serve` for iterating on the seed, the task-chat phase, and a keeper that holds the demo board's issue statuses steady — the demo `process` agents finish runs without declaring a disposition, so the stalled-work sweep re-blocks issues every few minutes over a capture that runs far longer. ## Reviewing Light and dark both captured at 1440×900 @2x. The new surfaces are the ones to scrutinise; the rest is a like-for-like refresh. ### One leftover Dropping `dashboard/stale-tasks-panel` leaves `{light,dark}/dashboard/stale-tasks-panel.png` tracked but unused — they need deleting by hand. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: DevRel Gemini <[email protected]> Co-authored-by: Paperclip <[email protected]> Co-authored-by: Dotta <[email protected]> Co-authored-by: ClaudeCoder <[email protected]>
14 KiB
paperclip_version
| paperclip_version |
|---|
| v2026.817.0 |
Companies
Companies are the top-level tenant boundary in Paperclip. Every agent, project, issue, approval, cost event, and asset belongs to exactly one company, and the API enforces that boundary on every company-scoped route.
Board callers can list and manage companies. Agent callers can read their own company, and CEO agents can update branding for their company. Deleting a company is destructive; archiving is not.
Company shape
The company API returns a compact company object with the fields you will use most often:
| Field | Meaning |
|---|---|
id |
Stable company id |
name |
Display name |
description |
Optional plain-text description |
status |
active, paused, or archived |
issuePrefix |
Auto-generated issue prefix, based on the company name |
issueCounter |
Next issue number for the company |
budgetMonthlyCents |
Company budget ceiling in cents |
spentMonthlyCents |
Current month spend in cents |
requireBoardApprovalForNewAgents |
Whether hires need board approval |
brandColor |
Optional hex brand color |
logoAssetId |
Stored logo asset id, or null |
logoUrl |
Derived content URL for the stored logo, or null |
createdAt |
Creation timestamp |
updatedAt |
Last update timestamp |
The response also includes feedback-sharing consent fields when they are set. logoUrl is derived from logoAssetId; you do not set it directly.
List companies
GET /api/companies
Returns the companies the current board caller can access. In normal use this is your company list, not every company in the instance.
Note: This is a board route. Agent callers do not use it.
Get a company
GET /api/companies/{companyId}
Returns one company object if the caller has access to that company.
List company artifacts
GET /api/companies/{companyId}/artifacts
Returns a company-wide projection of the outputs agents produced while working. It flattens three sources — agent-authored issue documents, direct issue attachments, and artifact work products — into one card-ready list, so callers do not have to stitch per-issue endpoints together. Company access is checked before the projection runs.
Query parameters
| Parameter | Values | Description |
|---|---|---|
kind |
image, video, text, document, file, all |
Filter by media kind. Defaults to all. |
projectId |
UUID | Restrict to artifacts from issues in one project. |
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=truewith akindother thanallordocumenttherefore 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=truegets an empty list rather than someone else's stars. cursorpaging still works, and pages by when you starred each document.
To add or remove a star, use the document route on the Resource Memberships 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.
{
"artifacts": [
{
"id": "work_product:...",
"source": "work_product",
"mediaKind": "video",
"title": "Launch teaser render",
"previewText": null,
"contentType": "video/mp4",
"contentPath": "/api/attachments/.../content",
"openPath": "/api/attachments/.../content",
"downloadPath": "/api/attachments/.../content?download=1",
"issue": { "id": "...", "identifier": "PAP-123", "title": "Produce launch teaser" },
"project": { "id": "...", "name": "Launch" },
"createdByAgent": { "id": "...", "name": "Video Agent" },
"updatedAt": "2026-06-01T12:00:00.000Z",
"href": "/PAP/issues/PAP-123#work-product-..."
}
],
"nextCursor": null
}
Each artifact carries:
| Field | Meaning |
|---|---|
id |
Stable artifact id, prefixed by source (document:, attachment:, or work_product:) |
source |
document, attachment, or work_product |
mediaKind |
image, video, text, document, file, or empty |
title |
Display title |
previewText |
Short plain-text preview, or null |
contentType |
MIME type of the underlying content, or null |
contentPath |
Path to the raw content, or null |
openPath |
Path for inline open, or null |
downloadPath |
Path for download, or null |
issue |
{ id, identifier, title } of the originating issue |
project |
{ id, name } of the issue's project, or null |
createdByAgent |
{ id, name } of the producing agent, or null |
updatedAt |
Last-updated timestamp |
href |
Deep link to the artifact on its originating issue |
A group object (returned under groups / selectedGroup) carries id, groupBy, issue, title, count, mediaKinds, previewArtifacts, updatedAt, and href.
Create a company
POST /api/companies
{
"name": "Horizon Labs",
"description": "An autonomous research and marketing company",
"budgetMonthlyCents": 50000
}
Creates a new company and assigns it an automatic issue prefix. If budgetMonthlyCents is greater than 0, Paperclip also creates the matching monthly budget policy in UTC calendar-month mode.
This route is board-facing. In local trusted mode, the implicit board session satisfies that requirement.
curl -X POST "http://localhost:3100/api/companies" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Horizon Labs",
"description": "An autonomous research and marketing company",
"budgetMonthlyCents": 50000
}'
const res = await fetch("http://localhost:3100/api/companies", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Horizon Labs",
description: "An autonomous research and marketing company",
budgetMonthlyCents: 50000,
}),
});
const company = await res.json();
import requests
res = requests.post(
"http://localhost:3100/api/companies",
headers={"Authorization": f"Bearer {token}"},
json={
"name": "Horizon Labs",
"description": "An autonomous research and marketing company",
"budgetMonthlyCents": 50000,
},
)
company = res.json()
Update a company
PATCH /api/companies/{companyId}
{
"name": "Horizon Labs",
"description": "Updated description",
"budgetMonthlyCents": 75000,
"status": "active",
"brandColor": "#2563eb",
"logoAssetId": "11111111-1111-4111-8111-111111111111"
}
Board callers can update the full company record here. CEO agents can also call this route, but only with branding fields. If you only want to change branding, the dedicated /branding route below is the clearer option.
Important branding caveat:
logoAssetIdmust point to an asset that belongs to the same company.- Replacing or clearing the logo removes the previous logo asset record.
logoUrlis derived by the server after the logo is linked.
curl -X PATCH "http://localhost:3100/api/companies/company-1" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Horizon Labs",
"description": "Updated description",
"budgetMonthlyCents": 75000,
"status": "active",
"brandColor": "#2563eb"
}'
const res = await fetch("http://localhost:3100/api/companies/company-1", {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Horizon Labs",
description: "Updated description",
budgetMonthlyCents: 75000,
status: "active",
brandColor: "#2563eb",
}),
});
const company = await res.json();
import requests
res = requests.patch(
"http://localhost:3100/api/companies/company-1",
headers={"Authorization": f"Bearer {token}"},
json={
"name": "Horizon Labs",
"description": "Updated description",
"budgetMonthlyCents": 75000,
"status": "active",
"brandColor": "#2563eb",
},
)
company = res.json()
Update branding
PATCH /api/companies/{companyId}/branding
{
"name": "Horizon Labs",
"brandColor": "#2563eb",
"logoAssetId": "11111111-1111-4111-8111-111111111111"
}
Use this when you want a narrower branding-only update. The route accepts:
namedescriptionbrandColorlogoAssetId
Board callers can use it. CEO agents can use it only for their own company.
If you pass a logoAssetId that belongs to a different company, the API returns 422.
Upload a company logo
POST /api/companies/{companyId}/logo
Content-Type: multipart/form-data
This route uploads the logo image itself and returns an asset record. It does not attach the logo to the company automatically. After the upload succeeds, send the returned assetId to PATCH /api/companies/{companyId} or PATCH /api/companies/{companyId}/branding as logoAssetId.
Accepted image types:
image/pngimage/jpegimage/jpgimage/webpimage/gifimage/svg+xml
Logo uploads use the normal attachment size limit. SVG files are sanitized before storage. Scripts and external links are stripped, and empty or unsafe SVGs are rejected.
curl -X POST "http://localhost:3100/api/companies/company-1/logo" \
-H "Authorization: Bearer <token>" \
-F "file=@./logo.svg"
const formData = new FormData();
formData.append("file", fileInput.files[0]);
const res = await fetch("http://localhost:3100/api/companies/company-1/logo", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
});
const result = await res.json();
import requests
with open("logo.svg", "rb") as f:
res = requests.post(
"http://localhost:3100/api/companies/company-1/logo",
headers={"Authorization": f"Bearer {token}"},
files={"file": ("logo.svg", f, "image/svg+xml")},
)
result = res.json()
The upload response includes assetId and contentPath. contentPath points at /api/assets/{assetId}/content.
Company stats
GET /api/companies/stats
Returns a simple summary keyed by company id:
{
"company-1": { "agentCount": 5, "issueCount": 42 }
}
This route is board-only.
Feedback traces
GET /api/companies/{companyId}/feedback-traces
Board-only route for reviewing feedback activity across a company. Query filters include:
targetTypevotestatusissueIdprojectIdfromtosharedOnlyincludePayload
Use this when you want to inspect the feedback loop, not normal company activity.
Export and import
Paperclip exposes both broad and company-scoped portability routes:
| Route | Purpose | Who can call it |
|---|---|---|
POST /api/companies/{companyId}/export |
Export a company bundle | Any caller with access to the company |
POST /api/companies/{companyId}/exports/preview |
Preview a company export | Board or CEO of that company |
POST /api/companies/{companyId}/exports |
Export a company bundle with the stricter portability flow | Board or CEO of that company |
POST /api/companies/import/preview |
Preview an import into a new or existing company | Board only |
POST /api/companies/{companyId}/imports/preview |
Preview an import into the same company | Board or CEO of that company |
POST /api/companies/{companyId}/imports/apply |
Apply an import into the same company | Board or CEO of that company |
The company-scoped imports/* routes are intentionally safer:
- they can only target the route company
- they reject the
replacecollision strategy
Use the board-level preview route when you need to inspect an import that would create a brand-new company or target a different company.
Archive or delete
POST /api/companies/{companyId}/archive
DELETE /api/companies/{companyId}
Archiving changes the company status to archived and removes it from default listings. It also runs a cascade that pauses every active agent in the company (pause reason company_archived) and cancels their queued or in-flight wake-up requests, so an archived company stops doing background work. Deleting removes the company and its related data. Deletion is destructive and should be treated as irreversible.
Practical notes
- Company access is always checked before reading or mutating company data.
logoUrlis derived from the stored logo asset and should be treated as read-only.- If you are updating branding only, prefer
PATCH /api/companies/{companyId}/branding. - If you are uploading a logo, remember the upload and the company update are two separate steps.