mirror of
https://github.com/paperclipai/paperclip-docs.git
synced 2026-11-10 12:01:30 -10:00
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]>
1069 lines
48 KiB
JavaScript
1069 lines
48 KiB
JavaScript
/**
|
|
* seed.mjs — create a rich, realistic demo dataset on a running Paperclip
|
|
* instance so the screenshot pipeline captures a company that looks genuinely
|
|
* in-use rather than a blank demo.
|
|
*
|
|
* Runs against an instance in local_trusted mode where every request is
|
|
* auto-authorized as instance admin (no auth headers needed).
|
|
*
|
|
* Every individual API call is soft-failed (try/catch, logged, non-fatal) so a
|
|
* single unsupported endpoint never aborts the whole seed — capture simply
|
|
* skips any route whose id ends up null.
|
|
*
|
|
* ── Endpoint evidence (verified against parent repo) ─────────────────────────
|
|
* POST /api/companies companies.ts:291 createCompanySchema (company.ts:16)
|
|
* note: issuePrefix is AUTO-DERIVED (first 3 uppercase alpha chars of
|
|
* name) and not settable via REST. "Acme Robotics" → "ACM". The real
|
|
* prefix is read back and written to .seed-ids.json as companyPrefix.
|
|
* PATCH /api/companies/:id companies.ts:329 updateCompanySchema (company.ts:25)
|
|
* { description?, budgetMonthlyCents?, brandColor?:#rrggbb, ... }
|
|
* POST /api/companies/:id/skills company-skills.ts:162 companySkillCreateSchema (company-skill.ts:154)
|
|
* POST /api/companies/:id/agents agents.ts:2123 createAgentSchema (agent.ts:66)
|
|
* { name, role?, title?, icon?, reportsTo?, desiredSkills?, adapterType, adapterConfig?,
|
|
* instructionsBundle?:{entryFile?,files}, budgetMonthlyCents? }
|
|
* POST /api/companies/:id/goals goals.ts:32 createGoalSchema (goal.ts:4) level∈company|team|agent|task
|
|
* POST /api/companies/:id/projects projects.ts:118 createProjectSchema (project.ts:113) status∈backlog|planned|in_progress|completed|cancelled
|
|
* POST /api/projects/:id/workspaces projects.ts:258 createProjectWorkspaceSchema (project.ts:84) requires cwd or repoUrl
|
|
* POST /api/companies/:id/issues issues.ts:3505 createIssueSchema (issue.ts:371) status∈backlog|todo|in_progress|in_review|done|blocked|cancelled, priority∈critical|high|medium|low, parentId for sub-issues
|
|
* POST /api/issues/:id/comments issues.ts:5611 addIssueCommentSchema (issue.ts:535) { body, authorType?:user|agent|system }
|
|
* POST /api/companies/:id/routines routines.ts:96 createRoutineSchema (routine.ts:52)
|
|
* POST /api/routines/:id/run routines.ts:414 runRoutineSchema (routine.ts:147) requires assigneeAgentId; persists a run row before any LLM work
|
|
* POST /api/companies/:id/invites access.ts:2946 createCompanyInviteSchema (access.ts:12) { allowedJoinTypes, humanRole? }
|
|
* GET /api/plugins/examples plugins.ts:792 -> [{ packageName, localPath, ... }]
|
|
* POST /api/plugins/install plugins.ts:1006 { packageName, version?, isLocalPath? }
|
|
* POST /api/companies/:id/cost-events costs.ts:73 createCostEventSchema (cost.ts:4) { agentId, provider, model, costCents, occurredAt, ... }
|
|
* POST /api/companies/:id/finance-events costs.ts:102 createFinanceEventSchema (finance.ts:4) { eventKind, biller, amountCents, occurredAt, direction? }
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
*/
|
|
|
|
import { writeFileSync } from "node:fs";
|
|
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 ────────────────────────────────────────────────────────────
|
|
|
|
async function api(method, path, body, baseUrl) {
|
|
const init = { method, headers: { "Content-Type": "application/json" } };
|
|
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}`);
|
|
}
|
|
// Some endpoints (202) may return empty bodies.
|
|
const text = await res.text();
|
|
return text ? JSON.parse(text) : {};
|
|
}
|
|
|
|
const get = (path, base) => api("GET", path, undefined, base);
|
|
const post = (path, body, base) => api("POST", path, body, base);
|
|
const patch = (path, body, base) => api("PATCH", path, body, base);
|
|
|
|
/** Normalise list responses that may be an array or `{ items: [...] }`. */
|
|
const asList = (v) => (Array.isArray(v) ? v : (v?.items ?? []));
|
|
|
|
/**
|
|
* Benign process-adapter config used for the demo org. The screenshot instance
|
|
* runs with NO LLM provider configured, and the scheduler heartbeats every
|
|
* enabled agent. A `process` agent created with an empty adapterConfig has no
|
|
* command to run, so each heartbeat fails instantly and the agent renders with a
|
|
* red "error" status ("Ada failed after 1 second", dashboard "N errors") — which
|
|
* makes the captured UI look broken. Giving every demo agent a command that
|
|
* always exits 0 turns those heartbeats into clean, completed runs, so agents
|
|
* render as healthy idle/working with a tidy transcript instead of errors.
|
|
*/
|
|
const DEMO_AGENT_CONFIG = {
|
|
command: "sh",
|
|
args: [
|
|
"-c",
|
|
"echo 'Reviewing assigned work…'; echo 'Nothing actionable this cycle — standing by.'; echo 'Done.'",
|
|
],
|
|
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.
|
|
*/
|
|
async function step(label, fn, fallback = null) {
|
|
try {
|
|
return await fn();
|
|
} catch (err) {
|
|
console.warn(`[seed] ${label} failed (non-fatal): ${err.message}`);
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
// ── Seed ──────────────────────────────────────────────────────────────────────
|
|
|
|
export default async function seed({ baseUrl = BASE_URL } = {}) {
|
|
console.log(`[seed] targeting ${baseUrl}`);
|
|
|
|
// ── 1. Company (find-or-create) ─────────────────────────────────────────────
|
|
let company;
|
|
let companyExisted = false;
|
|
const existingCompanies = asList(await get("/api/companies", baseUrl));
|
|
company = existingCompanies.find((c) => c.name === COMPANY_NAME);
|
|
if (company) {
|
|
companyExisted = true;
|
|
console.log(`[seed] reusing company "${company.name}" (${company.id}), prefix=${company.issuePrefix}`);
|
|
} else {
|
|
company = await post(
|
|
"/api/companies",
|
|
{
|
|
name: COMPANY_NAME,
|
|
description:
|
|
"Acme Robotics builds autonomous warehouse robots. This workspace runs the engineering, design, and research org end to end.",
|
|
},
|
|
baseUrl,
|
|
);
|
|
console.log(`[seed] created company "${company.name}" (${company.id}), prefix=${company.issuePrefix}`);
|
|
}
|
|
const companyId = company.id;
|
|
const companyPrefix = company.issuePrefix;
|
|
const C = (p) => `/api/companies/${companyId}${p}`;
|
|
|
|
// ── 2. Company branding + budget ────────────────────────────────────────────
|
|
await step("company branding/budget", async () => {
|
|
await patch(
|
|
`/api/companies/${companyId}`,
|
|
{
|
|
description:
|
|
"Acme Robotics builds autonomous warehouse robots. This workspace runs the engineering, design, and research org end to end.",
|
|
brandColor: "#6366f1",
|
|
budgetMonthlyCents: 750_000, // $7,500 / month
|
|
},
|
|
baseUrl,
|
|
);
|
|
console.log("[seed] set company brand color + monthly budget");
|
|
});
|
|
|
|
// ── 3. Company skills ───────────────────────────────────────────────────────
|
|
// Created before agents so agents can reference them in desiredSkills.
|
|
const SKILLS = [
|
|
{
|
|
name: "Code Review",
|
|
slug: "code-review",
|
|
description: "Review pull requests for correctness, style, and security.",
|
|
markdown:
|
|
"# Code Review\n\nReview diffs for correctness, security, and clarity. Leave actionable comments and approve only when CI is green.",
|
|
},
|
|
{
|
|
name: "Release Notes",
|
|
slug: "release-notes",
|
|
description: "Draft user-facing release notes from merged changes.",
|
|
markdown:
|
|
"# Release Notes\n\nSummarise merged changes in friendly, user-facing language grouped by Highlights, Fixes, and Internal.",
|
|
},
|
|
{
|
|
name: "Customer Triage",
|
|
slug: "customer-triage",
|
|
description: "Triage inbound support reports and route them.",
|
|
markdown:
|
|
"# Customer Triage\n\nClassify inbound reports by severity, reproduce when possible, and route to the right owner.",
|
|
},
|
|
{
|
|
name: "Incident Response",
|
|
slug: "incident-response",
|
|
description: "Coordinate response during production incidents.",
|
|
markdown:
|
|
"# Incident Response\n\nDeclare severity, post a status update every 15 minutes, and write a post-mortem within 48 hours.",
|
|
},
|
|
];
|
|
const existingSkills = asList(await step("list skills", () => get(C("/skills"), baseUrl), []));
|
|
for (const s of SKILLS) {
|
|
const found = existingSkills.find((x) => x.name === s.name || x.slug === s.slug);
|
|
if (found) {
|
|
console.log(`[seed] reusing skill "${s.name}"`);
|
|
continue;
|
|
}
|
|
await step(`create skill ${s.name}`, async () => {
|
|
await post(C("/skills"), s, baseUrl);
|
|
console.log(`[seed] created skill "${s.name}"`);
|
|
});
|
|
}
|
|
|
|
// ── 4. Agents (a small but full org) ────────────────────────────────────────
|
|
const agentSpecs = [
|
|
{
|
|
name: "Ada",
|
|
role: "ceo",
|
|
title: "Chief Executive",
|
|
icon: "crown",
|
|
manager: true,
|
|
budgetMonthlyCents: 300_000,
|
|
desiredSkills: ["release-notes"],
|
|
instructions:
|
|
"# Ada — Chief Executive\n\nYou run Acme Robotics. Set direction, unblock the team, and keep work aligned to company goals. Delegate execution to your reports.",
|
|
},
|
|
{
|
|
name: "Bob",
|
|
role: "engineer",
|
|
title: "Senior Engineer",
|
|
icon: "code",
|
|
budgetMonthlyCents: 200_000,
|
|
desiredSkills: ["code-review", "incident-response"],
|
|
instructions:
|
|
"# Bob — Senior Engineer\n\nImplement features and fix bugs on the Website and Mobile App projects. Write tests, open small PRs, and keep the build green.",
|
|
},
|
|
{
|
|
name: "Cleo",
|
|
role: "designer",
|
|
title: "Product Designer",
|
|
icon: "sparkles",
|
|
budgetMonthlyCents: 150_000,
|
|
desiredSkills: [],
|
|
instructions:
|
|
"# Cleo — Product Designer\n\nOwn the visual design system and review UI work for polish and accessibility.",
|
|
},
|
|
{
|
|
name: "Dax",
|
|
role: "qa",
|
|
title: "QA Engineer",
|
|
icon: "bug",
|
|
budgetMonthlyCents: 120_000,
|
|
desiredSkills: ["code-review", "customer-triage"],
|
|
instructions:
|
|
"# Dax — QA Engineer\n\nVerify changes against acceptance criteria, file clear bug reports, and triage inbound issues.",
|
|
},
|
|
{
|
|
name: "Eve",
|
|
role: "researcher",
|
|
title: "Research Lead",
|
|
icon: "telescope",
|
|
budgetMonthlyCents: 180_000,
|
|
desiredSkills: [],
|
|
instructions:
|
|
"# Eve — Research Lead\n\nInvestigate hard technical questions and produce concise, cited findings the team can act on.",
|
|
},
|
|
];
|
|
|
|
const existingAgents = asList(await step("list agents", () => get(C("/agents"), baseUrl), []));
|
|
const agents = {}; // name -> agent record
|
|
let managerId = null;
|
|
|
|
// Create the manager first so reports can point at it.
|
|
for (const spec of agentSpecs) {
|
|
const found = existingAgents.find((a) => a.name === spec.name);
|
|
if (found) {
|
|
agents[spec.name] = found;
|
|
if (spec.manager) managerId = found.id;
|
|
console.log(`[seed] reusing agent "${spec.name}" (${found.id})`);
|
|
continue;
|
|
}
|
|
const created = await step(`create agent ${spec.name}`, () =>
|
|
post(
|
|
C("/agents"),
|
|
{
|
|
name: spec.name,
|
|
role: spec.role,
|
|
title: spec.title,
|
|
icon: spec.icon,
|
|
adapterType: "process",
|
|
adapterConfig: DEMO_AGENT_CONFIG,
|
|
budgetMonthlyCents: spec.budgetMonthlyCents,
|
|
...(spec.manager ? {} : managerId ? { reportsTo: managerId } : {}),
|
|
...(spec.desiredSkills.length ? { desiredSkills: spec.desiredSkills } : {}),
|
|
instructionsBundle: {
|
|
entryFile: "AGENT.md",
|
|
files: { "AGENT.md": spec.instructions },
|
|
},
|
|
},
|
|
baseUrl,
|
|
),
|
|
);
|
|
if (created?.id) {
|
|
agents[spec.name] = created;
|
|
if (spec.manager) managerId = created.id;
|
|
console.log(`[seed] created agent "${spec.name}" (${created.id}), role=${spec.role}`);
|
|
}
|
|
}
|
|
|
|
const managerAgentId = agents.Ada?.id ?? null;
|
|
const workerAgentId = agents.Bob?.id ?? null;
|
|
const agentList = Object.values(agents);
|
|
|
|
// ── 5. Goals ────────────────────────────────────────────────────────────────
|
|
const goalSpecs = [
|
|
{ title: "Launch v1", description: "Ship the first public release of the warehouse robot platform.", status: "active" },
|
|
{ title: "Improve reliability", description: "Drive crash-free sessions above 99.5%.", status: "planned" },
|
|
];
|
|
const existingGoals = asList(await step("list goals", () => get(C("/goals"), baseUrl), []));
|
|
const goals = {};
|
|
for (const g of goalSpecs) {
|
|
const found = existingGoals.find((x) => x.title === g.title);
|
|
if (found) {
|
|
goals[g.title] = found;
|
|
console.log(`[seed] reusing goal "${g.title}"`);
|
|
continue;
|
|
}
|
|
const created = await step(`create goal ${g.title}`, () =>
|
|
post(C("/goals"), { ...g, level: "company", ownerAgentId: managerAgentId ?? undefined }, baseUrl),
|
|
);
|
|
if (created?.id) {
|
|
goals[g.title] = created;
|
|
console.log(`[seed] created goal "${g.title}" (${created.id})`);
|
|
}
|
|
}
|
|
const goalId = goals["Launch v1"]?.id ?? null;
|
|
|
|
// ── 6. Projects ─────────────────────────────────────────────────────────────
|
|
const projectSpecs = [
|
|
{
|
|
name: "Website",
|
|
description: "Public marketing site and customer portal.",
|
|
status: "in_progress",
|
|
goalIds: goalId ? [goalId] : [],
|
|
},
|
|
{
|
|
name: "Mobile App",
|
|
description: "iOS and Android companion app for fleet operators.",
|
|
status: "planned",
|
|
goalIds: goalId ? [goalId] : [],
|
|
},
|
|
];
|
|
const existingProjects = asList(await step("list projects", () => get(C("/projects"), baseUrl), []));
|
|
const projects = {};
|
|
for (const p of projectSpecs) {
|
|
const found = existingProjects.find((x) => x.name === p.name);
|
|
if (found) {
|
|
projects[p.name] = found;
|
|
console.log(`[seed] reusing project "${p.name}"`);
|
|
continue;
|
|
}
|
|
const created = await step(`create project ${p.name}`, () =>
|
|
post(
|
|
C("/projects"),
|
|
{
|
|
name: p.name,
|
|
description: p.description,
|
|
status: p.status,
|
|
...(p.goalIds.length ? { goalIds: p.goalIds } : {}),
|
|
...(managerAgentId ? { leadAgentId: managerAgentId } : {}),
|
|
},
|
|
baseUrl,
|
|
),
|
|
);
|
|
if (created?.id) {
|
|
projects[p.name] = created;
|
|
console.log(`[seed] created project "${p.name}" (${created.id})`);
|
|
}
|
|
}
|
|
const projectId = projects.Website?.id ?? null;
|
|
|
|
// ── 7. Workspace (under the Website project) ────────────────────────────────
|
|
let workspaceId = null;
|
|
if (projectId) {
|
|
const detail = await step("get project detail", () => get(`/api/projects/${projectId}`, baseUrl), {});
|
|
const existingWs = Array.isArray(detail.workspaces)
|
|
? detail.workspaces.find((w) => w.name === "main")
|
|
: null;
|
|
if (existingWs) {
|
|
workspaceId = existingWs.id;
|
|
console.log(`[seed] reusing workspace "main" (${workspaceId})`);
|
|
} else {
|
|
const created = await step("create workspace", () =>
|
|
post(
|
|
`/api/projects/${projectId}/workspaces`,
|
|
{
|
|
name: "main",
|
|
sourceType: "git_repo",
|
|
cwd: "/srv/acme/website",
|
|
repoUrl: "https://github.com/acme-robotics/website",
|
|
},
|
|
baseUrl,
|
|
),
|
|
);
|
|
if (created?.id) {
|
|
workspaceId = created.id;
|
|
console.log(`[seed] created workspace "main" (${workspaceId})`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── 8. Issues (mixed statuses, assignees, a parent with sub-issues) ─────────
|
|
const issueSpecs = ISSUE_SPECS;
|
|
const existingIssues = asList(await step("list issues", () => get(C("/issues"), baseUrl), []));
|
|
const issues = {};
|
|
for (const spec of issueSpecs) {
|
|
const found = existingIssues.find((x) => x.title === spec.title);
|
|
if (found) {
|
|
issues[spec.title] = found;
|
|
continue;
|
|
}
|
|
const created = await step(`create issue "${spec.title}"`, () =>
|
|
post(
|
|
C("/issues"),
|
|
{
|
|
title: spec.title,
|
|
description: `${spec.title}. Tracked for the ${spec.project} project.`,
|
|
status: spec.status,
|
|
priority: spec.priority,
|
|
...(agents[spec.assignee]?.id ? { assigneeAgentId: agents[spec.assignee].id } : {}),
|
|
...(projects[spec.project]?.id ? { projectId: projects[spec.project].id } : {}),
|
|
...(spec.goal && goalId ? { goalId } : {}),
|
|
},
|
|
baseUrl,
|
|
),
|
|
);
|
|
if (created?.id) {
|
|
issues[spec.title] = created;
|
|
console.log(`[seed] created issue "${spec.title}" (${created.id})`);
|
|
}
|
|
}
|
|
|
|
// The primary issue we point screenshots at (homepage hero — in progress, assigned).
|
|
const primaryIssue = issues["Design the homepage hero section"] ?? Object.values(issues)[0] ?? null;
|
|
const issueId = primaryIssue?.id ?? null;
|
|
|
|
// Sub-issues under the primary issue (only seed when fresh to avoid dupes).
|
|
if (issueId && !companyExisted) {
|
|
const subTitles = ["Hero: choose headline copy", "Hero: build responsive layout"];
|
|
for (const title of subTitles) {
|
|
await step(`create sub-issue "${title}"`, () =>
|
|
post(
|
|
C("/issues"),
|
|
{
|
|
title,
|
|
description: `${title} (child of the hero section work).`,
|
|
status: "todo",
|
|
priority: "medium",
|
|
parentId: issueId,
|
|
...(workerAgentId ? { assigneeAgentId: workerAgentId } : {}),
|
|
...(projectId ? { projectId } : {}),
|
|
},
|
|
baseUrl,
|
|
),
|
|
);
|
|
}
|
|
console.log("[seed] created sub-issues under primary issue");
|
|
}
|
|
|
|
// ── 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) {
|
|
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." },
|
|
];
|
|
|
|
// 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(() => {});
|
|
}
|
|
});
|
|
}
|
|
|
|
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 ──────────────────────────────────
|
|
let routineId = null;
|
|
const existingRoutines = asList(await step("list routines", () => get(C("/routines"), baseUrl), []));
|
|
const foundRoutine = existingRoutines.find((r) => r.title === "Daily standup");
|
|
if (foundRoutine) {
|
|
routineId = foundRoutine.id;
|
|
console.log(`[seed] reusing routine "Daily standup" (${routineId})`);
|
|
} else {
|
|
const created = await step("create routine", () =>
|
|
post(
|
|
C("/routines"),
|
|
{
|
|
title: "Daily standup",
|
|
description: "Automated daily status summary across active projects.",
|
|
assigneeAgentId: managerAgentId ?? undefined,
|
|
priority: "medium",
|
|
status: "active",
|
|
},
|
|
baseUrl,
|
|
),
|
|
);
|
|
if (created?.id) {
|
|
routineId = created.id;
|
|
console.log(`[seed] created routine "Daily standup" (${routineId})`);
|
|
}
|
|
}
|
|
// Trigger one run so the routine detail page shows an execution row. A run
|
|
// record is persisted before any LLM work, so this populates history even
|
|
// with no provider configured (the downstream agent work just won't run).
|
|
if (routineId && managerAgentId && !companyExisted) {
|
|
await step("trigger routine run", () =>
|
|
post(`/api/routines/${routineId}/run`, { assigneeAgentId: managerAgentId, source: "manual" }, baseUrl),
|
|
);
|
|
console.log("[seed] triggered one routine run (history row)");
|
|
}
|
|
|
|
// ── 11. Invites (fresh runs only) ───────────────────────────────────────────
|
|
if (!companyExisted) {
|
|
await step("create invite (operator)", () =>
|
|
post(C("/invites"), { allowedJoinTypes: "human", humanRole: "operator" }, baseUrl),
|
|
);
|
|
await step("create invite (both)", () =>
|
|
post(C("/invites"), { allowedJoinTypes: "both", humanRole: "viewer" }, baseUrl),
|
|
);
|
|
console.log("[seed] created invites");
|
|
}
|
|
|
|
// ── 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);
|
|
return true;
|
|
});
|
|
if (ok) count++;
|
|
}
|
|
console.log(`[seed] installed ${count} bundled plugin(s) (of ${examples.length} available)`);
|
|
});
|
|
|
|
// ── 13. Cost + finance events (fresh runs only — no natural key) ────────────
|
|
if (!companyExisted && agentList.length) {
|
|
await step("seed cost + finance events", async () => {
|
|
const providers = [
|
|
{ provider: "anthropic", model: "claude-opus-4-8", biller: "Anthropic" },
|
|
{ provider: "anthropic", model: "claude-sonnet-4-6", biller: "Anthropic" },
|
|
{ provider: "openai", model: "gpt-5.4", biller: "OpenAI" },
|
|
{ provider: "google", model: "gemini-2.5-pro", biller: "Google" },
|
|
];
|
|
const now = Date.now();
|
|
const day = 24 * 60 * 60 * 1000;
|
|
let costEvents = 0;
|
|
// ~32 cost events spread across the last 26 days, cycling agents/providers.
|
|
for (let i = 0; i < 32; i++) {
|
|
const agent = agentList[i % agentList.length];
|
|
const pr = providers[i % providers.length];
|
|
const occurredAt = new Date(now - (i % 26) * day - (i % 7) * 3600_000).toISOString();
|
|
const inputTokens = 1200 + (i * 137) % 9000;
|
|
const outputTokens = 400 + (i * 89) % 3000;
|
|
const costCents = 5 + ((i * 17) % 240); // 5..244 cents
|
|
const ok = await step(`cost-event #${i}`, async () => {
|
|
await post(
|
|
C("/cost-events"),
|
|
{
|
|
agentId: agent.id,
|
|
provider: pr.provider,
|
|
model: pr.model,
|
|
costCents,
|
|
occurredAt,
|
|
biller: pr.biller,
|
|
billingType: "metered_api",
|
|
inputTokens,
|
|
outputTokens,
|
|
},
|
|
baseUrl,
|
|
);
|
|
return true;
|
|
});
|
|
if (ok) costEvents++;
|
|
}
|
|
console.log(`[seed] seeded ${costEvents} cost events`);
|
|
|
|
// A handful of finance events (charges + a credit purchase).
|
|
const finance = [
|
|
{ eventKind: "inference_charge", biller: "Anthropic", amountCents: 18_400, direction: "debit" },
|
|
{ eventKind: "inference_charge", biller: "OpenAI", amountCents: 9_250, direction: "debit" },
|
|
{ eventKind: "platform_fee", biller: "Paperclip", amountCents: 4_900, direction: "debit" },
|
|
{ eventKind: "credit_purchase", biller: "Anthropic", amountCents: 50_000, direction: "credit" },
|
|
{ eventKind: "log_storage_charge", biller: "Paperclip", amountCents: 1_200, direction: "debit" },
|
|
];
|
|
let financeEvents = 0;
|
|
for (let i = 0; i < finance.length; i++) {
|
|
const f = finance[i];
|
|
const occurredAt = new Date(now - i * 4 * day).toISOString();
|
|
const ok = await step(`finance-event #${i}`, async () => {
|
|
await post(
|
|
C("/finance-events"),
|
|
{ ...f, currency: "USD", occurredAt },
|
|
baseUrl,
|
|
);
|
|
return true;
|
|
});
|
|
if (ok) financeEvents++;
|
|
}
|
|
console.log(`[seed] seeded ${financeEvents} finance events`);
|
|
});
|
|
}
|
|
|
|
// ── 14. Extended coverage entities ──────────────────────────────────────────
|
|
// These back the extra screenshot targets added in routes.mjs. All best-effort.
|
|
|
|
// 14a. A second, EMPTY company for empty-state shots (no agents/data).
|
|
let emptyPrefix = null;
|
|
await step("create empty company", async () => {
|
|
const all = asList(await get("/api/companies", baseUrl));
|
|
let empty = all.find((c) => c.name === "Newco Labs");
|
|
if (!empty) {
|
|
empty = await post("/api/companies", { name: "Newco Labs", description: "A brand-new, empty workspace." }, baseUrl);
|
|
console.log(`[seed] created empty company "Newco Labs" (${empty.id}), prefix=${empty.issuePrefix}`);
|
|
} else {
|
|
console.log(`[seed] reusing empty company (prefix=${empty.issuePrefix})`);
|
|
}
|
|
emptyPrefix = empty.issuePrefix;
|
|
});
|
|
|
|
// 14b. Budget-utilisation demo agents — seed cost events to 50% / 80% / 100%.
|
|
// Each gets budgetMonthlyCents=100000 ($1,000); spend is summed from cost events.
|
|
const budgetAgents = {
|
|
budgetHalfAgentId: { name: "Budget Demo (50%)", spend: 50_000, pause: false },
|
|
budgetWarnAgentId: { name: "Budget Demo (80%)", spend: 80_000, pause: false },
|
|
budgetMaxAgentId: { name: "Budget Demo (100%)", spend: 100_000, pause: true },
|
|
};
|
|
const budgetIds = { budgetHalfAgentId: null, budgetWarnAgentId: null, budgetMaxAgentId: null };
|
|
for (const [token, spec] of Object.entries(budgetAgents)) {
|
|
await step(`budget agent ${spec.name}`, async () => {
|
|
const existing = asList(await get(C("/agents"), baseUrl)).find((a) => a.name === spec.name);
|
|
let agent = existing;
|
|
if (!agent) {
|
|
agent = await post(
|
|
C("/agents"),
|
|
{ name: spec.name, role: "general", adapterType: "process", adapterConfig: DEMO_AGENT_CONFIG, budgetMonthlyCents: 100_000, reportsTo: managerAgentId ?? undefined },
|
|
baseUrl,
|
|
);
|
|
}
|
|
budgetIds[token] = agent.id;
|
|
if (!companyExisted) {
|
|
// One cost event sized to the target utilisation (current month).
|
|
await post(
|
|
C("/cost-events"),
|
|
{ agentId: agent.id, provider: "anthropic", model: "claude-opus-4-8", costCents: spec.spend, occurredAt: new Date().toISOString(), biller: "Anthropic", billingType: "metered_api", inputTokens: 5000, outputTokens: 2000 },
|
|
baseUrl,
|
|
);
|
|
if (spec.pause) {
|
|
await api("PATCH", `/api/agents/${agent.id}`, { status: "paused" }, baseUrl);
|
|
}
|
|
}
|
|
console.log(`[seed] budget agent "${spec.name}" → ${agent.id} (${spec.spend / 1000} of $1000)`);
|
|
});
|
|
}
|
|
|
|
// 14c. An HTTP-adapter agent so the http config form is reachable.
|
|
let httpAgentId = null;
|
|
await step("create http-adapter agent", async () => {
|
|
const existing = asList(await get(C("/agents"), baseUrl)).find((a) => a.name === "Webhook Worker");
|
|
let agent = existing;
|
|
if (!agent) {
|
|
agent = await post(
|
|
C("/agents"),
|
|
{ name: "Webhook Worker", role: "general", adapterType: "http", adapterConfig: {}, reportsTo: managerAgentId ?? undefined },
|
|
baseUrl,
|
|
);
|
|
}
|
|
httpAgentId = agent.id;
|
|
console.log(`[seed] http-adapter agent → ${agent.id}`);
|
|
});
|
|
|
|
// 14d. A company skill id (for the skill-detail / assign-to-agent shots).
|
|
let skillId = null;
|
|
await step("resolve a skill id", async () => {
|
|
const list = asList(await get(C("/skills"), baseUrl));
|
|
skillId = (list.find((s) => s.slug === "code-review") ?? list[0])?.id ?? null;
|
|
});
|
|
|
|
// 14e. Enable the experimental flags whose surfaces the docs screenshot:
|
|
// isolated workspaces (otherwise /workspaces redirects to /issues) plus the
|
|
// 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,
|
|
enableEnvironments: true,
|
|
enableExperimentalFileViewer: true,
|
|
enableExternalObjects: true,
|
|
enableTaskWatchdogs: 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");
|
|
});
|
|
|
|
// 14e-2. An issue that references external work objects (GitHub PR/issue
|
|
// URLs) for the external-objects screenshot. Created after the flag flips on
|
|
// so URL detection runs on create; the refresh call resolves live status when
|
|
// the capture host has GitHub egress (non-fatal when offline).
|
|
let externalObjectIssueId = null;
|
|
await step("create external-object issue", async () => {
|
|
const existing = asList(await get(C("/issues"), baseUrl)).find((i) => i.title === "Adopt upstream sandbox proxy fix");
|
|
let issue = existing;
|
|
if (!issue) {
|
|
issue = await post(
|
|
C("/issues"),
|
|
{
|
|
title: "Adopt upstream sandbox proxy fix",
|
|
description:
|
|
"Upstream shipped the fix in https://github.com/paperclipai/paperclip/pull/8512 — verify it covers our case, tracked upstream in https://github.com/paperclipai/paperclip/issues/4419.",
|
|
status: "in_progress",
|
|
priority: "medium",
|
|
assigneeAgentId: workerAgentId ?? undefined,
|
|
projectId: projectId ?? undefined,
|
|
},
|
|
baseUrl,
|
|
);
|
|
}
|
|
externalObjectIssueId = issue?.id ?? null;
|
|
if (externalObjectIssueId) {
|
|
try {
|
|
await post(`/api/issues/${externalObjectIssueId}/external-objects/refresh`, {}, baseUrl);
|
|
} catch (err) {
|
|
console.warn("[seed] external-object refresh failed (non-fatal):", err.message);
|
|
}
|
|
}
|
|
console.log(`[seed] external-object issue → ${externalObjectIssueId}`);
|
|
});
|
|
|
|
// 14f. Approval items (fresh runs only — no natural key).
|
|
const approvalIds = { hireApprovalId: null, strategyApprovalId: null, boardApprovalId: null, approvedApprovalId: null };
|
|
if (!companyExisted) {
|
|
await step("seed approvals", async () => {
|
|
const hire = await post(C("/approvals"), {
|
|
type: "hire_agent",
|
|
payload: { name: "Aria Chen", role: "engineer", title: "Senior Engineer", icon: "code", adapterType: "claude_local", adapterConfig: {}, budgetMonthlyCents: 150_000, desiredSkills: [], capabilities: "Backend development, API design", agentId: null },
|
|
}, baseUrl);
|
|
approvalIds.hireApprovalId = hire?.id ?? null;
|
|
|
|
const strat = await post(C("/approvals"), {
|
|
type: "approve_ceo_strategy",
|
|
payload: { title: "Q3 Product Roadmap", plan: "Expand into the enterprise segment by shipping SSO and audit logs in Q3.", description: "Strategic pivot toward mid-market and enterprise accounts." },
|
|
}, baseUrl);
|
|
approvalIds.strategyApprovalId = strat?.id ?? null;
|
|
|
|
const board = await post(C("/approvals"), {
|
|
type: "request_board_approval",
|
|
payload: { title: "Vendor contract: cloud reserved instances", summary: "Commit to a 1-year reservation to cut infra cost ~35%.", recommendedAction: "Approve the $12,000 annual commitment" },
|
|
}, baseUrl);
|
|
approvalIds.boardApprovalId = board?.id ?? null;
|
|
|
|
// One more, then approve it, for the "approved" state.
|
|
const toApprove = await post(C("/approvals"), {
|
|
type: "request_board_approval",
|
|
payload: { title: "Increase log retention to 90 days", summary: "Improves debuggability for incident response." },
|
|
}, baseUrl);
|
|
if (toApprove?.id) {
|
|
await post(`/api/approvals/${toApprove.id}/approve`, {}, baseUrl);
|
|
approvalIds.approvedApprovalId = toApprove.id;
|
|
}
|
|
console.log("[seed] seeded approvals:", JSON.stringify(approvalIds));
|
|
});
|
|
}
|
|
|
|
// 14g. Process-adapter "runner" agents — execute a real shell command (no LLM)
|
|
// so we get genuine run rows, a transcript, a running status, and a passing
|
|
// environment test. The process adapter runs `command` + `args` directly.
|
|
let runnerAgentId = null;
|
|
let runnerRunId = null;
|
|
let longRunnerAgentId = null;
|
|
|
|
await step("create + run a process agent (completed run → transcript)", async () => {
|
|
const existing = asList(await get(C("/agents"), baseUrl)).find((a) => a.name === "Task Runner");
|
|
let agent = existing;
|
|
if (!agent) {
|
|
agent = await post(
|
|
C("/agents"),
|
|
{
|
|
name: "Task Runner",
|
|
role: "engineer",
|
|
title: "Automation",
|
|
icon: "terminal",
|
|
adapterType: "process",
|
|
adapterConfig: {
|
|
command: "sh",
|
|
args: ["-c", "echo 'Analyzing repository...'; echo 'Applying changes to src/app.ts'; echo 'Running tests — 42 passed'; echo 'Done.'"],
|
|
timeoutSec: 30,
|
|
},
|
|
reportsTo: managerAgentId ?? undefined,
|
|
},
|
|
baseUrl,
|
|
);
|
|
}
|
|
runnerAgentId = agent.id;
|
|
if (!companyExisted) {
|
|
const run = await post(`/api/agents/${agent.id}/wakeup`, { source: "on_demand", triggerDetail: "manual" }, baseUrl);
|
|
runnerRunId = run?.id ?? run?.heartbeatRun?.id ?? run?.run?.id ?? null;
|
|
console.log(`[seed] Task Runner run → ${runnerRunId}`);
|
|
} else {
|
|
// Reuse the most recent completed run if present.
|
|
const runs = asList(await step("list runner runs", () => get(`/api/agents/${agent.id}/runs`, baseUrl), []));
|
|
runnerRunId = runs[0]?.id ?? null;
|
|
}
|
|
});
|
|
|
|
// Give the short run a moment to execute and complete before we capture it.
|
|
if (runnerRunId && !companyExisted) {
|
|
await new Promise((r) => setTimeout(r, 4000));
|
|
}
|
|
|
|
await step("create a long-running process agent (running status)", async () => {
|
|
const existing = asList(await get(C("/agents"), baseUrl)).find((a) => a.name === "Live Task");
|
|
let agent = existing;
|
|
if (!agent) {
|
|
agent = await post(
|
|
C("/agents"),
|
|
{
|
|
name: "Live Task",
|
|
role: "engineer",
|
|
title: "Long Job",
|
|
icon: "cpu",
|
|
adapterType: "process",
|
|
adapterConfig: {
|
|
// Long enough to outlast the full capture phase so the run is still
|
|
// in-flight (status "running", an in-progress run row) when capture
|
|
// reaches the run/status targets near the end of the list.
|
|
command: "sh",
|
|
args: ["-c", "echo 'Starting long task...'; sleep 3600; echo done"],
|
|
timeoutSec: 4200,
|
|
},
|
|
reportsTo: managerAgentId ?? undefined,
|
|
},
|
|
baseUrl,
|
|
);
|
|
}
|
|
longRunnerAgentId = agent.id;
|
|
// Kick it off LAST so it is still running throughout the capture phase.
|
|
if (!companyExisted) {
|
|
await post(`/api/agents/${agent.id}/wakeup`, { source: "on_demand", triggerDetail: "manual" }, baseUrl);
|
|
console.log("[seed] Live Task wakeup triggered (stays running during capture)");
|
|
}
|
|
});
|
|
|
|
// ── 14f. Execution workspace (direct DB insert) ─────────────────────────────
|
|
// Execution workspaces have no public create endpoint — they're provisioned by
|
|
// a real agent heartbeat. For screenshots we insert a realistic one directly so
|
|
// the execution-workspace detail tabs render instead of "Loading workspace…".
|
|
let executionWorkspaceId = null;
|
|
if (projectId && issueId) {
|
|
try {
|
|
const linkedIssueIds = [
|
|
issueId,
|
|
issues["Fix flaky integration test"]?.id,
|
|
issues["Add dark mode to the portal"]?.id,
|
|
].filter(Boolean);
|
|
const result = await seedExecutionWorkspace({
|
|
companyId,
|
|
projectId,
|
|
projectWorkspaceId: workspaceId,
|
|
sourceIssueId: issueId,
|
|
linkedIssueIds,
|
|
issuePrefix: companyPrefix,
|
|
});
|
|
executionWorkspaceId = result?.executionWorkspaceId ?? null;
|
|
} catch (err) {
|
|
console.warn("[seed] execution workspace seed failed (non-fatal):", err.message);
|
|
}
|
|
}
|
|
|
|
// ── 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 <name> while running <ISSUE>", 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,
|
|
companyId,
|
|
managerAgentId,
|
|
agentId: workerAgentId, // Bob — used for the agent-detail screenshots
|
|
projectId,
|
|
goalId,
|
|
routineId,
|
|
issueId,
|
|
workspaceId,
|
|
executionWorkspaceId,
|
|
emptyPrefix,
|
|
httpAgentId,
|
|
skillId,
|
|
externalObjectIssueId,
|
|
runnerAgentId,
|
|
runnerRunId,
|
|
longRunnerAgentId,
|
|
...budgetIds,
|
|
...approvalIds,
|
|
...newSurfaceIds,
|
|
};
|
|
|
|
writeFileSync(SEED_IDS_PATH, JSON.stringify(ids, null, 2) + "\n", "utf-8");
|
|
console.log(`[seed] wrote ids to ${SEED_IDS_PATH}`);
|
|
console.log("[seed] done:", JSON.stringify(ids, null, 2));
|
|
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<number>} 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]) {
|
|
const urlFlagIdx = process.argv.indexOf("--base-url");
|
|
const baseUrl = urlFlagIdx !== -1 ? process.argv[urlFlagIdx + 1] : BASE_URL;
|
|
seed({ baseUrl }).catch((err) => {
|
|
console.error("[seed] fatal:", err.message);
|
|
process.exit(1);
|
|
});
|
|
}
|