| import { randomUUID } from "node:crypto"; |
| import { definePlugin, runWorker, type PluginApiRequestInput } from "@paperclipai/plugin-sdk"; |
|
|
| type SmokeInput = { |
| companyId: string; |
| issueId: string; |
| assigneeAgentId?: string | null; |
| actorAgentId?: string | null; |
| actorUserId?: string | null; |
| actorRunId?: string | null; |
| }; |
|
|
| type SmokeSummary = { |
| rootIssueId: string; |
| childIssueId: string | null; |
| blockerIssueId: string | null; |
| billingCode: string; |
| joinedRows: unknown[]; |
| subtreeIssueIds: string[]; |
| wakeupQueued: boolean; |
| }; |
|
|
| let readSmokeSummary: ((companyId: string, issueId: string) => Promise<SmokeSummary | null>) | null = null; |
| let initializeSmoke: ((input: SmokeInput) => Promise<SmokeSummary>) | null = null; |
|
|
| function tableName(namespace: string) { |
| return `${namespace}.smoke_runs`; |
| } |
|
|
| function stringField(value: unknown): string | null { |
| return typeof value === "string" && value.trim().length > 0 ? value : null; |
| } |
|
|
| const plugin = definePlugin({ |
| async setup(ctx) { |
| readSmokeSummary = async function readSummary(companyId: string, issueId: string): Promise<SmokeSummary | null> { |
| const rows = await ctx.db.query<{ |
| root_issue_id: string; |
| child_issue_id: string | null; |
| blocker_issue_id: string | null; |
| billing_code: string; |
| issue_title: string; |
| last_summary: unknown; |
| }>( |
| `SELECT s.root_issue_id, s.child_issue_id, s.blocker_issue_id, s.billing_code, i.title AS issue_title, s.last_summary |
| FROM ${tableName(ctx.db.namespace)} s |
| JOIN public.issues i ON i.id = s.root_issue_id |
| WHERE s.root_issue_id = $1`, |
| [issueId], |
| ); |
| const row = rows[0]; |
| if (!row) return null; |
| const orchestration = await ctx.issues.summaries.getOrchestration({ |
| issueId, |
| companyId, |
| includeSubtree: true, |
| billingCode: row.billing_code, |
| }); |
| return { |
| rootIssueId: row.root_issue_id, |
| childIssueId: row.child_issue_id, |
| blockerIssueId: row.blocker_issue_id, |
| billingCode: row.billing_code, |
| joinedRows: rows, |
| subtreeIssueIds: orchestration.subtreeIssueIds, |
| wakeupQueued: Boolean((row.last_summary as { wakeupQueued?: unknown } | null)?.wakeupQueued), |
| }; |
| }; |
|
|
| initializeSmoke = async function runSmoke(input: SmokeInput): Promise<SmokeSummary> { |
| const root = await ctx.issues.get(input.issueId, input.companyId); |
| if (!root) throw new Error(`Issue not found: ${input.issueId}`); |
|
|
| const billingCode = `plugin-smoke:${input.issueId}`; |
| const actor = { |
| actorAgentId: input.actorAgentId ?? null, |
| actorUserId: input.actorUserId ?? null, |
| actorRunId: input.actorRunId ?? null, |
| }; |
| const blocker = await ctx.issues.create({ |
| companyId: input.companyId, |
| parentId: input.issueId, |
| inheritExecutionWorkspaceFromIssueId: input.issueId, |
| title: "Orchestration smoke blocker", |
| description: "Resolved blocker used to verify plugin relation writes without preventing the smoke wakeup.", |
| status: "done", |
| priority: "low", |
| billingCode, |
| originKind: `plugin:${ctx.manifest.id}:blocker`, |
| originId: `${input.issueId}:blocker`, |
| actor, |
| }); |
|
|
| const child = await ctx.issues.create({ |
| companyId: input.companyId, |
| parentId: input.issueId, |
| inheritExecutionWorkspaceFromIssueId: input.issueId, |
| title: "Orchestration smoke child", |
| description: "Generated by the orchestration smoke plugin to verify issue, document, relation, wakeup, and summary APIs.", |
| status: "todo", |
| priority: "medium", |
| assigneeAgentId: input.assigneeAgentId ?? root.assigneeAgentId ?? undefined, |
| billingCode, |
| originKind: `plugin:${ctx.manifest.id}:child`, |
| originId: `${input.issueId}:child`, |
| blockedByIssueIds: [blocker.id], |
| actor, |
| }); |
|
|
| await ctx.issues.relations.setBlockedBy(child.id, [blocker.id], input.companyId, actor); |
| await ctx.issues.documents.upsert({ |
| issueId: child.id, |
| companyId: input.companyId, |
| key: "orchestration-smoke", |
| title: "Orchestration Smoke", |
| format: "markdown", |
| body: [ |
| "# Orchestration Smoke", |
| "", |
| `- Root issue: ${input.issueId}`, |
| `- Child issue: ${child.id}`, |
| `- Billing code: ${billingCode}`, |
| ].join("\n"), |
| changeSummary: "Recorded orchestration smoke output", |
| }); |
|
|
| const wakeup = await ctx.issues.requestWakeup(child.id, input.companyId, { |
| reason: "plugin:orchestration_smoke", |
| contextSource: "plugin-orchestration-smoke", |
| idempotencyKey: `${input.issueId}:child`, |
| ...actor, |
| }); |
| const orchestration = await ctx.issues.summaries.getOrchestration({ |
| issueId: input.issueId, |
| companyId: input.companyId, |
| includeSubtree: true, |
| billingCode, |
| }); |
| const summarySnapshot = { |
| childIssueId: child.id, |
| blockerIssueId: blocker.id, |
| wakeupQueued: wakeup.queued, |
| subtreeIssueIds: orchestration.subtreeIssueIds, |
| }; |
|
|
| await ctx.db.execute( |
| `INSERT INTO ${tableName(ctx.db.namespace)} (id, root_issue_id, child_issue_id, blocker_issue_id, billing_code, last_summary) |
| VALUES ($1, $2, $3, $4, $5, $6::jsonb) |
| ON CONFLICT (id) DO UPDATE SET |
| child_issue_id = EXCLUDED.child_issue_id, |
| blocker_issue_id = EXCLUDED.blocker_issue_id, |
| billing_code = EXCLUDED.billing_code, |
| last_summary = EXCLUDED.last_summary, |
| updated_at = now()`, |
| [ |
| randomUUID(), |
| input.issueId, |
| child.id, |
| blocker.id, |
| billingCode, |
| JSON.stringify(summarySnapshot), |
| ], |
| ); |
|
|
| return { |
| rootIssueId: input.issueId, |
| childIssueId: child.id, |
| blockerIssueId: blocker.id, |
| billingCode, |
| joinedRows: await ctx.db.query( |
| `SELECT s.id, s.billing_code, i.title AS root_title |
| FROM ${tableName(ctx.db.namespace)} s |
| JOIN public.issues i ON i.id = s.root_issue_id |
| WHERE s.root_issue_id = $1`, |
| [input.issueId], |
| ), |
| subtreeIssueIds: orchestration.subtreeIssueIds, |
| wakeupQueued: wakeup.queued, |
| }; |
| }; |
|
|
| ctx.data.register("surface-status", async (params) => { |
| const companyId = stringField(params.companyId); |
| const issueId = stringField(params.issueId); |
| return { |
| status: "ok", |
| checkedAt: new Date().toISOString(), |
| databaseNamespace: ctx.db.namespace, |
| routeKeys: (ctx.manifest.apiRoutes ?? []).map((route) => route.routeKey), |
| capabilities: ctx.manifest.capabilities, |
| summary: companyId && issueId ? await readSmokeSummary?.(companyId, issueId) ?? null : null, |
| }; |
| }); |
|
|
| ctx.actions.register("initialize-smoke", async (params) => { |
| const companyId = stringField(params.companyId); |
| const issueId = stringField(params.issueId); |
| if (!companyId || !issueId) throw new Error("companyId and issueId are required"); |
| if (!initializeSmoke) throw new Error("Smoke initializer is not ready"); |
| return initializeSmoke({ |
| companyId, |
| issueId, |
| assigneeAgentId: stringField(params.assigneeAgentId), |
| actorAgentId: stringField(params.actorAgentId), |
| actorUserId: stringField(params.actorUserId), |
| actorRunId: stringField(params.actorRunId), |
| }); |
| }); |
| }, |
|
|
| async onApiRequest(input: PluginApiRequestInput) { |
| if (input.routeKey === "summary") { |
| const issueId = input.params.issueId; |
| return { |
| body: await readSmokeSummary?.(input.companyId, issueId) ?? null, |
| }; |
| } |
|
|
| if (input.routeKey === "initialize") { |
| if (!initializeSmoke) throw new Error("Smoke initializer is not ready"); |
| const body = input.body as Record<string, unknown> | null; |
| return { |
| status: 201, |
| body: await initializeSmoke({ |
| companyId: input.companyId, |
| issueId: input.params.issueId, |
| assigneeAgentId: stringField(body?.assigneeAgentId), |
| actorAgentId: input.actor.agentId ?? null, |
| actorUserId: input.actor.userId ?? null, |
| actorRunId: input.actor.runId ?? null, |
| }), |
| }; |
| } |
|
|
| return { |
| status: 404, |
| body: { error: `Unknown orchestration smoke route: ${input.routeKey}` }, |
| }; |
| }, |
|
|
| async onHealth() { |
| return { |
| status: "ok", |
| message: "Orchestration smoke plugin worker is running", |
| details: { |
| surfaces: ["database", "scoped-api-route", "issue-panel", "orchestration-apis"], |
| }, |
| }; |
| } |
| }); |
|
|
| export default plugin; |
| runWorker(plugin, import.meta.url); |
|
|