From c9a4f9e2f45186efd9a1d75f155a8b57413f361d Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 2 Sep 2026 09:21:11 -0700 Subject: [PATCH 1/7] Move bench templates to templates/ and split workflow-catalog's routes (CL-7373) Templates (WorkbenchDefinition data, instantiation, and the template/* settings vocabulary) move from packages/workflow-catalog into a root templates/ package, one directory per shipped template, peer of workflows/. The GitHub connect card and repo-grant reconciliation move to @workbench/connections; the template-block deploy route (hub-only) moves into apps/hub. Every importer switches its import path; exported names are unchanged. --- apps/hub/package.json | 3 +- apps/hub/src/index.ts | 10 +- apps/hub/src/templates/block-workflows.ts | 63 +++ .../templates/template-block-routes.test.ts | 134 +++++ .../src/templates/template-block-routes.ts | 108 ++++ apps/web/package.json | 2 +- apps/web/src/connect-github-actions.ts | 2 +- apps/web/src/instant-agent-create.test.ts | 2 +- apps/web/src/instant-agent-create.ts | 2 +- apps/web/src/myra-workbench.ts | 2 +- apps/web/src/pages/create-agent-panel.tsx | 2 +- apps/web/src/workbench-templates-api.ts | 4 +- apps/web/src/workbench-templates.ts | 2 +- apps/web/test/new-workbench-picker.test.tsx | 2 +- packages/agent-directory/package.json | 2 +- packages/agent-directory/src/client.ts | 2 +- .../src/resolve-myra-definition-id.ts | 2 +- packages/agent-directory/src/validation.ts | 2 +- .../src/visible-definitions.ts | 2 +- .../src/workflow-create-routes.ts | 2 +- .../artifacts-hub/src/template-library.ts | 4 +- packages/chat-ui/package.json | 2 +- packages/chat-ui/src/api.ts | 8 +- .../src/blocks/connect-github-actions.ts | 6 +- .../chat-ui/test/connect-github-flow.test.tsx | 4 +- packages/chat/package.json | 1 - packages/chat/src/connect-pending.ts | 4 +- packages/code-review/README.md | 2 +- packages/connections/package.json | 9 +- .../connect-github-credential-link.test.ts | 210 ++++++++ .../src/connect-github-routes.test.ts | 323 ++++++++++++ .../connections/src/connect-github-routes.ts | 403 +++++++++++++++ .../src/connect-github-setup.race.test.ts | 219 ++++++++ .../src/connect-github-setup.test.ts | 248 +++++++++ .../connections/src/connect-github-setup.ts | 190 +++++++ .../reconcile-duplicate-repo-grants.test.ts | 136 +++++ .../src/reconcile-duplicate-repo-grants.ts | 55 ++ packages/echo/README.md | 2 +- packages/evals/package.json | 2 +- packages/evals/src/targets/real-target.ts | 2 +- packages/hub-client/package.json | 2 +- packages/hub-client/src/seed.ts | 4 +- packages/plugins-ui/package.json | 1 - packages/routines/README.md | 4 +- packages/routines/package.json | 2 +- packages/routines/src/client.ts | 4 +- packages/routines/src/targets.ts | 2 +- packages/settings-ui/package.json | 2 +- .../src/connections-pinned-by.test.ts | 2 +- .../settings-ui/src/connections-section.tsx | 2 +- .../settings-ui/src/granola-webhook-card.tsx | 2 +- .../webhook-triggers/src/repo-review-lease.ts | 2 +- templates/LICENSE | 176 +++++++ templates/README.md | 16 + templates/code-review/index.ts | 74 +++ templates/due-diligence/index.ts | 40 ++ templates/gtm/index.ts | 97 ++++ templates/index.ts | 472 ++++++++++++++++++ templates/instantiate.test.ts | 288 +++++++++++ templates/instantiate.ts | 203 ++++++++ templates/package.json | 29 ++ templates/participant-agent-requests.ts | 49 ++ templates/schema.test.ts | 306 ++++++++++++ templates/settings.test.ts | 16 + templates/settings.ts | 71 +++ templates/tsconfig.json | 8 + workflows/diligence-brief/README.md | 2 +- workflows/last-30-days-research/README.md | 2 +- workflows/last-30-days-research/src/index.ts | 2 +- .../src/finalize-tool.ts | 2 +- .../process-granola-call/src/finalize-tool.ts | 2 +- workflows/process-granola-call/src/index.ts | 2 +- 72 files changed, 4003 insertions(+), 63 deletions(-) create mode 100644 apps/hub/src/templates/block-workflows.ts create mode 100644 apps/hub/src/templates/template-block-routes.test.ts create mode 100644 apps/hub/src/templates/template-block-routes.ts create mode 100644 packages/connections/src/connect-github-credential-link.test.ts create mode 100644 packages/connections/src/connect-github-routes.test.ts create mode 100644 packages/connections/src/connect-github-routes.ts create mode 100644 packages/connections/src/connect-github-setup.race.test.ts create mode 100644 packages/connections/src/connect-github-setup.test.ts create mode 100644 packages/connections/src/connect-github-setup.ts create mode 100644 packages/connections/src/reconcile-duplicate-repo-grants.test.ts create mode 100644 packages/connections/src/reconcile-duplicate-repo-grants.ts create mode 100644 templates/LICENSE create mode 100644 templates/README.md create mode 100644 templates/code-review/index.ts create mode 100644 templates/due-diligence/index.ts create mode 100644 templates/gtm/index.ts create mode 100644 templates/index.ts create mode 100644 templates/instantiate.test.ts create mode 100644 templates/instantiate.ts create mode 100644 templates/package.json create mode 100644 templates/participant-agent-requests.ts create mode 100644 templates/schema.test.ts create mode 100644 templates/settings.test.ts create mode 100644 templates/settings.ts create mode 100644 templates/tsconfig.json diff --git a/apps/hub/package.json b/apps/hub/package.json index 49e9c9c77..5b5a0e650 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -47,8 +47,8 @@ "@corbits/tool-registry-publish": "workspace:*", "@corbits/turn-artifacts": "workspace:*", "@corbits/url-path": "workspace:*", + "@corbits/code-review-workflow": "workspace:*", "@corbits/webhook-triggers": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", "@corbits/workflows": "workspace:*", "@intx/authz": "0.3.0", "@intx/crypto": "0.3.0", @@ -67,6 +67,7 @@ "@workbench/echo": "workspace:*", "@workbench/hub-client": "workspace:*", "@workbench/onboarding": "workspace:*", + "@workbench/templates": "workspace:*", "arktype": "catalog:", "better-auth": "^1.4.18", "drizzle-orm": "0.45.2", diff --git a/apps/hub/src/index.ts b/apps/hub/src/index.ts index 8bdeddc0f..61a61d3a2 100644 --- a/apps/hub/src/index.ts +++ b/apps/hub/src/index.ts @@ -187,13 +187,13 @@ import { deliveryWorkbenchRequiredForWorkflowName, isConversationalWorkflowName, validateTriggerFieldsAtCreate, - webhookTriggerName, workflowCatalogEntry, workflowDisplayName, workbenchTemplateLibraryEntries, -} from "@corbits/workflow-catalog"; -import { createConnectGithubRoutes } from "@corbits/workflow-catalog/connect-github-routes"; -import { createTemplateBlockRoutes } from "@corbits/workflow-catalog/template-block-routes"; +} from "@workbench/templates"; +import { createConnectGithubRoutes } from "@workbench/connections/connect-github-routes"; +import { webhookTriggerName } from "@workbench/connections/connect-github-setup"; +import { createTemplateBlockRoutes } from "./templates/template-block-routes"; import { createWorkflowDetailRoute, renderWorkflowSourceTree, @@ -2823,7 +2823,7 @@ export async function createHub(config: HubConfig) { const routineStore = createDrizzleRoutineStore(db); // The honest end-to-end delivery-destination rule: a workflow that // never posts to a workbench (e.g. recurring-task, always delivering - // to its creator's Inbox — see @corbits/workflow-catalog's + // to its creator's Inbox — see @workbench/templates's // `deliveryMode`) must never be forced to collect, or block on // missing, a `deliveryWorkbenchId` it would just discard. An unknown // definitionId (row missing, or its asset name isn't catalog-known) diff --git a/apps/hub/src/templates/block-workflows.ts b/apps/hub/src/templates/block-workflows.ts new file mode 100644 index 000000000..04434367a --- /dev/null +++ b/apps/hub/src/templates/block-workflows.ts @@ -0,0 +1,63 @@ +// The source form a template's referenced block workflow deploys as — +// the missing half of instantiation CL-6405 closes: `./instantiate.ts` +// resolves a manifest's participants through the agent-directory create +// path, and this module resolves its `blocks` into the same +// source-form deploy (a serialized definition rendered into a +// `@corbits/workflows`'s `./source` tree and projected onto a +// `workflow_definition` row — see `./template-block-routes.ts` and the +// hub's `deployWorkflowSource` binding). +// +// Server-only, on purpose: building `code-review`'s definition pulls in +// `@corbits/code-review-workflow` and with it `@intx/agent`/`@intx/workflow` +// — the heavy graph `./templates.ts` keeps every manifest consumer off. +// Only `./template-block-routes.ts` (mounted in `apps/hub`) imports +// this; it is deliberately not re-exported from the package root. + +import { + buildCodeReviewWorkflow, + serializeCodeReviewWorkflow, + type CodeReviewWorkflowInput, +} from "@corbits/code-review-workflow"; + +import { workflowCatalogEntry } from "@workbench/templates"; + +// Matches the conversational default every folded builder in this +// codebase uses (`AGENT_DEFINITION_TURN_TIMEOUT_MS`, +// `ASSISTANT_TURN_TIMEOUT_MS`): a review turn reads a diff and posts +// one review, the same order of work as a research or assistant turn. +const BLOCK_WORKFLOW_TURN_TIMEOUT_MS = 2 * 60 * 1000; + +export interface BlockWorkflowBuildInput { + readonly tenantDomain: string; + readonly inferencePreferences: CodeReviewWorkflowInput["inferencePreferences"]; +} + +export interface BlockWorkflowSource { + readonly assetName: string; + readonly displayName: string; + readonly workflowJson: string; +} + +/** + * The serialized source-form definition for one template block, or + * `undefined` for an asset name no template block builder covers yet — + * the GTM blocks resolve through their own (future) path, exactly like + * `instantiateWorkbenchTemplate`'s participants; a route answering + * `undefined` as a 404 is the honest statement of that gap. + */ +export function buildBlockWorkflowSource( + assetName: string, + input: BlockWorkflowBuildInput, +): BlockWorkflowSource | undefined { + if (assetName !== "code-review") return undefined; + const definition = buildCodeReviewWorkflow({ + triggerAddress: `${assetName}@${input.tenantDomain}`, + inferencePreferences: input.inferencePreferences, + turnTimeoutMs: BLOCK_WORKFLOW_TURN_TIMEOUT_MS, + }); + return { + assetName, + displayName: workflowCatalogEntry(assetName)?.displayName ?? assetName, + workflowJson: serializeCodeReviewWorkflow(definition), + }; +} diff --git a/apps/hub/src/templates/template-block-routes.test.ts b/apps/hub/src/templates/template-block-routes.test.ts new file mode 100644 index 000000000..f6488c0d7 --- /dev/null +++ b/apps/hub/src/templates/template-block-routes.test.ts @@ -0,0 +1,134 @@ +// Exercises `createTemplateBlockRoutes`' HTTP surface through the real +// `buildBlockWorkflowSource` builder, mounted the same way +// `./connect-github-routes.test.ts` mounts its routes: a bare `Hono` +// with a tenant-injecting middleware, every port a plain fake. +import { describe, expect, test } from "bun:test"; +import { Hono } from "hono"; +import type { MiddlewareHandler } from "hono"; +import type { RequireGrant, TenantEnv } from "@intx/hub-api"; + +import { + createTemplateBlockRoutes, + type TemplateBlockRoutesDeps, +} from "./template-block-routes"; + +const TENANT = { + id: "tnt_1", + name: "Acme", + slug: "acme", + domain: "acme.example", + parentId: null, + config: null, + createdAt: new Date(), + updatedAt: new Date(), +}; + +const PRINCIPAL = { + id: "prn_alice", + tenantId: TENANT.id, + kind: "user" as const, + refId: "prn_alice", + status: "active" as const, + createdAt: new Date(), + updatedAt: new Date(), +}; + +const allowAll: RequireGrant = () => async (_c, next) => { + await next(); +}; + +function mountAs(routes: Hono): Hono { + const asTenant: MiddlewareHandler = async (c, next) => { + c.set("tenant", TENANT); + c.set("principal", PRINCIPAL); + await next(); + }; + const app = new Hono(); + app.use("*", asTenant); + app.route("/", routes); + return app; +} + +type DeployedSource = Parameters< + TemplateBlockRoutesDeps["deployWorkflowSource"] +>[0]; + +function buildApp(overrides: Partial = {}) { + const deployed: DeployedSource[] = []; + const deps: TemplateBlockRoutesDeps = { + requireGrant: allowAll, + log: () => {}, + inferencePreferences: async () => [ + { provider: "anthropic", model: "claude-sonnet-5" }, + ], + deployWorkflowSource: async (args) => { + deployed.push(args); + return { id: "wfd_code_review", created: true }; + }, + ...overrides, + }; + return { app: mountAs(createTemplateBlockRoutes(deps)), deployed }; +} + +describe("POST /:assetName/deploy", () => { + test("deploys the code-review block as a source-form definition carrying the github tool pin", async () => { + const { app, deployed } = buildApp(); + const res = await app.request("/code-review/deploy", { method: "POST" }); + expect(res.status).toBe(201); + expect(await res.json()).toEqual({ id: "wfd_code_review", created: true }); + + expect(deployed).toHaveLength(1); + const source = deployed[0]; + if (source === undefined) throw new Error("nothing deployed"); + expect(source.tenantId).toBe(TENANT.id); + expect(source.principalId).toBe(PRINCIPAL.id); + expect(source.assetName).toBe("code-review"); + expect(source.displayName).toBe("Code review"); + + const definition = JSON.parse(source.workflowJson) as { + triggers: { type: string; to: string }[]; + steps: Record< + string, + { agent: { toolPackagePins?: { name: string }[] } } + >; + }; + expect(definition.triggers).toEqual([ + { type: "mail", to: "code-review@acme.example" }, + ]); + const pins = Object.values(definition.steps).flatMap( + (step) => step.agent.toolPackagePins ?? [], + ); + expect(pins.map((pin) => pin.name)).toContain("@corbits/github-tools"); + }); + + test("an already-deployed block answers 200 with created: false", async () => { + const { app } = buildApp({ + deployWorkflowSource: async () => ({ + id: "wfd_existing", + created: false, + }), + }); + const res = await app.request("/code-review/deploy", { method: "POST" }); + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ id: "wfd_existing", created: false }); + }); + + test("an asset name no block builder covers answers 404, deploying nothing", async () => { + const { app, deployed } = buildApp(); + const res = await app.request("/granola-call/deploy", { method: "POST" }); + expect(res.status).toBe(404); + expect(deployed).toHaveLength(0); + }); + + test("a failing deploy port answers a 500 envelope, never a raw error", async () => { + const { app } = buildApp({ + deployWorkflowSource: async () => { + throw new Error("disk full"); + }, + }); + const res = await app.request("/code-review/deploy", { method: "POST" }); + expect(res.status).toBe(500); + const body = (await res.json()) as { error?: { userMessage?: string } }; + expect(JSON.stringify(body)).not.toContain("disk full"); + }); +}); diff --git a/apps/hub/src/templates/template-block-routes.ts b/apps/hub/src/templates/template-block-routes.ts new file mode 100644 index 000000000..a07a9f4aa --- /dev/null +++ b/apps/hub/src/templates/template-block-routes.ts @@ -0,0 +1,108 @@ +// The instantiate path's block-workflow deploy surface (CL-6405): +// `instantiateWorkbenchTemplate`'s `deployBlockWorkflow` port binds to +// `POST /:assetName/deploy` here, the way its `createParticipantAgent` +// port binds to `POST /agent-definitions`. Mounted per-tenant inside +// the platform's native tenant middleware, mirroring +// `./connect-github-routes.ts`: every side effect a host needs — the +// tenant's default inference preferences, and the actual source-form +// asset write + `workflow_definition` projection — arrives as an +// injected port, so `apps/hub` is the only place drizzle and the +// `AssetService` are touched and this stays testable with plain fakes. +import { Hono } from "hono"; +import type { RequireGrant, TenantEnv } from "@intx/hub-api"; +import { makeErrorEnvelope } from "@workbench/hub-client"; + +import { + buildBlockWorkflowSource, + type BlockWorkflowBuildInput, +} from "./block-workflows"; + +const DEPLOY_FAILED_MESSAGE = + "Couldn't set up this template's workflow. Try again in a moment."; + +export type TemplateBlockRoutesDeps = { + requireGrant: RequireGrant; + /** Where a failure's real cause goes — the same CL-6360 idiom + * `./connect-github-routes.ts` documents: the client sees one honest + * `userMessage`, the raw detail lands here. */ + log: (line: string) => void; + /** The tenant's default inference preferences — the same resolution a + * fresh workbench host launches against (`@corbits/chat`'s + * `createWorkbenchHostInferencePreferencesResolver`), so a deployed + * block runs on the model the bench actually connected. */ + inferencePreferences( + tenantId: string, + ): Promise; + /** The source-form deploy itself: renders `workflowJson` into a + * `@corbits/workflows`'s `./source` tree on a `workflow`-kind asset and + * projects it onto a `workflow_definition` row — the exact + * materialization `createAgentDefinitionCore` runs for a participant + * agent, minus its agent-only prompt/skills machinery. `created` is + * `false` when the tenant already carries a deployed definition under + * `assetName` (the port skips instead of double-deploying). */ + deployWorkflowSource(args: { + readonly tenantId: string; + readonly principalId: string; + readonly assetName: string; + readonly displayName: string; + readonly workflowJson: string; + }): Promise<{ readonly id: string; readonly created: boolean }>; +}; + +export function createTemplateBlockRoutes( + deps: TemplateBlockRoutesDeps, +): Hono { + const app = new Hono(); + + app.post( + "/:assetName/deploy", + deps.requireGrant("workflow:*", "create"), + async (c) => { + const tenant = c.get("tenant"); + const principal = c.get("principal"); + const assetName = c.req.param("assetName"); + + const source = buildBlockWorkflowSource(assetName, { + tenantDomain: tenant.domain, + inferencePreferences: await deps.inferencePreferences(tenant.id), + }); + if (source === undefined) { + return c.json( + makeErrorEnvelope({ + code: "not_found", + userMessage: `"${assetName}" isn't a deployable template workflow.`, + }), + 404, + ); + } + + try { + const result = await deps.deployWorkflowSource({ + tenantId: tenant.id, + principalId: principal.id, + assetName: source.assetName, + displayName: source.displayName, + workflowJson: source.workflowJson, + }); + return c.json( + { id: result.id, created: result.created }, + result.created ? 201 : 200, + ); + } catch (cause) { + const message = cause instanceof Error ? cause.message : String(cause); + deps.log( + `template-blocks: deploying "${assetName}" failed for tenant ${tenant.id}: ${message}`, + ); + return c.json( + makeErrorEnvelope({ + code: "deploy_failed", + userMessage: DEPLOY_FAILED_MESSAGE, + }), + 500, + ); + } + }, + ); + + return app; +} diff --git a/apps/web/package.json b/apps/web/package.json index e2467efb8..0d2a94a51 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -41,7 +41,6 @@ "@corbits/slug": "workspace:*", "@corbits/text-diff": "workspace:*", "@corbits/url-path": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", "@corbits/workflows": "workspace:*", "@corbits/icons": "workspace:*", "@intx/types": "workspace:*", @@ -50,6 +49,7 @@ "@tanstack/react-query": "catalog:", "@workbench/connections": "workspace:*", "@workbench/hub-client": "workspace:*", + "@workbench/templates": "workspace:*", "arktype": "catalog:", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/apps/web/src/connect-github-actions.ts b/apps/web/src/connect-github-actions.ts index 95e87d2c2..71a2a0bb0 100644 --- a/apps/web/src/connect-github-actions.ts +++ b/apps/web/src/connect-github-actions.ts @@ -10,7 +10,7 @@ // (`connect-github-block.tsx`'s `DisconnectedBody`) is what actually asks // for the token, so this binding never needs a prompt or a modal of its // own. Reading state and starting reviews call the workbench-scoped routes -// `@corbits/workflow-catalog`'s `createConnectGithubRoutes` exposes. +// `@workbench/connections`'s `createConnectGithubRoutes` exposes. // // `subscribeConnectState` has no server push to fold from yet (connecting a // credential publishes no `chat.settings` event of its own — only diff --git a/apps/web/src/instant-agent-create.test.ts b/apps/web/src/instant-agent-create.test.ts index 7e6221a9c..8d2044478 100644 --- a/apps/web/src/instant-agent-create.test.ts +++ b/apps/web/src/instant-agent-create.test.ts @@ -3,7 +3,7 @@ import { QueryClient } from "@tanstack/react-query"; import { CODE_REVIEW_TEMPLATE, serializeWorkbenchDefinition, -} from "@corbits/workflow-catalog"; +} from "@workbench/templates"; import { createWorkbenchFromTemplate, diff --git a/apps/web/src/instant-agent-create.ts b/apps/web/src/instant-agent-create.ts index 53ad80639..c6152d07f 100644 --- a/apps/web/src/instant-agent-create.ts +++ b/apps/web/src/instant-agent-create.ts @@ -25,7 +25,7 @@ import { templateSettingsPatch, type WorkbenchDefinition, type WorkbenchOnboardingStep, -} from "@corbits/workflow-catalog"; +} from "@workbench/templates"; import { deployWorkbenchTemplateBlock, diff --git a/apps/web/src/myra-workbench.ts b/apps/web/src/myra-workbench.ts index cdd17f711..1a109706e 100644 --- a/apps/web/src/myra-workbench.ts +++ b/apps/web/src/myra-workbench.ts @@ -10,7 +10,7 @@ import { isWorkbenchTitleMatch, type Workbench, } from "@corbits/chat-ui"; -import { WORKFLOW_CATALOG } from "@corbits/workflow-catalog"; +import { WORKFLOW_CATALOG } from "@workbench/templates"; import { listAgentDefinitions, type AgentDefinition } from "./agents-api"; diff --git a/apps/web/src/pages/create-agent-panel.tsx b/apps/web/src/pages/create-agent-panel.tsx index f1814a7a3..0c54160c6 100644 --- a/apps/web/src/pages/create-agent-panel.tsx +++ b/apps/web/src/pages/create-agent-panel.tsx @@ -169,7 +169,7 @@ type Suggestion = { }; /** Every card here names something this workbench can actually deploy - * today (`@corbits/workflow-catalog`'s "Morning brief", "Granola call + * today (`@corbits/workflows`'s "Morning brief", "Granola call * notes", and general conversational research) — plain copy, no * capability this platform can't back up yet. */ const SUGGESTIONS: readonly Suggestion[] = [ diff --git a/apps/web/src/workbench-templates-api.ts b/apps/web/src/workbench-templates-api.ts index dc7abceb7..92f3156bf 100644 --- a/apps/web/src/workbench-templates-api.ts +++ b/apps/web/src/workbench-templates-api.ts @@ -1,6 +1,6 @@ // Reads a workbench template's manifest from the bench library (CL-6344) // — `instant-agent-create.ts` instantiates from this seeded row, never -// from a hardcoded `@corbits/workflow-catalog` import. The read itself +// from a hardcoded `@workbench/templates` import. The read itself // is what converges the shelf (CL-6458), so these routes answer for a // bench of any age. The wire shape is the library's `{id, content}` // entry; the content string re-enters through the catalog's own @@ -11,7 +11,7 @@ import { type } from "arktype"; import { parseWorkbenchDefinition, type WorkbenchDefinition, -} from "@corbits/workflow-catalog"; +} from "@workbench/templates"; import { ApiQueryError } from "@corbits/api-query"; const TemplateLibraryEntry = type({ id: "string > 0", content: "string > 0" }); diff --git a/apps/web/src/workbench-templates.ts b/apps/web/src/workbench-templates.ts index 0e07225bc..ab3a45499 100644 --- a/apps/web/src/workbench-templates.ts +++ b/apps/web/src/workbench-templates.ts @@ -1,7 +1,7 @@ // The picker's prefab catalog: one entry per one-click shortcut card // (CL-6628). `code-review` and `blank` are pinned to the approved mock // (CL-6342) verbatim; `due-diligence` mirrors the backend's -// `DUE_DILIGENCE_TEMPLATE` (`@corbits/workflow-catalog`, CL-6499) — it and +// `DUE_DILIGENCE_TEMPLATE` (`@workbench/templates`, CL-6499) — it and // every other id here are still gated by what this bench's library // actually serves before either is offered as a live card (see // `NewWorkbenchPickerRoute`'s `servedTemplateIds`). See diff --git a/apps/web/test/new-workbench-picker.test.tsx b/apps/web/test/new-workbench-picker.test.tsx index 00345ef4b..c510238ad 100644 --- a/apps/web/test/new-workbench-picker.test.tsx +++ b/apps/web/test/new-workbench-picker.test.tsx @@ -12,7 +12,7 @@ import { CODE_REVIEW_TEMPLATE, DUE_DILIGENCE_TEMPLATE, serializeWorkbenchDefinition, -} from "@corbits/workflow-catalog"; +} from "@workbench/templates"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; diff --git a/packages/agent-directory/package.json b/packages/agent-directory/package.json index 71f9e23da..2f4c4b657 100644 --- a/packages/agent-directory/package.json +++ b/packages/agent-directory/package.json @@ -18,7 +18,7 @@ "@corbits/error-sink": "workspace:*", "@corbits/folded-run-one-shot": "workspace:*", "@corbits/skills": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", + "@workbench/templates": "workspace:*", "@corbits/workflows": "workspace:*", "@intx/agent": "workspace:*", "@intx/db": "workspace:*", diff --git a/packages/agent-directory/src/client.ts b/packages/agent-directory/src/client.ts index 44bbc1623..57bb62040 100644 --- a/packages/agent-directory/src/client.ts +++ b/packages/agent-directory/src/client.ts @@ -15,7 +15,7 @@ import { withDisplayNames, type WithDisplayName, } from "@corbits/chat/display-name"; -import { isConversationalWorkflowName } from "@corbits/workflow-catalog"; +import { isConversationalWorkflowName } from "@workbench/templates"; // `deriveDisplayName`/`humanizeSlug` (CL-6413) live in `@corbits/chat` // itself, not here: this package already depends on `@corbits/chat` (for diff --git a/packages/agent-directory/src/resolve-myra-definition-id.ts b/packages/agent-directory/src/resolve-myra-definition-id.ts index 5bedb72a7..cc7dde244 100644 --- a/packages/agent-directory/src/resolve-myra-definition-id.ts +++ b/packages/agent-directory/src/resolve-myra-definition-id.ts @@ -7,7 +7,7 @@ import { and, eq } from "drizzle-orm"; import type { DB } from "@intx/db"; import { workflowDefinition } from "@intx/db/schema"; -import { WORKFLOW_CATALOG } from "@corbits/workflow-catalog"; +import { WORKFLOW_CATALOG } from "@workbench/templates"; /** Myra's asset name in the seeded workflow catalog — the same lookup * `apps/web/src/myra-workbench.ts` does client-side, mirrored here for diff --git a/packages/agent-directory/src/validation.ts b/packages/agent-directory/src/validation.ts index d4c71c507..3571f1109 100644 --- a/packages/agent-directory/src/validation.ts +++ b/packages/agent-directory/src/validation.ts @@ -68,7 +68,7 @@ export const CreateAgentDefinitionInput = type({ // core resolves each to `*`, matching `./workflow-create-routes.ts`'s // own handling of the same field). Absent for a person hand-authoring // an agent through a form, which has no affordance for typing one; the - // one caller that supplies it is `@corbits/workflow-catalog`'s + // one caller that supplies it is `@workbench/templates`'s // `instantiateWorkbenchTemplate`, installing a template participant // (Scout, Jimmy) whose tools ship as pinned packages rather than // inline capabilities. diff --git a/packages/agent-directory/src/visible-definitions.ts b/packages/agent-directory/src/visible-definitions.ts index 8f5d27d63..08c01996c 100644 --- a/packages/agent-directory/src/visible-definitions.ts +++ b/packages/agent-directory/src/visible-definitions.ts @@ -11,7 +11,7 @@ import { and, eq } from "drizzle-orm"; import type { DB } from "@intx/db"; import { getAncestorChain, schema } from "@intx/db"; import { isWorkbenchHostDefinitionName } from "@corbits/chat/workbench-host-naming"; -import { isConversationalWorkflowName } from "@corbits/workflow-catalog"; +import { isConversationalWorkflowName } from "@workbench/templates"; import { deriveDisplayName } from "./client"; export type VisibleAgentDefinition = { diff --git a/packages/agent-directory/src/workflow-create-routes.ts b/packages/agent-directory/src/workflow-create-routes.ts index b5079a6b2..4fa886c18 100644 --- a/packages/agent-directory/src/workflow-create-routes.ts +++ b/packages/agent-directory/src/workflow-create-routes.ts @@ -41,7 +41,7 @@ import type { AssetService } from "@intx/hub-sessions"; import { skillNameSchema } from "@corbits/skills"; import { isWorkbenchHostDefinitionName } from "@corbits/chat/workbench-host-naming"; -import { isAutomatableWorkflowName } from "@corbits/workflow-catalog"; +import { isAutomatableWorkflowName } from "@workbench/templates"; import { createAgentDefinitionCore, diff --git a/packages/artifacts-hub/src/template-library.ts b/packages/artifacts-hub/src/template-library.ts index 1b2f604e4..80aca8d01 100644 --- a/packages/artifacts-hub/src/template-library.ts +++ b/packages/artifacts-hub/src/template-library.ts @@ -47,7 +47,7 @@ export const WORKBENCH_TEMPLATE_ARTIFACT_KIND = "workbench-template"; /** One shelf entry to seed: the template's stable id and its serialized * manifest. Kept as plain strings so this package never depends on - * `@corbits/workflow-catalog` — the manifest vocabulary stays there. */ + * `@workbench/templates` — the manifest vocabulary stays there. */ export type TemplateLibraryEntry = { readonly id: string; readonly content: string; @@ -427,7 +427,7 @@ export type CreateTemplateLibraryRoutesDeps = { /** `GET /` lists every seeded template entry; `GET /:templateId` fetches * one. Both converge the tenant's shelf before reading it. Content * travels as the seeded string — the client parses it with - * `@corbits/workflow-catalog`'s manifest schema at its own boundary. */ + * `@workbench/templates`'s manifest schema at its own boundary. */ export function createTemplateLibraryRoutes( deps: CreateTemplateLibraryRoutesDeps, ): Hono { diff --git a/packages/chat-ui/package.json b/packages/chat-ui/package.json index 7fbad1f90..2df47e712 100644 --- a/packages/chat-ui/package.json +++ b/packages/chat-ui/package.json @@ -25,7 +25,7 @@ "@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635", "@tanstack/react-query": "catalog:", "@workbench/connections": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", + "@workbench/templates": "workspace:*", "arktype": "catalog:", "@corbits/icons": "workspace:*", "react": "^19.2.0", diff --git a/packages/chat-ui/src/api.ts b/packages/chat-ui/src/api.ts index 60c239d22..db2e1a40a 100644 --- a/packages/chat-ui/src/api.ts +++ b/packages/chat-ui/src/api.ts @@ -15,7 +15,7 @@ import { parseParticipants } from "@corbits/chat/participants"; import type { ParticipantRecord } from "@corbits/chat/participants"; import type { WorkbenchOnboardingStep } from "@corbits/chat/blocks"; import { UnauthenticatedError } from "@corbits/api-query"; -import { jimmyAgentRequest } from "@corbits/workflow-catalog"; +import { jimmyAgentRequest } from "@workbench/templates"; import { CHAT_STRINGS } from "./strings"; export { @@ -742,7 +742,7 @@ export function postWorkbenchOnboardingStep( ); } -// Jimmy's own request shape, the same `@corbits/workflow-catalog` object a +// Jimmy's own request shape, the same `@workbench/templates` object a // workbench template's participant create used to resolve — CL-6499 removed // Jimmy's template (he is not a "kind of workbench"), so this dialog's own // "Add Jimmy" quick-create row (see `invite-agent-dialog.tsx`) is his only @@ -1203,7 +1203,7 @@ export type WorkbenchSettingsPatch = { readonly "chat/contextWindow"?: number | null; /** * `template/*` keys: the room's own record of which template minted - * it and what it still needs connected, per `@corbits/workflow-catalog`'s + * it and what it still needs connected, per `@workbench/templates`'s * own schema for this namespace. `chat`'s settings route validates * only its own `chat/*` keys and passes any other namespace through * opaquely (see `packages/chat/src/workbench-settings.ts`) — a @@ -1228,7 +1228,7 @@ export function patchWorkbenchSettings( } // The room's GitHub connect card (CL-6344): the live read and the -// start-reviewing write, backed by `@corbits/workflow-catalog`'s +// start-reviewing write, backed by `@workbench/templates`'s // `createConnectGithubRoutes` (mounted at // `/api/tenants/:tenantId/workbenches/:workbenchId/github/*`). Connecting // the PAT itself is a separate, already-generic surface — diff --git a/packages/chat-ui/src/blocks/connect-github-actions.ts b/packages/chat-ui/src/blocks/connect-github-actions.ts index 5c10ff775..07fa3e518 100644 --- a/packages/chat-ui/src/blocks/connect-github-actions.ts +++ b/packages/chat-ui/src/blocks/connect-github-actions.ts @@ -6,7 +6,7 @@ // `@workbench/connections`' generic `/:connectorId/complete` route // (`github`'s PAT test-and-store — already fully generic, no bespoke // GitHub route needed), `@corbits/github-tools`' `listRepos`, and -// `@corbits/workflow-catalog`'s `startReviewingRepos`. +// `@workbench/connections`'s `startReviewingRepos`. import type { ConnectGithubRepo } from "./connect-github-block"; export type { ConnectGithubRepo }; @@ -34,7 +34,7 @@ export type ConnectGithubActions = { * `startReviewing`, `skip`) change something, re-reading * `getConnectState`. A credential completed elsewhere (the Plugins * page, another tab) settles this connector's entry on - * `@corbits/workflow-catalog`'s `template/pendingConnections` + * `@workbench/templates`'s `template/pendingConnections` * (CL-6463's `settleConnectedService`) so the *next* fresh * `getConnectState` — e.g. on this card's next mount — already reads * connected, since that read resolves against the real credential, @@ -72,7 +72,7 @@ export type ConnectGithubActions = { { readonly ok: true } | { readonly ok: false; readonly message: string } >; /** Mints a grant and a live webhook trigger per repo id, then - * records the selection — `@corbits/workflow-catalog`'s + * records the selection — `@workbench/templates`'s * `startReviewingRepos`, called through the host's own binding. */ readonly startReviewing: ( repoIds: readonly string[], diff --git a/packages/chat-ui/test/connect-github-flow.test.tsx b/packages/chat-ui/test/connect-github-flow.test.tsx index d9f6c836e..b7c9ccf8e 100644 --- a/packages/chat-ui/test/connect-github-flow.test.tsx +++ b/packages/chat-ui/test/connect-github-flow.test.tsx @@ -4,7 +4,7 @@ // `WorkbenchTimeline` (the real registry, the real container, the real // presentational `ConnectGithubBlockView`), drives it through connect → // list repos → pick three → "Start reviewing", and proves: -// 1. `@corbits/workflow-catalog`'s real `startReviewingRepos` mints +// 1. `@workbench/connections`'s real `startReviewingRepos` mints // one grant and one webhook trigger per selected repo, and records // the selection. // 2. The card settles into its connected state by the host fanning an @@ -18,7 +18,7 @@ import type { Root } from "react-dom/client"; import { startReviewingRepos, type ConnectGithubSetupPorts, -} from "@corbits/workflow-catalog"; +} from "@workbench/connections/connect-github-setup"; import type { MessageItem } from "../src/api"; import type { diff --git a/packages/chat/package.json b/packages/chat/package.json index ea4636823..3abb0a78e 100644 --- a/packages/chat/package.json +++ b/packages/chat/package.json @@ -35,7 +35,6 @@ "@corbits/folded-runs": "workspace:*", "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "@corbits/turn-artifacts": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", "@corbits/workflows": "workspace:*", "@intx/agent": "workspace:*", "@intx/authz": "0.3.0", diff --git a/packages/chat/src/connect-pending.ts b/packages/chat/src/connect-pending.ts index 2ac18d66b..ac75c5f88 100644 --- a/packages/chat/src/connect-pending.ts +++ b/packages/chat/src/connect-pending.ts @@ -9,7 +9,7 @@ // as the connecting person. // // The code-review template's own GitHub connect card registers -// under a second, template-owned key (`@corbits/workflow-catalog`'s +// under a second, template-owned key (`@workbench/templates`'s // `template/pendingConnections`) instead of `connections/pending` — a // credential completed anywhere other than that card's own submit (the // Plugins page, another tab) never reached it. Rather than stand up a @@ -44,7 +44,7 @@ export const CONNECTIONS_PENDING_KEY = "connections/pending"; export const CONNECTION_CONNECTED_EVENT = "connection.connected"; /** The code-review template's own pending-connections key - * (`@corbits/workflow-catalog`'s `templateSettingsPatch`/ + * (`@workbench/templates`'s `templateSettingsPatch`/ * `templateReposSettingsPatch`) — a room minted from that template * tracks its GitHub card's pending state here instead of under * `CONNECTIONS_PENDING_KEY`. `settleConnectedService` knows this one diff --git a/packages/code-review/README.md b/packages/code-review/README.md index 6045a1be3..ceac49359 100644 --- a/packages/code-review/README.md +++ b/packages/code-review/README.md @@ -56,7 +56,7 @@ subpaths (`@corbits/code-review/reviewers`, `@corbits/code-review/agent-requests`), separate from the package root. Both files have zero imports of their own; the root also re-exports the review run and GitHub client, which pull in `@corbits/github-tools` and -`@intx/agent`'s full provider surface. `@corbits/workflow-catalog`'s +`@intx/agent`'s full provider surface. `@workbench/templates`'s `CODE_REVIEW_TEMPLATE` (CL-6344) imports the roster through these subpaths for exactly that reason — see its own module doc. diff --git a/packages/connections/package.json b/packages/connections/package.json index 188faf99b..8992d011a 100644 --- a/packages/connections/package.json +++ b/packages/connections/package.json @@ -11,15 +11,20 @@ "./persist-credential": "./src/persist-credential.ts", "./plugins": "./src/plugins.ts", "./provider-health": "./src/provider-health.ts", - "./mcp-presets": "./src/mcp-presets.ts" + "./mcp-presets": "./src/mcp-presets.ts", + "./connect-github-routes": "./src/connect-github-routes.ts", + "./connect-github-setup": "./src/connect-github-setup.ts", + "./reconcile-duplicate-repo-grants": "./src/reconcile-duplicate-repo-grants.ts" }, "scripts": { "typecheck": "tsc --noEmit", "test": "bun test" }, "dependencies": { + "@corbits/code-review": "workspace:*", "@corbits/credential-providers": "workspace:*", "@corbits/error-sink": "workspace:*", + "@corbits/github-tools": "workspace:*", "@corbits/inference-settings": "workspace:*", "@corbits/mcp-tools": "workspace:*", "@intx/crypto": "0.3.0", @@ -28,9 +33,11 @@ "@intx/types": "workspace:*", "@modelcontextprotocol/sdk": "catalog:", "@workbench/hub-client": "workspace:*", + "@workbench/templates": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", "hono": "^4.11.9", + "postgres": "catalog:", "simple-icons": "^16.28.0" }, "devDependencies": { diff --git a/packages/connections/src/connect-github-credential-link.test.ts b/packages/connections/src/connect-github-credential-link.test.ts new file mode 100644 index 000000000..c0b5b311b --- /dev/null +++ b/packages/connections/src/connect-github-credential-link.test.ts @@ -0,0 +1,210 @@ +// CL-6463: the room's GitHub connect card submits a PAT through +// `@workbench/connections`' generic `persistConnectorCredential` (the +// exact sequence `POST /:connectorId/complete` runs) and reads its own +// connected state back through `createConnectGithubRoutes`' injected +// `resolveGithubConfig`. A host binds both against one real credential +// store, keyed by the `github` connector's own `CONNECTOR_REGISTRY` +// `displayName` (`apps/hub/src/index.ts`) -- never a second, hardcoded +// name literal that can drift out of step with it. This suite proves that +// contract with the real `persistConnectorCredential` and +// `createConnectGithubRoutes` functions, faking only the database layer +// underneath them: no real network, no real database. +import { describe, expect, test } from "bun:test"; +import { Hono } from "hono"; +import type { MiddlewareHandler } from "hono"; +import type { RequireGrant, TenantEnv } from "@intx/hub-api"; +import type { ApiCall, EnsureCredentialArgs } from "@workbench/hub-client"; +import { + CONNECTOR_REGISTRY, + persistConnectorCredential, +} from "@workbench/connections"; + +import { createConnectGithubRoutes } from "./connect-github-routes"; + +const TENANT = { + id: "tnt_1", + name: "Acme", + slug: "acme", + domain: "acme.example", + parentId: null, + config: null, + createdAt: new Date(), + updatedAt: new Date(), +}; + +const PRINCIPAL = { + id: "prn_alice", + tenantId: TENANT.id, + kind: "user" as const, + refId: "prn_alice", + status: "active" as const, + createdAt: new Date(), + updatedAt: new Date(), +}; + +const allowAll: RequireGrant = () => async (_c, next) => { + await next(); +}; + +function mountAs(routes: Hono): Hono { + const asTenant: MiddlewareHandler = async (c, next) => { + c.set("tenant", TENANT); + c.set("principal", PRINCIPAL); + await next(); + }; + const app = new Hono(); + app.use("*", asTenant); + app.route("/", routes); + return app; +} + +const fakeApi: ApiCall = async () => ({ + status: 200, + data: undefined, + cookies: [], +}); + +/** A minimal stand-in for the `credential` table: one row per name, the + * exact shape `resolveCredentialByName` reads and `ensureCredential` + * writes -- keyed by whatever name the caller passes, never assumed. */ +function createFakeCredentialStore() { + const rowsByName = new Map(); + let nextId = 1; + const ensureCredentialFn = async ( + _api: ApiCall, + _cookies: string[], + args: EnsureCredentialArgs, + ) => { + const id = `crd_${String(nextId)}`; + nextId += 1; + rowsByName.set(args.name, { id, secret: args.secret }); + return id; + }; + return { + ensureCredentialFn, + resolveByName: (name: string) => rowsByName.get(name), + }; +} + +/** Builds the `resolveGithubConfig` port the exact way + * `apps/hub/src/index.ts` does: read the credential row named by the + * `github` connector's own `displayName`, never a second literal. */ +function buildResolveGithubConfig( + store: ReturnType, + credentialName: string, +) { + return async (_tenantId: string) => { + const row = store.resolveByName(credentialName); + return row === undefined ? undefined : { apiKey: row.secret }; + }; +} + +/** `CONNECTOR_REGISTRY["github"]` is a fixed registry entry that always + * exists; a missing one is a broken test fixture, not a case to assert + * away with a non-null assertion. */ +function requireGithubDescriptor() { + const descriptor = CONNECTOR_REGISTRY["github"]; + if (descriptor === undefined) { + throw new Error('CONNECTOR_REGISTRY has no "github" entry'); + } + return descriptor; +} + +describe("the room GitHub connect card reads what its own submit writes", () => { + test("a PAT submitted through the connections' complete sequence is reported connected by the card's own state route", async () => { + const store = createFakeCredentialStore(); + const githubDescriptor = requireGithubDescriptor(); + + // The card's submit path: `POST /connections/github/complete` runs + // this exact sequence once the PAT probe passes. + await persistConnectorCredential({ + api: fakeApi, + cookies: [], + tenantId: TENANT.id, + descriptor: githubDescriptor, + secret: "ghp_test_token", + log: () => {}, + ensureProviderFn: async () => "prv_github", + ensureCredentialFn: store.ensureCredentialFn, + }); + + // The card's own read path, wired the way `apps/hub/src/index.ts` + // wires it: keyed by `githubDescriptor.displayName`, the same field + // `persistConnectorCredential` names the row with above. + const routes = createConnectGithubRoutes({ + requireGrant: allowAll, + log: () => {}, + resolveGithubConfig: buildResolveGithubConfig( + store, + githubDescriptor.displayName, + ), + resolveCodeReviewDefinitionId: async () => "wfd_code_review", + acquireRepoReviewLease: async () => true, + releaseRepoReviewLease: async () => {}, + hasRepoGrant: async () => false, + mintRepoGrant: async () => {}, + createWebhookTrigger: async () => ({ id: "trg_1" }), + hasWebhookTrigger: async () => false, + getTemplateSettings: async () => ({ + pendingConnections: [], + selectedRepos: [], + }), + persistSelectedRepos: async () => {}, + onReviewingStarted: async () => {}, + listReposFn: async () => [], + fetchAuthenticatedLoginFn: async () => "octocat", + }); + const app = mountAs(routes); + + const response = await app.request("/w_1/github/state"); + expect(response.status).toBe(200); + const body = (await response.json()) as { kind: string }; + expect(body.kind).toBe("connected"); + }); + + test("reading by any name other than the connector's own displayName silently leaves the card stuck disconnected -- this is the drift the two stores must never fall into", async () => { + const store = createFakeCredentialStore(); + const githubDescriptor = requireGithubDescriptor(); + + await persistConnectorCredential({ + api: fakeApi, + cookies: [], + tenantId: TENANT.id, + descriptor: githubDescriptor, + secret: "ghp_test_token", + log: () => {}, + ensureProviderFn: async () => "prv_github", + ensureCredentialFn: store.ensureCredentialFn, + }); + + // A second, independently-typed name -- e.g. the connector's `id` + // instead of its `displayName` -- is exactly the kind of hardcoded + // literal CL-6463 removed from `apps/hub/src/index.ts`. Reading by it + // instead of the descriptor's own field reproduces the stuck card. + const routes = createConnectGithubRoutes({ + requireGrant: allowAll, + log: () => {}, + resolveGithubConfig: buildResolveGithubConfig(store, githubDescriptor.id), + resolveCodeReviewDefinitionId: async () => "wfd_code_review", + acquireRepoReviewLease: async () => true, + releaseRepoReviewLease: async () => {}, + hasRepoGrant: async () => false, + mintRepoGrant: async () => {}, + createWebhookTrigger: async () => ({ id: "trg_1" }), + hasWebhookTrigger: async () => false, + getTemplateSettings: async () => ({ + pendingConnections: [], + selectedRepos: [], + }), + persistSelectedRepos: async () => {}, + onReviewingStarted: async () => {}, + listReposFn: async () => [], + fetchAuthenticatedLoginFn: async () => "octocat", + }); + const app = mountAs(routes); + + const response = await app.request("/w_1/github/state"); + const body = (await response.json()) as { kind: string }; + expect(body.kind).toBe("disconnected"); + }); +}); diff --git a/packages/connections/src/connect-github-routes.test.ts b/packages/connections/src/connect-github-routes.test.ts new file mode 100644 index 000000000..fbe4debb0 --- /dev/null +++ b/packages/connections/src/connect-github-routes.test.ts @@ -0,0 +1,323 @@ +// Exercises `createConnectGithubRoutes`' HTTP surface end to end through +// the real `startReviewingRepos` orchestration (`./connect-github-setup`), +// mounted the same way `packages/connections/src/routes.test.ts` mounts +// its own routes: a bare `Hono` with a tenant-injecting middleware, no +// real network, no real database — every port is a plain fake. +import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"; +import { Hono } from "hono"; +import type { MiddlewareHandler } from "hono"; +import type { RequireGrant, TenantEnv } from "@intx/hub-api"; +import * as errorSink from "@corbits/error-sink"; +import type { + GitHubClientConfig, + GitHubRepoSummary, +} from "@corbits/github-tools"; + +import { + createConnectGithubRoutes, + type ConnectGithubRoutesDeps, +} from "./connect-github-routes"; + +const TENANT = { + id: "tnt_1", + name: "Acme", + slug: "acme", + domain: "acme.example", + parentId: null, + config: null, + createdAt: new Date(), + updatedAt: new Date(), +}; + +const PRINCIPAL = { + id: "prn_alice", + tenantId: TENANT.id, + kind: "user" as const, + refId: "prn_alice", + status: "active" as const, + createdAt: new Date(), + updatedAt: new Date(), +}; + +const allowAll: RequireGrant = () => async (_c, next) => { + await next(); +}; + +const REPOS: readonly GitHubRepoSummary[] = [ + { id: "1", name: "acme/widgets" }, + { id: "2", name: "acme/gadgets" }, +]; + +function mountAs(routes: Hono): Hono { + const asTenant: MiddlewareHandler = async (c, next) => { + c.set("tenant", TENANT); + c.set("principal", PRINCIPAL); + await next(); + }; + const app = new Hono(); + app.use("*", asTenant); + app.route("/", routes); + return app; +} + +function buildApp(overrides: Partial = {}) { + const grants: { tenantId: string; repo: GitHubRepoSummary }[] = []; + const triggers: { tenantId: string; repo: GitHubRepoSummary }[] = []; + const introductionCalls: { + tenantId: string; + workbenchId: string; + principalId: string; + introductions: readonly { handle: string; text: string }[]; + }[] = []; + let settings: { + pendingConnections: readonly string[]; + selectedRepos: readonly string[]; + } = { pendingConnections: ["github"], selectedRepos: [] }; + const githubConfig: GitHubClientConfig | undefined = { apiKey: "ghp_test" }; + + const deps: ConnectGithubRoutesDeps = { + requireGrant: allowAll, + log: () => {}, + resolveGithubConfig: async () => githubConfig, + resolveCodeReviewDefinitionId: async () => "wfd_code_review", + acquireRepoReviewLease: async () => true, + releaseRepoReviewLease: async () => {}, + hasRepoGrant: async (tenantId, repo) => + grants.some((g) => g.tenantId === tenantId && g.repo.id === repo.id), + mintRepoGrant: async (tenantId, repo) => { + grants.push({ tenantId, repo }); + }, + createWebhookTrigger: async ( + tenantId, + _principalId, + _definitionId, + repo, + ) => { + triggers.push({ tenantId, repo }); + return { id: `trg_${repo.id}` }; + }, + hasWebhookTrigger: async (_tenantId, _definitionId, repo) => + triggers.some((t) => t.repo.id === repo.id), + getTemplateSettings: async () => settings, + persistSelectedRepos: async ( + _tenantId, + _workbenchId, + _principalId, + patch, + ) => { + settings = { + pendingConnections: patch["template/pendingConnections"], + selectedRepos: patch["template/selectedRepos"], + }; + }, + onReviewingStarted: async ( + tenantId, + workbenchId, + principalId, + introductions, + ) => { + introductionCalls.push({ + tenantId, + workbenchId, + principalId, + introductions, + }); + }, + listReposFn: async () => REPOS, + fetchAuthenticatedLoginFn: async () => "octocat", + ...overrides, + }; + + const routes = createConnectGithubRoutes(deps); + return { + app: mountAs(routes), + grants, + triggers, + introductionCalls, + settingsNow: () => settings, + }; +} + +afterEach(() => { + mock.restore(); +}); + +describe("GET /:workbenchId/github/state", () => { + test("reports disconnected with no github credential", async () => { + const { app } = buildApp({ resolveGithubConfig: async () => undefined }); + const response = await app.request("/wb_1/github/state"); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ kind: "disconnected" }); + }); + + test("reports connected repos and the room's selected ids once a credential exists", async () => { + const { app } = buildApp(); + const response = await app.request("/wb_1/github/state"); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + kind: "connected", + orgName: "octocat", + repos: REPOS, + selectedRepoIds: [], + }); + }); + + test("surfaces a consumer-language error, never the raw GitHub failure, on a listRepos throw", async () => { + const { app } = buildApp({ + listReposFn: async () => { + throw new Error( + "GitHub request to /user/repos failed: 401 Bad credentials", + ); + }, + }); + const response = await app.request("/wb_1/github/state"); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + kind: "error", + message: "Couldn't read your GitHub repositories. Try reconnecting.", + }); + }); +}); + +describe("POST /:workbenchId/github/start-reviewing", () => { + test("mints a grant and a live webhook trigger per repo, then persists the selection", async () => { + const harness = buildApp(); + const response = await harness.app.request("/wb_1/github/start-reviewing", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ repoIds: ["1", "2"] }), + }); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ startedTriggerCount: 2 }); + expect(harness.grants.map((g) => g.repo.name)).toEqual([ + "acme/widgets", + "acme/gadgets", + ]); + expect(harness.triggers.map((t) => t.repo.name)).toEqual([ + "acme/widgets", + "acme/gadgets", + ]); + expect(harness.settingsNow()).toEqual({ + pendingConnections: [], + selectedRepos: ["1", "2"], + }); + }); + + test("posts each reviewer's introduction once, naming the selected repos, after success", async () => { + const harness = buildApp(); + const response = await harness.app.request("/wb_1/github/start-reviewing", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ repoIds: ["1", "2"] }), + }); + expect(response.status).toBe(200); + expect(harness.introductionCalls.length).toBe(1); + const [call] = harness.introductionCalls; + if (call === undefined) throw new Error("expected one introduction call"); + expect(call.tenantId).toBe("tnt_1"); + expect(call.workbenchId).toBe("wb_1"); + expect(call.principalId).toBe("prn_alice"); + expect(call.introductions.length).toBe(3); + for (const introduction of call.introductions) { + expect( + ["acme/widgets", "acme/gadgets"].some((name) => + introduction.text.includes(name), + ), + ).toBe(true); + } + }); + + test("a rejecting introduction port still yields 200 and reports the failure", async () => { + const report = spyOn(errorSink, "reportError").mockReturnValue("ref_test"); + const harness = buildApp({ + onReviewingStarted: async () => { + throw new Error("boom"); + }, + }); + const response = await harness.app.request("/wb_1/github/start-reviewing", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ repoIds: ["1"] }), + }); + expect(response.status).toBe(200); + expect(report).toHaveBeenCalled(); + expect(report.mock.calls[0]?.[0]).toBeInstanceOf(Error); + expect(report.mock.calls[0]?.[1]).toMatchObject({ + operation: "connect-github.reviewerIntroductions", + tenantId: "tnt_1", + roomId: "wb_1", + }); + }); + + test("a second start-reviewing does not re-post introductions already landed for the room", async () => { + const harness = buildApp(); + const startReviewing = (repoIds: readonly string[]) => + harness.app.request("/wb_1/github/start-reviewing", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ repoIds }), + }); + const first = await startReviewing(["1"]); + expect(first.status).toBe(200); + expect(harness.introductionCalls).toHaveLength(1); + const second = await startReviewing(["1", "2"]); + expect(second.status).toBe(200); + expect(harness.introductionCalls).toHaveLength(1); + }); + + test("400s on a malformed body without leaking raw parser text", async () => { + const harness = buildApp(); + const response = await harness.app.request("/wb_1/github/start-reviewing", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ repoIds: "not-an-array" }), + }); + expect(response.status).toBe(400); + const body = (await response.json()) as { error: { code: string } }; + expect(body.error.code).toBe("bad_request"); + expect(harness.introductionCalls).toEqual([]); + }); + + test("409s when the tenant has no github credential yet", async () => { + const harness = buildApp({ resolveGithubConfig: async () => undefined }); + const response = await harness.app.request("/wb_1/github/start-reviewing", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ repoIds: ["1"] }), + }); + expect(response.status).toBe(409); + expect(harness.grants).toEqual([]); + expect(harness.introductionCalls).toEqual([]); + }); + + test("502s when GitHub cannot be read, without posting introductions", async () => { + const harness = buildApp({ + listReposFn: async () => { + throw new Error( + "GitHub request to /user/repos failed: 401 Bad credentials", + ); + }, + }); + const response = await harness.app.request("/wb_1/github/start-reviewing", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ repoIds: ["1"] }), + }); + expect(response.status).toBe(502); + expect(harness.introductionCalls).toEqual([]); + }); + + test("404s when the code-review workflow isn't deployed in this tenant", async () => { + const harness = buildApp({ + resolveCodeReviewDefinitionId: async () => undefined, + }); + const response = await harness.app.request("/wb_1/github/start-reviewing", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ repoIds: ["1"] }), + }); + expect(response.status).toBe(404); + expect(harness.grants).toEqual([]); + expect(harness.introductionCalls).toEqual([]); + }); +}); diff --git a/packages/connections/src/connect-github-routes.ts b/packages/connections/src/connect-github-routes.ts new file mode 100644 index 000000000..f8d0ad81e --- /dev/null +++ b/packages/connections/src/connect-github-routes.ts @@ -0,0 +1,403 @@ +// The room's own GitHub connect card, wired to real ports (CL-6344): the +// production half of `./connect-github-setup.ts`'s `startReviewingRepos` +// and the read side `@corbits/chat-ui`'s `ConnectGithubActions` polls. +// Mounted per-workbench, inside the platform's native tenant middleware +// (`TenantEnv`'s `tenant`/`principal` resolved before any handler here +// runs), the same way `@corbits/webhook-triggers`' management routes and +// `@workbench/connections`' routes are. +// +// Connecting the PAT itself is deliberately NOT a route here — +// `@corbits/chat-ui`'s `connect-github-actions.ts` header already says +// why: `@workbench/connections`' generic `/:connectorId/complete` (with +// `connectorId: "github"`, already registered in `CONNECTOR_REGISTRY`) is +// the credential test-and-store surface, and this module never +// reimplements it. What this module owns is the two things that need the +// decrypted secret and the live repo list: reading the connect card's +// state, and starting reviews once repos are picked. +// +// Every side effect a host needs — decrypting the tenant's GitHub +// credential, minting a grant, creating a live `webhook_trigger` row, +// resolving the deployed code-review workflow definition, and patching +// the room's settings — arrives as an injected port, exactly like +// `@workbench/connections`' `createConnectionRoutes`. `apps/hub` is the +// only caller that binds these against drizzle; this module never touches +// it, so it stays testable with plain fakes (`./connect-github-routes.test.ts`). +import { Hono } from "hono"; +import { type } from "arktype"; +import type { RequireGrant, TenantEnv } from "@intx/hub-api"; +import { cookiesFromHeader, makeErrorEnvelope } from "@workbench/hub-client"; +import { reportError } from "@corbits/error-sink"; + +import { + fetchAuthenticatedLogin, + listRepos, + type GitHubClientConfig, + type GitHubRepoSummary, +} from "@corbits/github-tools"; +import { + reviewerIntroductions, + type ReviewerIntroduction, +} from "@corbits/code-review/introductions"; + +import { startReviewingRepos } from "./connect-github-setup"; +import { templateReposSettingsPatch } from "@workbench/templates/settings"; + +// `startReviewingRepos`' own selected-repo mismatch throw and a GitHub +// client failure both carry provider/internal detail (a rejected repo id, +// an HTTP status line) that a person pasting a token never needs to read — +// the CL-6360 idiom this route follows throughout via +// `@workbench/hub-client`'s `makeErrorEnvelope`: log the real cause behind +// a `refId`, answer with one honest, actionable `userMessage`. +const REPOS_UNREADABLE_MESSAGE = + "Couldn't read your GitHub repositories. Try reconnecting."; +const START_REVIEWING_FAILED_MESSAGE = + "Couldn't start reviewing those repositories. Try again in a moment."; + +const StartReviewingBody = type({ + repoIds: "string[]", +}); + +export type ConnectGithubTemplateSettings = { + readonly pendingConnections: readonly string[]; + readonly selectedRepos: readonly string[]; +}; + +export type ConnectGithubRoutesDeps = { + requireGrant: RequireGrant; + /** Where a failure's real cause goes — the CL-6360 idiom: the client + * only ever sees `{code, userMessage, refId}`; the raw detail (a + * GitHub HTTP status line, a `startReviewingRepos` mismatch) is + * logged here, keyed by the same `refId`, so an operator can look it + * up without the client having seen it. */ + log: (line: string) => void; + /** The tenant's decrypted GitHub PAT, ready to hand `@corbits/github-tools` + * — `undefined` when no `github` credential resolves for this tenant + * (nothing connected yet). A host binds this to `resolveCredentialByName` + * + `CredentialCipher.decrypt` against the `github` connector's own + * `CONNECTOR_REGISTRY` credential name, the same secret + * `buildCredentialDelivery` decrypts for a tool binding — never a second, + * bespoke credential store. */ + resolveGithubConfig( + tenantId: string, + ): Promise; + /** The tenant's deployed `code-review` workflow definition id — + * `undefined` when the template's own workflow was never deployed for + * this tenant (a create-flow bug, not something this route can fix). */ + resolveCodeReviewDefinitionId(tenantId: string): Promise; + /** Acquires the short-lived lease serializing one repo's setup + * (CL-7242) — see `./connect-github-setup.ts`'s + * `ConnectGithubSetupPorts.acquireRepoReviewLease` for why this is + * the actual concurrency backstop, not `hasRepoGrant`/`hasWebhookTrigger` + * below. A host binds this to `@corbits/webhook-triggers`' + * `RepoReviewLeaseStore.acquire`. */ + acquireRepoReviewLease( + tenantId: string, + repo: GitHubRepoSummary, + ): Promise; + /** Releases a lease `acquireRepoReviewLease` won — see + * `ConnectGithubSetupPorts.releaseRepoReviewLease`. A host binds + * this to `RepoReviewLeaseStore.release`. */ + releaseRepoReviewLease( + tenantId: string, + repo: GitHubRepoSummary, + ): Promise; + /** True once this repo already has the `repo:` grant — see + * `./connect-github-setup.ts`'s `ConnectGithubSetupPorts.hasRepoGrant` + * for why this makes a retry between minting the grant and creating + * the trigger safe. */ + hasRepoGrant( + tenantId: string, + repo: GitHubRepoSummary, + cookies: string[], + ): Promise; + /** Mints the `repo:`-scoped grant a launched review run + * needs to read this repo — see `./connect-github-setup.ts`'s + * `ConnectGithubSetupPorts.mintRepoGrant` for the exact resource shape. */ + mintRepoGrant( + tenantId: string, + repo: GitHubRepoSummary, + cookies: string[], + ): Promise; + /** Creates the live `webhook_trigger` row this repo's pull-request-opened + * events fire, scoped to the resolved code-review definition. A host + * binds this to `@corbits/webhook-triggers`' `WebhookTriggerStore.create`. */ + createWebhookTrigger( + tenantId: string, + principalId: string, + codeReviewDefinitionId: string, + repo: GitHubRepoSummary, + ): Promise<{ readonly id: string }>; + /** True once this repo already has a live webhook trigger for the + * resolved code-review definition — see + * `./connect-github-setup.ts`'s `ConnectGithubSetupPorts.hasWebhookTrigger` + * for why this makes a retry after a mid-loop failure safe. */ + hasWebhookTrigger( + tenantId: string, + codeReviewDefinitionId: string, + repo: GitHubRepoSummary, + ): Promise; + /** The room's current `template/*` settings — read before every + * `start-reviewing` write so the state read and the persisted patch + * never race a stale pending-connections list. */ + getTemplateSettings( + tenantId: string, + workbenchId: string, + ): Promise; + /** Applies the settings PATCH `./settings.ts`'s + * `templateReposSettingsPatch` builds and publishes the room's + * `chat.settings` stream event — the connect card refetches its own + * state as the direct consequence of the `start-reviewing` call that + * triggers this patch, never a fold off the stream event itself. */ + persistSelectedRepos( + tenantId: string, + workbenchId: string, + principalId: string, + patch: ReturnType, + ): Promise; + /** Posts each reviewer's canned introduction to the room once + * `startReviewingRepos` succeeds — the identity beat of the first + * minute a person sees after picking repos. Called once, after the + * repos are recorded and before the 200 response; a later + * `start-reviewing` (change repos) does not call this again. A + * rejection is reported through `reportError` and never fails the + * response — the webhook triggers already exist, so a person must + * not see an error for a missed introduction. */ + onReviewingStarted( + tenantId: string, + workbenchId: string, + principalId: string, + introductions: readonly ReviewerIntroduction[], + ): Promise; + /** Test-only override, defaulting to `@corbits/github-tools`' real + * `listRepos` — lets `connect-github-routes.test.ts` stub the GitHub + * client without reaching for module mocking or the real network. */ + listReposFn?: typeof listRepos; + /** Test-only override, matching `listReposFn`'s pattern. */ + fetchAuthenticatedLoginFn?: typeof fetchAuthenticatedLogin; +}; + +export function createConnectGithubRoutes( + deps: ConnectGithubRoutesDeps, +): Hono { + const app = new Hono(); + const runListRepos = deps.listReposFn ?? listRepos; + const runFetchAuthenticatedLogin = + deps.fetchAuthenticatedLoginFn ?? fetchAuthenticatedLogin; + + async function readGithubState( + tenantId: string, + config: GitHubClientConfig, + ): Promise< + | { + ok: true; + orgName: string; + repos: readonly GitHubRepoSummary[]; + } + | { ok: false } + > { + try { + const [orgName, repos] = await Promise.all([ + runFetchAuthenticatedLogin(config), + runListRepos(config), + ]); + return { ok: true, orgName, repos }; + } catch (cause) { + // The GitHub client's own errors (transport, HTTP status, shape + // mismatch) carry no actionable detail for the person who pasted a + // token — see this module's own `REPOS_UNREADABLE_MESSAGE` comment + // — so only the log line below carries the real cause. + const message = cause instanceof Error ? cause.message : String(cause); + deps.log( + `connect-github: reading GitHub state failed for tenant ${tenantId}: ${message}`, + ); + return { ok: false }; + } + } + + app.get( + "/:workbenchId/github/state", + deps.requireGrant("credential:*", "read"), + async (c) => { + const tenant = c.get("tenant"); + const workbenchId = c.req.param("workbenchId"); + const config = await deps.resolveGithubConfig(tenant.id); + if (config === undefined) { + return c.json({ kind: "disconnected" }, 200); + } + + const state = await readGithubState(tenant.id, config); + if (!state.ok) { + return c.json( + { kind: "error", message: REPOS_UNREADABLE_MESSAGE }, + 200, + ); + } + + const settings = await deps.getTemplateSettings(tenant.id, workbenchId); + return c.json( + { + kind: "connected", + orgName: state.orgName, + repos: state.repos, + selectedRepoIds: settings.selectedRepos, + }, + 200, + ); + }, + ); + + app.post( + "/:workbenchId/github/start-reviewing", + deps.requireGrant("credential:*", "read"), + async (c) => { + const body = StartReviewingBody( + await c.req.json().catch(() => undefined), + ); + if (body instanceof type.errors) { + return c.json( + makeErrorEnvelope({ + code: "bad_request", + userMessage: "Pick at least one repository.", + }), + 400, + ); + } + + const tenant = c.get("tenant"); + const principal = c.get("principal"); + const workbenchId = c.req.param("workbenchId"); + + const config = await deps.resolveGithubConfig(tenant.id); + if (config === undefined) { + return c.json( + makeErrorEnvelope({ + code: "not_connected", + userMessage: "Connect GitHub first.", + }), + 409, + ); + } + + const state = await readGithubState(tenant.id, config); + if (!state.ok) { + return c.json( + makeErrorEnvelope({ + code: "upstream_unavailable", + userMessage: REPOS_UNREADABLE_MESSAGE, + }), + 502, + ); + } + + const codeReviewDefinitionId = await deps.resolveCodeReviewDefinitionId( + tenant.id, + ); + if (codeReviewDefinitionId === undefined) { + return c.json( + makeErrorEnvelope({ + code: "not_found", + userMessage: + "This workbench's code-review workflow isn't set up yet.", + }), + 404, + ); + } + + try { + const settingsBefore = await deps.getTemplateSettings( + tenant.id, + workbenchId, + ); + const introductionsAlreadyPosted = + settingsBefore.selectedRepos.length > 0; + const result = await startReviewingRepos(body.repoIds, state.repos, { + acquireRepoReviewLease: (repo) => + deps.acquireRepoReviewLease(tenant.id, repo), + releaseRepoReviewLease: (repo) => + deps.releaseRepoReviewLease(tenant.id, repo), + hasRepoGrant: (repo) => + deps.hasRepoGrant( + tenant.id, + repo, + cookiesFromHeader(c.req.header("cookie")), + ), + mintRepoGrant: (repo) => + deps.mintRepoGrant( + tenant.id, + repo, + cookiesFromHeader(c.req.header("cookie")), + ), + createWebhookTrigger: (repo) => + deps.createWebhookTrigger( + tenant.id, + principal.id, + codeReviewDefinitionId, + repo, + ), + hasWebhookTrigger: (repo) => + deps.hasWebhookTrigger(tenant.id, codeReviewDefinitionId, repo), + persistSelectedRepos: async (repoIds) => { + const settings = await deps.getTemplateSettings( + tenant.id, + workbenchId, + ); + const pendingConnections = settings.pendingConnections.filter( + (id) => id !== "github", + ); + await deps.persistSelectedRepos( + tenant.id, + workbenchId, + principal.id, + templateReposSettingsPatch(pendingConnections, repoIds), + ); + }, + }); + + const reposById = new Map( + state.repos.map((repo) => [repo.id, repo] as const), + ); + const repoNames = body.repoIds + .map((repoId) => reposById.get(repoId)?.name) + .filter((name): name is string => name !== undefined); + if (!introductionsAlreadyPosted) { + try { + await deps.onReviewingStarted( + tenant.id, + workbenchId, + principal.id, + reviewerIntroductions(repoNames), + ); + } catch (cause) { + reportError(cause, { + operation: "connect-github.reviewerIntroductions", + tenantId: tenant.id, + roomId: workbenchId, + }); + } + } + + return c.json( + { startedTriggerCount: result.createdTriggerIds.length }, + 200, + ); + } catch (cause) { + // A repoId `state.repos` doesn't carry means the card's own + // selection state is stale against a live re-list — never + // something a raw stack trace helps the person fix. + const message = cause instanceof Error ? cause.message : String(cause); + deps.log( + `connect-github: start-reviewing failed for tenant ${tenant.id}, workbench ${workbenchId}: ${message}`, + ); + return c.json( + makeErrorEnvelope({ + code: "start_reviewing_failed", + userMessage: START_REVIEWING_FAILED_MESSAGE, + }), + 400, + ); + } + }, + ); + + return app; +} diff --git a/packages/connections/src/connect-github-setup.race.test.ts b/packages/connections/src/connect-github-setup.race.test.ts new file mode 100644 index 000000000..2f45bfb5a --- /dev/null +++ b/packages/connections/src/connect-github-setup.race.test.ts @@ -0,0 +1,219 @@ +// CL-7242: reconstructs the audit's own reproduction -- two concurrent +// `startReviewingRepos` calls for the same repo -- against real, +// database-backed ports rather than plain fakes. `hasRepoGrant`/ +// `mintRepoGrant` here fake a plain read-then-insert against the +// `grant` table directly, standing in for `apps/hub/src/index.ts`'s +// real binding (HTTP calls through `native-repo-grants.ts`, see +// CL-7242 follow-up "Mint workbench tenants and repo grants via +// Interchange HTTP") without a live hub-api server -- what this test +// actually proves is that `startReviewingRepos`' lease serializes any +// such hasRepoGrant/mintRepoGrant pair correctly, which is exactly +// what makes the real HTTP-bound versions safe too. The lease store's +// own test (`@corbits/webhook-triggers`'s +// `repo-review-lease.drizzle.test.ts`) proves the underlying +// compare-and-swap works; this proves `startReviewingRepos` itself +// settles on exactly one grant and one trigger, racing the two calls +// a double-click or a client retry would produce. DB-gated: skipped +// when DATABASE_URL is unset. Runs against its own scratch database +// (mirroring `@corbits/webhook-triggers`' own `store.drizzle.test.ts`), +// never the developer's or the walking-skeleton suite's. +import { afterAll, beforeAll, expect, test } from "bun:test"; +import { drizzle } from "drizzle-orm/postgres-js"; +import { and, eq } from "drizzle-orm"; +import postgres from "postgres"; + +import { runMigrations } from "@intx/db"; +import * as intxSchema from "@intx/db/schema"; +import { grant as grantTable, role as roleTable } from "@intx/db/schema"; +import { createNoopCredentialCipher } from "@intx/crypto"; +import { + createDrizzleRepoReviewLeaseStore, + createDrizzleWebhookTriggerStore, + applyWebhookTriggersMigrations, +} from "@corbits/webhook-triggers"; +import type { GitHubRepoSummary } from "@corbits/github-tools"; + +import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { dbGate } from "../../../scripts/e2e/db-gate"; +import { + startReviewingRepos, + webhookTriggerName, + type ConnectGithubSetupPorts, +} from "./connect-github-setup"; + +function scratchUrlFor(e2eUrl: string): string { + const url = new URL(e2eUrl); + const database = url.pathname.replace(/^\//, ""); + url.pathname = `/${database}_connect_github_race_test`; + return url.toString(); +} + +const databaseUrl = e2eDatabaseUrl(); +const describeIfDb = dbGate(databaseUrl, import.meta.path); + +const REPO: GitHubRepoSummary = { + id: "1", + name: "acme/widgets", +}; +const TENANT_ID = "tnt_race"; +const DEFINITION_ID = "def_code_review"; + +describeIfDb("startReviewingRepos under real concurrency (CL-7242)", () => { + const scratchUrl = scratchUrlFor( + databaseUrl ?? "postgres://localhost:5432/unused", + ); + const scratchTarget = new URL(scratchUrl); + const scratchDatabase = scratchTarget.pathname.replace(/^\//, ""); + + beforeAll(async () => { + const maintenanceUrl = new URL(scratchUrl); + maintenanceUrl.pathname = "/postgres"; + const maintenance = postgres(maintenanceUrl.toString(), { + max: 1, + onnotice: () => undefined, + }); + try { + await maintenance.unsafe(`DROP DATABASE IF EXISTS "${scratchDatabase}"`); + await maintenance.unsafe(`CREATE DATABASE "${scratchDatabase}"`); + } finally { + await maintenance.end(); + } + const parsed = new URL(scratchUrl); + await runMigrations( + { + host: parsed.hostname, + port: Number(parsed.port || 5432), + user: decodeURIComponent(parsed.username), + password: decodeURIComponent(parsed.password), + database: scratchDatabase, + }, + { schema: "public" }, + ); + await applyWebhookTriggersMigrations(scratchUrl); + }); + + afterAll(async () => { + const maintenanceUrl = new URL(scratchUrl); + maintenanceUrl.pathname = "/postgres"; + const maintenance = postgres(maintenanceUrl.toString(), { + max: 1, + onnotice: () => undefined, + }); + try { + await maintenance.unsafe(`DROP DATABASE IF EXISTS "${scratchDatabase}"`); + } finally { + await maintenance.end(); + } + }); + + test("two concurrent calls for the same repo mint exactly one grant and one trigger", async () => { + const client = postgres(scratchUrl, { + max: 10, + onnotice: () => undefined, + }); + try { + const db = drizzle(client, { schema: intxSchema }); + const webhookStore = createDrizzleWebhookTriggerStore( + db, + createNoopCredentialCipher(), + ); + const leaseStore = createDrizzleRepoReviewLeaseStore(db); + + const roleId = "role_race_member"; + await client`INSERT INTO "tenant" (id, name, slug, domain) VALUES (${TENANT_ID}, 'Acme', 'acme-race', 'acme-race.example')`; + await client`INSERT INTO "role" (id, tenant_id, name) VALUES (${roleId}, ${TENANT_ID}, 'member')`; + + // Mirrors apps/hub/src/index.ts's real wiring exactly: the lease + // is acquired first, and mintRepoGrant is a bare insert against + // Interchange's own `grant` table -- no onConflict, no index of + // ours on their table. Only the lease serializes the two racing + // calls below. + const buildPorts = (): ConnectGithubSetupPorts => ({ + acquireRepoReviewLease: (repo) => + leaseStore.acquire(TENANT_ID, repo.name), + releaseRepoReviewLease: (repo) => + leaseStore.release(TENANT_ID, repo.name), + hasRepoGrant: async (repo) => { + const existing = await db.query.grant.findFirst({ + where: and( + eq(grantTable.tenantId, TENANT_ID), + eq(grantTable.resource, `repo:${repo.name}`), + eq(grantTable.action, "read"), + ), + columns: { id: true }, + }); + return existing !== undefined; + }, + mintRepoGrant: async (repo) => { + const memberRole = await db.query.role.findFirst({ + where: and( + eq(roleTable.tenantId, TENANT_ID), + eq(roleTable.name, "member"), + ), + columns: { id: true }, + }); + if (memberRole === undefined) throw new Error("no member role"); + await db.insert(grantTable).values({ + id: `grant_${crypto.randomUUID()}`, + tenantId: TENANT_ID, + roleId: memberRole.id, + resource: `repo:${repo.name}`, + action: "read", + effect: "allow", + origin: "system", + }); + }, + hasWebhookTrigger: async (repo) => { + const triggers = await webhookStore.list(TENANT_ID); + const name = webhookTriggerName(repo); + return triggers.some( + (t) => t.workflowDefinitionId === DEFINITION_ID && t.name === name, + ); + }, + createWebhookTrigger: async (repo) => { + const row = await webhookStore.ensure({ + id: `wht_${crypto.randomUUID()}`, + tenantId: TENANT_ID, + name: webhookTriggerName(repo), + workflowDefinitionId: DEFINITION_ID, + inputTemplate: + "Review the pull request at {{pull_request.html_url}}", + secret: crypto.randomUUID(), + createdBy: "user_1", + }); + return { id: row.id }; + }, + persistSelectedRepos: async () => {}, + }); + + const [first, second] = await Promise.all([ + startReviewingRepos(["1"], [REPO], buildPorts()), + startReviewingRepos(["1"], [REPO], buildPorts()), + ]); + + // Whether the second call gets skipped by the lease or simply + // finds the work already done (CL-7134's fast path) depends on + // real, non-deterministic timing between the two real Postgres + // round trips -- both are correct outcomes. The property this + // test actually cares about is that at most one trigger is ever + // created, and the database never ends up with a duplicate. + const totalCreated = + first.createdTriggerIds.length + second.createdTriggerIds.length; + expect(totalCreated).toBe(1); + + const grantRows = await client` + SELECT id FROM "grant" + WHERE tenant_id = ${TENANT_ID} AND resource = 'repo:acme/widgets' + `; + expect(grantRows).toHaveLength(1); + + const triggerRows = await client` + SELECT id FROM "webhook_triggers"."webhook_trigger" + WHERE tenant_id = ${TENANT_ID} AND workflow_definition_id = ${DEFINITION_ID} + `; + expect(triggerRows).toHaveLength(1); + } finally { + await client.end(); + } + }); +}); diff --git a/packages/connections/src/connect-github-setup.test.ts b/packages/connections/src/connect-github-setup.test.ts new file mode 100644 index 000000000..bc8ebae6d --- /dev/null +++ b/packages/connections/src/connect-github-setup.test.ts @@ -0,0 +1,248 @@ +import { describe, expect, test } from "bun:test"; + +import { + startReviewingRepos, + webhookTriggerName, + type ConnectGithubSetupPorts, +} from "./connect-github-setup"; +import type { GitHubRepoSummary } from "@corbits/github-tools"; + +const REPOS: readonly GitHubRepoSummary[] = [ + { id: "1", name: "acme/widgets" }, + { id: "2", name: "acme/gadgets" }, + { id: "3", name: "acme/sprockets" }, +]; + +function fakePorts() { + const grantedRepos: string[] = []; + const createdTriggerRepos: string[] = []; + let persistedRepoIds: readonly string[] | undefined; + const ports: ConnectGithubSetupPorts = { + async acquireRepoReviewLease() { + return true; + }, + async releaseRepoReviewLease() {}, + async hasRepoGrant() { + return false; + }, + async mintRepoGrant(repo) { + grantedRepos.push(repo.name); + }, + async createWebhookTrigger(repo) { + createdTriggerRepos.push(repo.name); + return { id: `trg_${repo.id}` }; + }, + async hasWebhookTrigger() { + return false; + }, + async persistSelectedRepos(repoIds) { + persistedRepoIds = repoIds; + }, + }; + return { + ports, + grantedRepos, + createdTriggerRepos, + persistedRepoIds: () => persistedRepoIds, + }; +} + +/** + * A grant-store- and `WebhookTriggerStore`-backed fake: `hasRepoGrant` + * and `hasWebhookTrigger` each reflect what `mintRepoGrant` and + * `createWebhookTrigger` have actually persisted so far — independently + * of each other, the same as the real ports bind against the real + * `grant` table and `WebhookTriggerStore.list`. Used to prove a retry + * after a mid-loop failure is idempotent regardless of exactly where in + * a repo's two steps the failure landed. + */ +function fakeBackedPorts() { + const grantedRepoNames = new Set(); + const existingTriggerRepoNames = new Set(); + const leasedRepoNames = new Set(); + const grantedRepos: string[] = []; + const createdTriggerRepos: string[] = []; + const ports: ConnectGithubSetupPorts = { + // Synchronous check-and-set, same as the real lease's DB-side + // compare-and-swap: two calls sharing this same `ports` object + // (e.g. via `Promise.all`) race here exactly like two real + // concurrent HTTP requests race the real `RepoReviewLeaseStore`. + async acquireRepoReviewLease(repo) { + if (leasedRepoNames.has(repo.name)) return false; + leasedRepoNames.add(repo.name); + return true; + }, + async releaseRepoReviewLease(repo) { + leasedRepoNames.delete(repo.name); + }, + async hasRepoGrant(repo) { + return grantedRepoNames.has(repo.name); + }, + async mintRepoGrant(repo) { + grantedRepos.push(repo.name); + grantedRepoNames.add(repo.name); + }, + async createWebhookTrigger(repo) { + createdTriggerRepos.push(repo.name); + existingTriggerRepoNames.add(repo.name); + return { id: `trg_${repo.id}` }; + }, + async hasWebhookTrigger(repo) { + return existingTriggerRepoNames.has(repo.name); + }, + async persistSelectedRepos() {}, + }; + return { ports, grantedRepos, createdTriggerRepos }; +} + +describe("startReviewingRepos", () => { + test("mints a grant and a webhook trigger per selected repo, then persists the selection", async () => { + const fake = fakePorts(); + const result = await startReviewingRepos(["1", "3"], REPOS, fake.ports); + + expect(fake.grantedRepos).toEqual(["acme/widgets", "acme/sprockets"]); + expect(fake.createdTriggerRepos).toEqual([ + "acme/widgets", + "acme/sprockets", + ]); + expect(result.createdTriggerIds).toEqual(["trg_1", "trg_3"]); + expect(fake.persistedRepoIds()).toEqual(["1", "3"]); + }); + + test("never mints anything for a repo that isn't selected", async () => { + const fake = fakePorts(); + await startReviewingRepos(["2"], REPOS, fake.ports); + expect(fake.grantedRepos).toEqual(["acme/gadgets"]); + }); + + test("throws rather than silently dropping a repoId the listed repos don't carry", async () => { + const fake = fakePorts(); + await expect( + startReviewingRepos(["not-a-real-repo"], REPOS, fake.ports), + ).rejects.toThrow(/not in the listed repos/); + expect(fake.grantedRepos).toEqual([]); + }); + + test("retrying after a mid-loop failure never mints a duplicate grant or trigger", async () => { + const fake = fakeBackedPorts(); + const failingPorts: ConnectGithubSetupPorts = { + ...fake.ports, + async mintRepoGrant(repo) { + if (repo.name === "acme/gadgets") { + throw new Error("mint failed"); + } + await fake.ports.mintRepoGrant(repo); + }, + }; + + await expect( + startReviewingRepos(["1", "2", "3"], REPOS, failingPorts), + ).rejects.toThrow(/mint failed/); + + // The first repo made it through before the failure; the other two + // never got a grant or a trigger. + expect(fake.grantedRepos).toEqual(["acme/widgets"]); + expect(fake.createdTriggerRepos).toEqual(["acme/widgets"]); + + // Retrying the same selection (as the route's "Try again" does) + // skips the repo that's already set up and only mints for the rest. + const result = await startReviewingRepos( + ["1", "2", "3"], + REPOS, + fake.ports, + ); + + expect(fake.grantedRepos).toEqual([ + "acme/widgets", + "acme/gadgets", + "acme/sprockets", + ]); + expect(fake.createdTriggerRepos).toEqual([ + "acme/widgets", + "acme/gadgets", + "acme/sprockets", + ]); + expect(result.createdTriggerIds).toEqual(["trg_2", "trg_3"]); + }); + + test("retrying after a failure between minting the grant and creating the trigger never re-mints the grant", async () => { + const fake = fakeBackedPorts(); + const failingPorts: ConnectGithubSetupPorts = { + ...fake.ports, + async createWebhookTrigger(repo) { + if (repo.name === "acme/gadgets") { + throw new Error("trigger create failed"); + } + return fake.ports.createWebhookTrigger(repo); + }, + }; + + await expect( + startReviewingRepos(["1", "2", "3"], REPOS, failingPorts), + ).rejects.toThrow(/trigger create failed/); + + // The failing repo's grant was minted before the trigger create blew + // up; the repo after it was never reached at all. + expect(fake.grantedRepos).toEqual(["acme/widgets", "acme/gadgets"]); + expect(fake.createdTriggerRepos).toEqual(["acme/widgets"]); + + const result = await startReviewingRepos( + ["1", "2", "3"], + REPOS, + fake.ports, + ); + + // acme/gadgets' grant is not re-minted on retry — only its missing + // trigger is created. + expect(fake.grantedRepos).toEqual([ + "acme/widgets", + "acme/gadgets", + "acme/sprockets", + ]); + expect(fake.createdTriggerRepos).toEqual([ + "acme/widgets", + "acme/gadgets", + "acme/sprockets", + ]); + expect(result.createdTriggerIds).toEqual(["trg_2", "trg_3"]); + }); + + test("two concurrent calls for the same repo mint exactly one grant and one trigger (CL-7242)", async () => { + // Reconstructs the audit's own reproduction: `Promise.all` of two + // concurrent `startReviewingRepos` calls sharing the same + // backing state, for the same repo. Before the lease existed, + // both calls' `hasRepoGrant`/`hasWebhookTrigger` reads landed + // before either write, so both minted -- 2 grants, 2 triggers, + // for one repo. `acquireRepoReviewLease`'s synchronous + // check-and-set means only one of the two calls below ever gets + // past the lease for "acme/widgets": the other's per-repo body + // never runs at all. + const fake = fakeBackedPorts(); + + const [first, second] = await Promise.all([ + startReviewingRepos(["1"], REPOS, fake.ports), + startReviewingRepos(["1"], REPOS, fake.ports), + ]); + + expect(fake.grantedRepos).toEqual(["acme/widgets"]); + expect(fake.createdTriggerRepos).toEqual(["acme/widgets"]); + + const totalCreated = + first.createdTriggerIds.length + second.createdTriggerIds.length; + expect(totalCreated).toBe(1); + + const totalSkipped = + first.skippedRepoIds.length + second.skippedRepoIds.length; + expect(totalSkipped).toBe(1); + }); +}); + +describe("webhookTriggerName", () => { + test("names a repo's trigger consistently, the one convention both the create and the lookup bind against", () => { + const repo: GitHubRepoSummary = { + id: "1", + name: "acme/widgets", + }; + expect(webhookTriggerName(repo)).toBe("acme/widgets pull-request-opened"); + }); +}); diff --git a/packages/connections/src/connect-github-setup.ts b/packages/connections/src/connect-github-setup.ts new file mode 100644 index 000000000..c65fca755 --- /dev/null +++ b/packages/connections/src/connect-github-setup.ts @@ -0,0 +1,190 @@ +// What runs once a person has actually picked repos on a room's GitHub +// connect card and starts reviewing. `instantiateWorkbenchTemplate` +// resolves a template at create time, before any repo is known; this +// module resolves the repo-scoped half once the person answers the +// connect card and creates the live webhook trigger per repo, mirroring +// `instantiate.ts`'s own shape exactly: pure orchestration over injected +// async ports, no HTTP, no store, testable with plain fakes. +import type { GitHubRepoSummary } from "@corbits/github-tools"; + +export interface ConnectGithubSetupPorts { + /** + * Acquires the short-lived lease serializing this repo's setup + * (CL-7242): true means this call now owns it and must run the rest + * of this repo's body below; false means another call currently + * owns it (or very recently did) and this repo must be skipped + * entirely, including `hasRepoGrant`/`hasWebhookTrigger`. This is + * the actual concurrency backstop — `hasRepoGrant` and + * `hasWebhookTrigger` below are a fast path for a *sequential* + * retry (CL-7134), not a lock, and are only safe to reach because + * the lease already ensures two concurrent calls for the same repo + * can never both get here. A host binds this to + * `@corbits/webhook-triggers`' `RepoReviewLeaseStore.acquire`. + */ + acquireRepoReviewLease(repo: GitHubRepoSummary): Promise; + /** + * Releases the lease `acquireRepoReviewLease` won, once this repo's + * body finishes (success or failure) — so a legitimate retry never + * waits out the lease's own staleness window. A host binds this to + * `RepoReviewLeaseStore.release`. + */ + releaseRepoReviewLease(repo: GitHubRepoSummary): Promise; + /** + * True once this repo already has the `repo:` grant — + * checked before minting one, so a retry after a failure between + * minting the grant and creating the trigger skips straight past + * minting for a repo that already has one (CL-7134's fast path). The + * `grant` table (`vendor/intx/db`) carries no unique constraint over + * tenant/resource/action, so this read is the only thing standing + * between a retry and a duplicate row absent the lease below. A host + * binds this to GET `/api/tenants/:id/grants?resource=repo:`. + * + * Safe to be a plain read, not a conflict-checked one: the lease + * above already guarantees only one caller ever reaches this point + * for a given repo at a time. + */ + hasRepoGrant(repo: GitHubRepoSummary): Promise; + /** + * Mints one grant scoped to `repo:` (the `owner/name` full + * name — the same `":"` resource-string shape + * `idResource("room", "id")` builds in `@intx/hub-api`'s grant + * middleware, applied here to a repo instead of a room). A host binds + * this to POST `/api/tenants/:id/grants`; this module never touches + * drizzle directly. + * + * A plain insert is safe here (CL-7242): the lease above is what + * makes this call-site single-flight per repo, so this never needs + * its own conflict handling against the platform's `grant` table. + */ + mintRepoGrant(repo: GitHubRepoSummary): Promise; + /** + * Creates the live `webhook_trigger` row this repo's pull-request-opened + * events fire — the onboarding card's start-reviewing step is what + * creates this trigger, for each repo the person picked. A host + * binds this to `@corbits/webhook-triggers`' `WebhookTriggerStore.ensure` + * rather than `create`: the lease already makes this call-site + * single-flight per repo, so `ensure`'s own idempotence + * (backed by `webhook_trigger_tenant_definition_name_unique`, our + * own schema, unaffected by CL-7242's vendored-table constraint) is + * pure defense-in-depth — a lease bug degrades to a silent no-op + * here instead of a hard failure or a real duplicate trigger. + */ + createWebhookTrigger( + repo: GitHubRepoSummary, + ): Promise<{ readonly id: string }>; + /** + * True once this repo already has a live webhook trigger — checked + * before creating one, so a retry after a mid-loop failure (a repo + * 1..N-1 already set up, N onward not) skips straight past creating + * one for a repo a prior attempt already finished (CL-7134's fast + * path). A host binds this to a read against + * `@corbits/webhook-triggers`' `WebhookTriggerStore.list`. + * + * This is never cleared on GitHub disconnect: nothing here disables or + * deletes a trigger, so re-adding a repo after a reconnect finds its + * old trigger still live and skips it rather than minting a new one — + * intentional, not a gap this module owns closing. + */ + hasWebhookTrigger(repo: GitHubRepoSummary): Promise; + /** + * Records which repos this room is reviewing — the `template/*` + * settings namespace's `selectedRepos` key (`./settings.ts`'s + * `templateReposSettingsPatch`). A host binds this to the room's + * existing settings PATCH route; the connect-github card refetches + * its own state as the direct consequence of the call that triggers + * this patch, never a fold off the resulting stream event. + */ + persistSelectedRepos(repoIds: readonly string[]): Promise; +} + +export interface StartReviewingReposResult { + readonly createdTriggerIds: readonly string[]; + /** + * Repos this call didn't touch because another call currently owns + * (or very recently owned) their setup lease — not an error, but + * worth surfacing rather than silently dropping: if the lease + * holder crashed, this is the caller's only signal that a repo in + * the selection got no setup work done at all this round. + */ + readonly skippedRepoIds: readonly string[]; +} + +/** + * Mints one grant and one webhook trigger per selected repo that doesn't + * already have one, then records the selection. `repoIds` must all + * resolve against `repos` — a caller passing an id `repos` doesn't carry + * is a bug in how the connect card's own selection state was built, not + * something to silently drop. + * + * Idempotent by construction: the grant and the trigger are each gated + * on their own existence check, independently, rather than one check + * guarding both — a retry after a failure between minting the grant and + * creating the trigger must still create the trigger without re-minting + * the grant, and a retry after a failure before the grant was minted + * must still mint it. A repo both checks already report true for is + * skipped entirely. (CL-7134.) + * + * `hasRepoGrant`/`hasWebhookTrigger` are a fast path for a *sequential* + * retry, not a lock: two truly concurrent calls for the same repo (a + * double-click, or a client retrying an in-flight request rather than + * a failed one) can both read "not set up yet" before either write + * lands. `acquireRepoReviewLease` is the actual concurrency backstop + * (CL-7242): only the caller that wins the lease enters the rest of a + * repo's body at all, so the checks below never race against a + * concurrent duplicate for the same repo — the lease is released + * (`releaseRepoReviewLease`) as soon as that body finishes, success or + * failure, so a legitimate retry is never blocked by its own prior + * attempt. + */ +export async function startReviewingRepos( + repoIds: readonly string[], + repos: readonly GitHubRepoSummary[], + ports: ConnectGithubSetupPorts, +): Promise { + const reposById = new Map(repos.map((repo) => [repo.id, repo])); + const selected = repoIds.map((repoId) => { + const repo = reposById.get(repoId); + if (repo === undefined) { + throw new Error( + `startReviewingRepos was asked to review repo "${repoId}", which is not in the listed repos`, + ); + } + return repo; + }); + + const createdTriggerIds: string[] = []; + const skippedRepoIds: string[] = []; + for (const repo of selected) { + if (!(await ports.acquireRepoReviewLease(repo))) { + skippedRepoIds.push(repo.id); + continue; + } + try { + if (!(await ports.hasRepoGrant(repo))) { + await ports.mintRepoGrant(repo); + } + if (await ports.hasWebhookTrigger(repo)) { + continue; + } + const trigger = await ports.createWebhookTrigger(repo); + createdTriggerIds.push(trigger.id); + } finally { + await ports.releaseRepoReviewLease(repo); + } + } + + await ports.persistSelectedRepos(repoIds); + + return { createdTriggerIds, skippedRepoIds }; +} + +/** + * The one place the `webhook_trigger.name` convention for a repo's + * pull-request-opened trigger is spelled out — both + * `createWebhookTrigger`'s insert and `hasWebhookTrigger`'s lookup bind + * against this, in `apps/hub`, so the two can never drift into matching + * different strings. + */ +export function webhookTriggerName(repo: GitHubRepoSummary): string { + return `${repo.name} pull-request-opened`; +} diff --git a/packages/connections/src/reconcile-duplicate-repo-grants.test.ts b/packages/connections/src/reconcile-duplicate-repo-grants.test.ts new file mode 100644 index 000000000..dd7a0e378 --- /dev/null +++ b/packages/connections/src/reconcile-duplicate-repo-grants.test.ts @@ -0,0 +1,136 @@ +// DB-gated: skipped when no DATABASE_URL is reachable. Runs against +// its own scratch database. +// +// CL-7242: `reconcileDuplicateRepoGrants` is the one-time cleanup for +// duplicate `repo:` grants a database can already carry +// from before the `repo_review_lease` fix started preventing new +// ones. Proves it keeps exactly one grant per group (never zero, +// matching the "a repo never ends up with fewer grants than it had" +// bar), never touches an unrelated grant, and is safe to re-run. +import { afterAll, beforeAll, expect, test } from "bun:test"; +import postgres from "postgres"; + +import { runMigrations } from "@intx/db"; + +import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; +import { reconcileDuplicateRepoGrants } from "./reconcile-duplicate-repo-grants"; +import { dbGate } from "../../../scripts/e2e/db-gate"; + +function scratchUrlFor(e2eUrl: string): string { + const url = new URL(e2eUrl); + const database = url.pathname.replace(/^\//, ""); + url.pathname = `/${database}_reconcile_duplicate_repo_grants_test`; + return url.toString(); +} + +const databaseUrl = e2eDatabaseUrl(); +const describeIfDb = dbGate(databaseUrl, import.meta.path); + +const TENANT_ID = "tnt_reconcile"; +const ROLE_ID = "role_reconcile_member"; + +describeIfDb("reconcileDuplicateRepoGrants", () => { + const scratchUrl = scratchUrlFor( + databaseUrl ?? "postgres://localhost:5432/unused", + ); + const scratchTarget = new URL(scratchUrl); + const scratchDatabase = scratchTarget.pathname.replace(/^\//, ""); + + beforeAll(async () => { + const maintenanceUrl = new URL(scratchUrl); + maintenanceUrl.pathname = "/postgres"; + const maintenance = postgres(maintenanceUrl.toString(), { + max: 1, + onnotice: () => undefined, + }); + try { + await maintenance.unsafe(`DROP DATABASE IF EXISTS "${scratchDatabase}"`); + await maintenance.unsafe(`CREATE DATABASE "${scratchDatabase}"`); + } finally { + await maintenance.end(); + } + const parsed = new URL(scratchUrl); + await runMigrations( + { + host: parsed.hostname, + port: Number(parsed.port || 5432), + user: decodeURIComponent(parsed.username), + password: decodeURIComponent(parsed.password), + database: scratchDatabase, + }, + { schema: "public" }, + ); + const sql = postgres(scratchUrl, { max: 1, onnotice: () => undefined }); + try { + await sql`INSERT INTO "tenant" (id, name, slug, domain) VALUES (${TENANT_ID}, 'Acme', 'acme-reconcile', 'acme-reconcile.example')`; + await sql`INSERT INTO "role" (id, tenant_id, name) VALUES (${ROLE_ID}, ${TENANT_ID}, 'member')`; + } finally { + await sql.end(); + } + }); + + afterAll(async () => { + const maintenanceUrl = new URL(scratchUrl); + maintenanceUrl.pathname = "/postgres"; + const maintenance = postgres(maintenanceUrl.toString(), { + max: 1, + onnotice: () => undefined, + }); + try { + await maintenance.unsafe(`DROP DATABASE IF EXISTS "${scratchDatabase}"`); + } finally { + await maintenance.end(); + } + }); + + test("keeps exactly one grant per duplicated (tenant, resource, action) and leaves other grants alone", async () => { + const sql = postgres(scratchUrl, { max: 1, onnotice: () => undefined }); + try { + // Two duplicate repo grants from the pre-lease race, one pair from + // each generation of `mintRepoGrant`: the direct-insert path this + // PR replaced wrote `'system'`-origin rows; the current + // `mintRepoGrantViaHttp` path writes `'creator'`-origin rows. + await sql` + INSERT INTO "grant" (id, tenant_id, role_id, resource, action, effect, origin, created_at) + VALUES + ('grant_dup_1', ${TENANT_ID}, ${ROLE_ID}, 'repo:acme/widgets', 'read', 'allow', 'system', now() - interval '1 hour'), + ('grant_dup_2', ${TENANT_ID}, ${ROLE_ID}, 'repo:acme/widgets', 'read', 'allow', 'creator', now()) + `; + // A non-duplicated repo grant, a non-repo system grant, and a + // repo grant with an origin this feature never writes -- none + // should ever be touched. + await sql` + INSERT INTO "grant" (id, tenant_id, role_id, resource, action, effect, origin) + VALUES + ('grant_solo', ${TENANT_ID}, ${ROLE_ID}, 'repo:acme/gadgets', 'read', 'allow', 'creator'), + ('grant_unrelated', ${TENANT_ID}, ${ROLE_ID}, 'workbench:room-1', 'read', 'allow', 'system'), + ('grant_other_origin', ${TENANT_ID}, ${ROLE_ID}, 'repo:acme/gizmos', 'read', 'allow', 'role') + `; + + const first = await reconcileDuplicateRepoGrants(scratchUrl); + expect(first.removedIds).toEqual(["grant_dup_2"]); + + const widgetsRows = await sql` + SELECT id FROM "grant" WHERE tenant_id = ${TENANT_ID} AND resource = 'repo:acme/widgets' + `; + expect(widgetsRows).toHaveLength(1); + expect(widgetsRows[0]?.["id"]).toBe("grant_dup_1"); + + const untouchedRows = await sql` + SELECT id FROM "grant" WHERE id IN ('grant_solo', 'grant_unrelated', 'grant_other_origin') + ORDER BY id + `; + expect(untouchedRows.map((r) => r["id"])).toEqual([ + "grant_other_origin", + "grant_solo", + "grant_unrelated", + ]); + + // Idempotent: nothing left to remove on a second run. + const second = await reconcileDuplicateRepoGrants(scratchUrl); + expect(second.removedIds).toEqual([]); + } finally { + await sql.end(); + } + }); +}); diff --git a/packages/connections/src/reconcile-duplicate-repo-grants.ts b/packages/connections/src/reconcile-duplicate-repo-grants.ts new file mode 100644 index 000000000..2b6f66f7d --- /dev/null +++ b/packages/connections/src/reconcile-duplicate-repo-grants.ts @@ -0,0 +1,55 @@ +// One-time cleanup for pre-existing duplicate `repo:` +// grants a database can already carry from CL-7242's check-then-act +// race, before this fix's lease (`@corbits/webhook-triggers`' +// `repo_review_lease`) started preventing new ones. +// +// This is ordinary application code, not a migration: a plain DELETE +// against Interchange's own `grant` table, issued through `@intx/db`'s +// published `grant` export — the exact same table object +// `apps/hub/src/index.ts` already reads and writes through for every +// other grant operation in this codebase. No schema is touched (no +// `ALTER TABLE`, no index, no constraint), so this creates no re-pin +// debt: a DDL delta would need hand-reapplying at every future +// `vendor/intx/*` re-pin, but a DML statement runs once and leaves +// nothing behind for a re-pin to reconcile. Deliberately safe to +// re-run — a database with no duplicates left just deletes nothing. +import postgres from "postgres"; + +export interface ReconcileDuplicateRepoGrantsReport { + /** Ids of the duplicate rows removed, kept for the caller to log. */ + readonly removedIds: readonly string[]; +} + +/** + * Keeps the lowest-`id` row per `(tenant_id, resource, action)` among + * `repo:`-prefixed grants with an origin `mintRepoGrant` could plausibly + * have written and deletes the rest. `'creator'` is what the current + * `mintRepoGrantViaHttp` path (`apps/hub/src/native-repo-grants.ts`) + * writes; `'system'` is what the direct-insert path this PR replaced + * used to write, so a database carrying duplicates from either + * generation of this feature gets them cleaned up. Never touches any + * other grant family. + */ +export async function reconcileDuplicateRepoGrants( + databaseUrl: string, +): Promise { + const sql = postgres(databaseUrl, { max: 1, onnotice: () => undefined }); + try { + const removed = await sql<{ id: string }[]>` + DELETE FROM "grant" AS g + USING "grant" AS older + WHERE g.origin IN ('system', 'creator') + AND g.resource LIKE 'repo:%' + AND older.origin IN ('system', 'creator') + AND older.resource LIKE 'repo:%' + AND g.tenant_id = older.tenant_id + AND g.resource = older.resource + AND g.action = older.action + AND older.id < g.id + RETURNING g.id + `; + return { removedIds: removed.map((row) => row.id) }; + } finally { + await sql.end(); + } +} diff --git a/packages/echo/README.md b/packages/echo/README.md index 47993b50b..272a01254 100644 --- a/packages/echo/README.md +++ b/packages/echo/README.md @@ -3,7 +3,7 @@ The smallest hub extension, kept deliberately trivial: a tenant-scoped route that echoes a `POST` body back verbatim as plain text. It exists as a wiring check for the mail-triggered contract, not as a real assistant — -the `echo` entry in `@corbits/workflow-catalog`'s `WORKFLOW_CATALOG` +the `echo` entry in `@workbench/templates`'s `WORKFLOW_CATALOG` describes it the same way. ## Composition with @intx/* diff --git a/packages/evals/package.json b/packages/evals/package.json index 85a46e4c8..ad49cd147 100644 --- a/packages/evals/package.json +++ b/packages/evals/package.json @@ -18,7 +18,7 @@ "@corbits/migration-runner": "workspace:*", "@corbits/routines": "workspace:*", "@corbits/webhook-triggers": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", + "@workbench/templates": "workspace:*", "@intx/agent": "workspace:*", "@intx/db": "workspace:*", "@intx/hub-api": "workspace:*", diff --git a/packages/evals/src/targets/real-target.ts b/packages/evals/src/targets/real-target.ts index 306c5c606..dac681ce5 100644 --- a/packages/evals/src/targets/real-target.ts +++ b/packages/evals/src/targets/real-target.ts @@ -33,7 +33,7 @@ import { instantiateWorkbenchTemplate, parseWorkbenchDefinition, templateSettingsPatch, -} from "@corbits/workflow-catalog"; +} from "@workbench/templates"; import { signPayload, WEBHOOK_SIGNATURE_HEADER, diff --git a/packages/hub-client/package.json b/packages/hub-client/package.json index ceb84ec2c..745523f8a 100644 --- a/packages/hub-client/package.json +++ b/packages/hub-client/package.json @@ -24,7 +24,7 @@ "@corbits/last-30-days-research-workflow": "workspace:*", "@corbits/tool-registry-publish": "workspace:*", "@corbits/workbench-digest-workflow": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", + "@workbench/templates": "workspace:*", "@corbits/workflows": "workspace:*", "@intx/inference": "workspace:*", "@intx/types": "workspace:*", diff --git a/packages/hub-client/src/seed.ts b/packages/hub-client/src/seed.ts index bab86992c..ecfbed6e3 100644 --- a/packages/hub-client/src/seed.ts +++ b/packages/hub-client/src/seed.ts @@ -7,7 +7,7 @@ // // Workflow package metadata (automatable, displayName) lives in each // workflows/*/package.json under `corbits.workflow` and is mirrored in -// `@corbits/workflow-catalog`. Seed stamps displayName onto the asset so +// `@workbench/templates`. Seed stamps displayName onto the asset so // the routines picker can show a friendly label without reading package.json. import { @@ -44,7 +44,7 @@ import { buildLast30DaysResearchWorkflow, serializeLast30DaysResearchWorkflow, } from "@corbits/last-30-days-research-workflow"; -import { WORKFLOW_CATALOG } from "@corbits/workflow-catalog"; +import { WORKFLOW_CATALOG } from "@workbench/templates"; import { capabilitiesForDeployment } from "@corbits/inference-catalog/offering-capabilities"; import { quirksForDeployment } from "@corbits/inference-catalog/ollama-context-defaults"; import { type PublishCorbitsToolsRegistryArgs } from "@corbits/tool-registry-publish"; diff --git a/packages/plugins-ui/package.json b/packages/plugins-ui/package.json index 1648f8fa3..05efa6eff 100644 --- a/packages/plugins-ui/package.json +++ b/packages/plugins-ui/package.json @@ -16,7 +16,6 @@ "dependencies": { "@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635", "@corbits/settings-ui": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", "@workbench/connections": "workspace:*", "arktype": "catalog:", "@corbits/icons": "workspace:*", diff --git a/packages/routines/README.md b/packages/routines/README.md index b39b699de..f1b4b7cf2 100644 --- a/packages/routines/README.md +++ b/packages/routines/README.md @@ -14,7 +14,7 @@ Built on `@intx/hub-api`'s `TenantEnv`/`requireGrant` route convention and session orchestration directly — launching a run (manual "run now" or a scheduled fire) goes through `@corbits/folded-runs`, the same launch core `@corbits/chat`'s invite flow uses. It also depends on -`@corbits/workflow-catalog` to resolve a definition's delivery mode +`@workbench/templates` to resolve a definition's delivery mode (workbench vs. inbox-only) so a routine's create/fire path never requires a `deliveryWorkbenchId` a workflow would silently discard. @@ -96,7 +96,7 @@ otherwise) only groups the picker. - `listRoutineTargets(deps, query)` authorizes every candidate with `@intx/authz`'s `authorize` on `workflow-definition:` / `read` before it is counted, sorted, or returned — a denied row never shapes the page — - then applies the product filter (`@corbits/workflow-catalog`'s + then applies the product filter (`@workbench/templates`'s `isAutomatableWorkflowName` or `isConversationalWorkflowName`, never a workbench-host anchor name) and orders by `(name asc, definitionAssetId asc)`. Pagination is an opaque cursor over that key; `limit` defaults to diff --git a/packages/routines/package.json b/packages/routines/package.json index 615271a96..0e2a20e80 100644 --- a/packages/routines/package.json +++ b/packages/routines/package.json @@ -22,7 +22,7 @@ "@corbits/migration-runner": "workspace:*", "@corbits/slug": "workspace:*", "cronstrue": "^3.24.0", - "@corbits/workflow-catalog": "workspace:*", + "@workbench/templates": "workspace:*", "@corbits/workflows": "workspace:*", "@intx/authz": "0.3.0", "@intx/db": "workspace:*", diff --git a/packages/routines/src/client.ts b/packages/routines/src/client.ts index 3469ec126..ce75542f9 100644 --- a/packages/routines/src/client.ts +++ b/packages/routines/src/client.ts @@ -151,7 +151,7 @@ export type CreateRoutineInput = { readonly trigger: RoutineTriggerT; readonly scope: "personal" | "bench"; /** Omitted only for a workflow whose result never posts to a workbench - * (see `@corbits/workflow-catalog`'s `WorkflowCatalogEntry.deliveryMode`) + * (see `@workbench/templates`'s `WorkflowCatalogEntry.deliveryMode`) * — the server itself still rejects a missing value for every other * workflow (`deliveryWorkbenchRequired`, routes.ts). */ readonly deliveryWorkbenchId?: string; @@ -198,7 +198,7 @@ export function routineRunsPath(tenantId: string, id: string): string { // `kind` groups the picker without changing execution semantics: an // "agent" is a single-step conversational fold, everything else a // "workflow". `assetName` is the raw catalog key -// (`@corbits/workflow-catalog`'s `workflowCatalogEntry`); `name` is the +// (`@workbench/templates`'s `workflowCatalogEntry`); `name` is the // display label. export const RoutineTargetKind = type("'agent' | 'workflow'"); export type RoutineTargetKind = typeof RoutineTargetKind.infer; diff --git a/packages/routines/src/targets.ts b/packages/routines/src/targets.ts index 41f099a2f..649008f2b 100644 --- a/packages/routines/src/targets.ts +++ b/packages/routines/src/targets.ts @@ -20,7 +20,7 @@ import { isAutomatableWorkflowName, isConversationalWorkflowName, workflowDisplayName, -} from "@corbits/workflow-catalog"; +} from "@workbench/templates"; import { listLaunchableDefinitions, type LaunchableDefinition, diff --git a/packages/settings-ui/package.json b/packages/settings-ui/package.json index 195fc707c..923ebd045 100644 --- a/packages/settings-ui/package.json +++ b/packages/settings-ui/package.json @@ -23,7 +23,7 @@ "@corbits/error-sink": "workspace:*", "@corbits/inference-settings": "workspace:*", "@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635", - "@corbits/workflow-catalog": "workspace:*", + "@workbench/templates": "workspace:*", "@intx/types": "workspace:*", "@workbench/connections": "workspace:*", "arktype": "catalog:", diff --git a/packages/settings-ui/src/connections-pinned-by.test.ts b/packages/settings-ui/src/connections-pinned-by.test.ts index 2161d2e07..964fca1b8 100644 --- a/packages/settings-ui/src/connections-pinned-by.test.ts +++ b/packages/settings-ui/src/connections-pinned-by.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { WORKFLOW_CATALOG } from "@corbits/workflow-catalog"; +import { WORKFLOW_CATALOG } from "@workbench/templates"; import { CONNECTOR_PINNED_WORKFLOWS } from "./connections-pinned-by"; diff --git a/packages/settings-ui/src/connections-section.tsx b/packages/settings-ui/src/connections-section.tsx index 30b9ca41e..a88c21068 100644 --- a/packages/settings-ui/src/connections-section.tsx +++ b/packages/settings-ui/src/connections-section.tsx @@ -26,7 +26,7 @@ import { type ConnectorDescriptor, } from "@workbench/connections/registry"; import { MCP_PRESET_CONNECTOR_IDS } from "@workbench/connections/mcp-presets"; -import { workflowDisplayName } from "@corbits/workflow-catalog"; +import { workflowDisplayName } from "@workbench/templates"; import { buildEffectiveInferenceRows, computeGlobalRoutePatches, diff --git a/packages/settings-ui/src/granola-webhook-card.tsx b/packages/settings-ui/src/granola-webhook-card.tsx index c438759b0..e50187024 100644 --- a/packages/settings-ui/src/granola-webhook-card.tsx +++ b/packages/settings-ui/src/granola-webhook-card.tsx @@ -8,7 +8,7 @@ // // "Granola-ish" means bound to the `granola-call` workflow definition // specifically — the one automatable Granola workflow in -// `@corbits/workflow-catalog` (`process-granola-call` is a child run, +// `@workbench/templates` (`process-granola-call` is a child run, // never a routine). A routine picker is deliberately not built: with // exactly one candidate workflow, offering a picker over a list of one // would be UI theater, not a real choice. diff --git a/packages/webhook-triggers/src/repo-review-lease.ts b/packages/webhook-triggers/src/repo-review-lease.ts index 6fa2b18ef..2238f5720 100644 --- a/packages/webhook-triggers/src/repo-review-lease.ts +++ b/packages/webhook-triggers/src/repo-review-lease.ts @@ -1,6 +1,6 @@ // Closes a check-then-act race in the GitHub connect card's // start-reviewing step (CL-7242): `startReviewingRepos` -// (`@corbits/workflow-catalog`) loops over selected repos doing +// (`@workbench/connections`) loops over selected repos doing // hasRepoGrant/mintRepoGrant then hasWebhookTrigger/createWebhookTrigger // per repo, each a plain read followed by a conditional write with no // atomic backstop. Two concurrent calls for the same repo (a diff --git a/templates/LICENSE b/templates/LICENSE new file mode 100644 index 000000000..c6487f4fd --- /dev/null +++ b/templates/LICENSE @@ -0,0 +1,176 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. + +This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. + +Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. + +We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. + +The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. + +GNU LESSER GENERAL PUBLIC LICENSE +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. + +However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. + +When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. + +If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: + + a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. + + e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. + + b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). + +To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + + This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice +That's all there is to it! diff --git a/templates/README.md b/templates/README.md new file mode 100644 index 000000000..fea840dd7 --- /dev/null +++ b/templates/README.md @@ -0,0 +1,16 @@ +# Workbench templates + +Each subdirectory is one shipped `WorkbenchDefinition`: the agents, +routines, webhook triggers, plugins, and onboarding walkthrough a +"pick a kind of workbench" row creates. `index.ts` is the single +description both `apps/web`'s picker and `./instantiate.ts` read — the +schema, the assembled `WORKBENCH_TEMPLATES` list, and the +parse/serialize helpers a bench-library row round-trips through. + +- `gtm/` — the go-to-market workbench (CL-6349) +- `code-review/` — three reviewer lenses over every pull request +- `due-diligence/` — Scout plus Myra for research and follow-up + +`./instantiate.ts` resolves a definition against a bench over injected +ports; `./settings.ts` is the `template/*` settings vocabulary a room +persists about the template it was minted from. diff --git a/templates/code-review/index.ts b/templates/code-review/index.ts new file mode 100644 index 000000000..a8a75ded9 --- /dev/null +++ b/templates/code-review/index.ts @@ -0,0 +1,74 @@ +// The code-review definition: three reviewer lenses over every pull +// request. Its blocks install the one `code-review` workflow; the +// reviewer roster itself is `@corbits/code-review`'s own +// `CODE_REVIEW_REVIEWERS` — mirrored into `agents` here rather than +// duplicated, so a reviewer's handle, name, and one-line role can never +// drift between the package that runs the review and the definition +// that describes it. +import type { WorkbenchDefinition } from "../index"; +// Imported from the package's own `./reviewers` subpath, never its root +// (`@corbits/code-review`) — the root barrel also re-exports the review +// run and GitHub client, which pull in `@corbits/github-tools` and +// `@intx/agent`'s full provider surface. `reviewers.ts` itself has no +// imports at all, so this subpath keeps every consumer of this +// definition (this package's whole point) off that much heavier graph. +import { CODE_REVIEW_REVIEWERS } from "@corbits/code-review/reviewers"; + +export const CODE_REVIEW_TEMPLATE: WorkbenchDefinition = { + id: "code-review", + title: "Code review", + promise: + "Three reviewers read every pull request and post what they'd change.", + blocks: [{ assetName: "code-review", version: "0.0.1" }], + // GitHub is the one thing this template cannot work without: no + // repository, no diff to read and nowhere to post the review. + plugins: { required: ["github"], optional: [] }, + tools: ["@corbits/github-tools"], + routines: [], + webhookTriggers: [ + { + key: "pull-request-opened", + blockAssetName: "code-review", + label: "Review new pull requests", + why: "A review is worth most posted while the pull request is still open for comment, so this fires the moment GitHub says one exists rather than waiting on a clock.", + triggerFieldKey: "pullRequestUrl", + }, + ], + agents: [ + ...CODE_REVIEW_REVIEWERS.map((reviewer) => ({ + handle: reviewer.handle, + displayName: reviewer.displayName, + blockAssetName: "code-review", + role: reviewer.description, + })), + ], + openInputs: [ + { + key: "repos", + label: "Which repositories?", + placeholder: "corbitsdev/workbench", + help: "The GitHub repositories to watch. Every new pull request there gets reviewed.", + required: true, + appliesToWebhookTrigger: "pull-request-opened", + }, + ], + onboardingSteps: [ + { + kind: "connect-plugin", + connectorId: "github", + title: "Connect GitHub", + why: "Create a token scoped to the repositories you want reviewed — that is what the reviewers will be able to read.", + }, + { + kind: "pick-github-repos", + title: "Choose what gets reviewed", + why: "Of the repositories your token reaches, these are the ones a new pull request starts a review in.", + }, + { + kind: "start-webhook-trigger", + webhookTriggerKey: "pull-request-opened", + title: "Start reviewing", + why: "From now on, every new pull request in those repositories gets a review.", + }, + ], +}; diff --git a/templates/due-diligence/index.ts b/templates/due-diligence/index.ts new file mode 100644 index 000000000..87e8f5690 --- /dev/null +++ b/templates/due-diligence/index.ts @@ -0,0 +1,40 @@ +// The due-diligence template (CL-6499): Scout for the web/firm-memory +// research and Myra to talk through what it found. Scout is a +// standalone chat agent, not a lens over a block workflow — it has no +// cron, no webhook, nothing to schedule — so it carries no +// `blockAssetName` and this template's `blocks` list stays empty. +// Exa (Scout's web-research tool) resolves through the keyless MCP +// preset, so nothing here blocks the create on a connection. +import type { WorkbenchDefinition } from "../index"; +import { + SCOUT_AGENT_HANDLE, + SCOUT_AGENT_DISPLAY_NAME, + SCOUT_AGENT_DESCRIPTION, + SCOUT_TOOL_PACKAGE_PINS, +} from "@corbits/scout-agent/definition"; + +export const DUE_DILIGENCE_TEMPLATE: WorkbenchDefinition = { + id: "due-diligence", + title: "Due Diligence", + promise: + "Scout checks a company, deal, or vendor against the web and what your team already knows, and saves what it finds so you can pick it up later.", + blocks: [], + plugins: { required: [], optional: ["exa"] }, + tools: SCOUT_TOOL_PACKAGE_PINS.map((pin) => pin.name), + routines: [], + webhookTriggers: [], + agents: [ + { + handle: "myra", + displayName: "Myra", + role: "Talks through what Scout found and helps you decide what to do with it.", + }, + { + handle: SCOUT_AGENT_HANDLE, + displayName: SCOUT_AGENT_DISPLAY_NAME, + role: SCOUT_AGENT_DESCRIPTION, + }, + ], + openInputs: [], + onboardingSteps: [], +}; diff --git a/templates/gtm/index.ts b/templates/gtm/index.ts new file mode 100644 index 000000000..4d5c1fe06 --- /dev/null +++ b/templates/gtm/index.ts @@ -0,0 +1,97 @@ +// The GTM template (CL-6349): the go-to-market workbench, ported from the +// OG gtm-workbench's four v1 workflows. +// +// The call backbone (granola-call discovering new calls, process-granola-call +// writing each one up) is what makes the workbench useful on day one, so it +// is the routine that runs on a clock without anyone asking. The web watch +// runs on its own weekly clock. The CRM agent and the collateral drafter are +// on-demand: both start from a specific thing a person points at. +import type { WorkbenchDefinition } from "../index"; + +export const GTM_TEMPLATE: WorkbenchDefinition = { + id: "gtm", + title: "Go to market", + promise: + "Your calls get written up, your CRM tasks get worked, and the web gets watched — you approve anything that leaves the room.", + blocks: [ + { assetName: "granola-call", version: "0.0.1" }, + { assetName: "process-granola-call", version: "0.0.1" }, + { assetName: "attio-task-agent", version: "0.0.1" }, + { assetName: "exa-topic-watch", version: "0.0.1" }, + { assetName: "pain-point-collateral", version: "0.0.1" }, + ], + // Attio and Exa are hard requirements: the CRM agent has nothing to + // work without Attio, and the web watch has nothing to read without + // Exa. Granola is what the call backbone runs on — the create flow + // offers it up front, but a workbench with the CRM agent and the web + // watch alone is still a real workbench, so it never blocks the create. + plugins: { required: ["attio", "exa"], optional: ["granola"] }, + tools: [], + webhookTriggers: [], + routines: [ + { + key: "call-discovery", + blockAssetName: "granola-call", + label: "Check for new calls", + cron: "*/30 9-18 * * 1-5", + why: "Notes are worth most right after the call, so this looks for new ones through the working day rather than once overnight.", + }, + { + key: "topic-watch", + blockAssetName: "exa-topic-watch", + label: "Watch the web", + cron: "0 8 * * 1", + why: "One digest at the start of the week, so a quiet week reads as quiet instead of as five empty runs.", + }, + ], + agents: [ + { + handle: "crm", + displayName: "CRM task agent", + blockAssetName: "attio-task-agent", + role: "Point it at a CRM task and it works the task — reading first, drafting second, and asking before it writes anything back.", + }, + { + handle: "collateral", + displayName: "Pain-point collateral", + blockAssetName: "pain-point-collateral", + role: "Give it a call transcript and it drafts a piece aimed at the pain point the customer actually named.", + }, + ], + openInputs: [ + { + key: "topic", + label: "What should we watch?", + placeholder: "AI coding agents for go-to-market", + help: "One topic, as specific as you can make it. The weekly digest covers this and nothing else.", + required: true, + appliesToRoutine: "topic-watch", + }, + ], + // GTM is not offered by the picker yet, and both its instantiation + // paths say so out loud rather than half-creating a workbench: + // `instantiateWorkbenchTemplate` throws because its agents are lenses + // over deployed workflow definitions with no agent-directory create + // request, and the web `beginOnboarding` binding throws because + // neither Attio nor Exa has an in-room onboarding card. + onboardingSteps: [ + { + kind: "connect-plugin", + connectorId: "attio", + title: "Connect Attio", + why: "The CRM agent reads and works your tasks in Attio — without it there is nothing to work.", + }, + { + kind: "connect-plugin", + connectorId: "exa", + title: "Connect Exa", + why: "The weekly web watch reads the open web through Exa.", + }, + { + kind: "connect-plugin", + connectorId: "granola", + title: "Connect Granola", + why: "Connect it and every new call gets written up on its own; skip it and the rest of the workbench still works.", + }, + ], +}; diff --git a/templates/index.ts b/templates/index.ts new file mode 100644 index 000000000..5281ab3fc --- /dev/null +++ b/templates/index.ts @@ -0,0 +1,472 @@ +// Workbench definitions: the single description of what "pick a kind of +// workbench" actually creates — its default agents, routines, tools, +// plugins, and the ordered onboarding walkthrough a person works +// through once the room exists. +// +// `@corbits/workflows/catalog`'s `WORKFLOW_CATALOG` describes ONE +// workflow at a time — what it does, what it needs connected, what its +// trigger carries. A definition is the layer above: a named workbench +// worth having, assembled out of several of those workflows. The three +// shipped definitions (one per subdirectory here) are the bench +// library's templates; a *template* is a shipped definition, not a +// second kind of thing. +// +// Everything here is pure data. Creating a workbench from a definition +// is a host concern (`apps/web`'s picker starts the flow, `./instantiate` +// resolves it); this module is the single description both the picker +// and the creator read, so neither hand-types an asset name, a cron, a +// connector id, or a step of onboarding copy. +// +// Blocks are referenced by asset name AND version. A definition names +// the exact `workflows//package.json` version it was designed +// against, so bumping a workflow is a deliberate edit here rather than +// a silent change in what it creates. + +import { type } from "arktype"; + +/** One workflow a template installs, pinned to the version it was + * designed against. `assetName` matches a `WORKFLOW_CATALOG` entry. */ +export const WorkbenchTemplateBlock = type({ + assetName: "string > 0", + version: "/^[0-9]+\\.[0-9]+\\.[0-9]+$/", +}); +export type WorkbenchTemplateBlock = typeof WorkbenchTemplateBlock.infer; + +/** + * One routine a template creates on install. `cron` is a 5-field + * expression in the grammar `@corbits/routines`' `cron.ts` speaks — the + * authority on whether one is valid and when it fires; the shape check + * here only catches a malformed literal at module load. + */ +export const WorkbenchTemplateRoutine = type({ + /** Stable key an open input can point at. Unique within a template. */ + key: "/^[a-z][a-z0-9-]*$/", + /** Which block runs on this schedule. */ + blockAssetName: "string > 0", + /** What the person sees this routine called. */ + label: "string > 0", + cron: "/^\\S+ \\S+ \\S+ \\S+ \\S+$/", + /** One honest line: why this runs on a clock at all. */ + why: "string > 0", +}); +export type WorkbenchTemplateRoutine = typeof WorkbenchTemplateRoutine.infer; + +/** + * One agent a person can address in the created workbench. `handle` is + * what they type to reach it. `blockAssetName` names the workflow behind + * it when the agent is a lens over one of the definition's own blocks + * (the code-review reviewers); it is absent for an agent that is a + * standalone chat agent installed straight through the agent-directory + * create path (Scout, Jimmy) with no block of its own to reference. + */ +export const WorkbenchDefinitionAgent = type({ + handle: "/^[a-z][a-z0-9-]*$/", + displayName: "string > 0", + "blockAssetName?": "string > 0", + /** One honest line: what this agent is for. */ + role: "string > 0", +}); +export type WorkbenchDefinitionAgent = typeof WorkbenchDefinitionAgent.infer; + +/** + * One workflow a template fires from an inbound webhook rather than a + * clock — the PR-review trigger a code-review template needs, as opposed + * to `WorkbenchTemplateRoutine`'s cron-scheduled kind. `triggerFieldKey` + * names the block's own `WorkflowCatalogEntry.triggerFields` entry the + * webhook payload fills — see `@corbits/workflows/catalog`'s + * `WorkflowTriggerField`. Creating the live `webhook_trigger` row itself + * (`@corbits/webhook-triggers`) needs a repo to scope it to, which only + * exists once the person has answered the template's own open input for + * it — this is the spec a create flow resolves against that answer, not + * the row. + */ +export const WorkbenchTemplateWebhookTrigger = type({ + /** Stable key an open input can point at. Unique within a template. */ + key: "/^[a-z][a-z0-9-]*$/", + /** Which block this webhook launches a run of. */ + blockAssetName: "string > 0", + /** What the person sees this trigger called. */ + label: "string > 0", + /** One honest line: why this fires on a webhook instead of a clock. */ + why: "string > 0", + triggerFieldKey: "/^[a-zA-Z][a-zA-Z0-9]*$/", +}); +export type WorkbenchTemplateWebhookTrigger = + typeof WorkbenchTemplateWebhookTrigger.infer; + +/** + * One answer the template cannot supply for the person — the questions + * the create flow asks before anything runs. Exactly one of + * `appliesToRoutine` / `appliesToWebhookTrigger` names the trigger this + * input's answer feeds: a cron-scheduled routine, or a webhook trigger + * spec. + */ +export const WorkbenchTemplateOpenInput = type({ + key: "/^[a-zA-Z][a-zA-Z0-9]*$/", + label: "string > 0", + "placeholder?": "string", + help: "string > 0", + required: "boolean", + "appliesToRoutine?": "/^[a-z][a-z0-9-]*$/", + "appliesToWebhookTrigger?": "/^[a-z][a-z0-9-]*$/", +}); +export type WorkbenchTemplateOpenInput = + typeof WorkbenchTemplateOpenInput.infer; + +/** + * One step in a definition's onboarding walkthrough, in the order a + * person works through it. This is the *definition-level* step — the + * full ordered walkthrough a template describes. `@corbits/chat`'s own + * `WorkbenchOnboardingStep` is the narrower wire-level body a host + * posts into a room to raise one card; the two are different types and + * neither is derived from the other. + */ +export const WorkbenchOnboardingStep = type({ + kind: "'connect-plugin'", + connectorId: "string > 0", + title: "string > 0", + why: "string > 0", +}) + .or({ kind: "'pick-github-repos'", title: "string > 0", why: "string > 0" }) + .or({ + kind: "'start-webhook-trigger'", + webhookTriggerKey: "/^[a-z][a-z0-9-]*$/", + title: "string > 0", + why: "string > 0", + }); +export type WorkbenchOnboardingStep = typeof WorkbenchOnboardingStep.infer; + +/** + * The full definition, as parsed back off a trust boundary — the bench + * library row a hub seeded (see `@corbits/artifacts-hub`'s template + * library) travels over HTTP before a picker instantiates from it, so + * it re-enters through this schema, never through `as`. + */ +export const WorkbenchDefinitionSchema = type({ + id: "/^[a-z][a-z0-9-]*$/", + title: "string > 0", + promise: "string > 0", + blocks: WorkbenchTemplateBlock.array(), + plugins: { required: "string[]", optional: "string[]" }, + tools: "string[]", + routines: WorkbenchTemplateRoutine.array(), + webhookTriggers: WorkbenchTemplateWebhookTrigger.array(), + agents: WorkbenchDefinitionAgent.array(), + openInputs: WorkbenchTemplateOpenInput.array(), + onboardingSteps: WorkbenchOnboardingStep.array(), +}); + +export type WorkbenchDefinition = { + readonly id: string; + /** What the picker row calls it. */ + readonly title: string; + /** The picker row's one-line promise, in the reader's language. */ + readonly promise: string; + readonly blocks: readonly WorkbenchTemplateBlock[]; + /** + * Connector ids (see `@workbench/connections`' `CONNECTOR_REGISTRY` and + * `MCP_PRESETS`): `required` is what this definition cannot work + * without, in the order the walkthrough asks for them; `optional` + * makes it better and never gates the create. + */ + readonly plugins: { + readonly required: readonly string[]; + readonly optional: readonly string[]; + }; + /** Tool-package names this definition's agents need — package names + * only, never `{name, version}` pins: the pinned version lives with + * the agent package that owns the tool. */ + readonly tools: readonly string[]; + readonly routines: readonly WorkbenchTemplateRoutine[]; + /** Webhook-fired triggers this definition installs — empty for a + * clock-only definition like GTM. */ + readonly webhookTriggers: readonly WorkbenchTemplateWebhookTrigger[]; + readonly agents: readonly WorkbenchDefinitionAgent[]; + readonly openInputs: readonly WorkbenchTemplateOpenInput[]; + /** The ordered walkthrough a freshly created workbench runs — connect + * the plugin, pick what it works on, start the trigger. */ + readonly onboardingSteps: readonly WorkbenchOnboardingStep[]; +}; + +export { GTM_TEMPLATE } from "./gtm"; +export { CODE_REVIEW_TEMPLATE } from "./code-review"; +export { DUE_DILIGENCE_TEMPLATE } from "./due-diligence"; +export { + instantiateWorkbenchTemplate, + type ParticipantAgentRequest, + type WorkbenchTemplateInstantiationPorts, + type WorkbenchTemplateInstantiationResult, +} from "./instantiate"; +export { + jimmyAgentRequest, + scoutAgentRequest, +} from "./participant-agent-requests"; +export { + TemplateReposSettingsPatch, + TemplateSettingsPatch, + templateReposSettingsPatch, + templateSettingsPatch, +} from "./settings"; +export { + WorkflowTriggerField, + WORKFLOW_CATALOG, + isAutomatableWorkflowName, + isConversationalWorkflowName, + deliveryWorkbenchRequiredForWorkflowName, + workflowCatalogEntry, + workflowDisplayName, + validateTriggerFieldsAtCreate, + type WorkflowCatalogEntry, + type TriggerFieldsValidation, +} from "./catalog"; + +import { GTM_TEMPLATE } from "./gtm"; +import { CODE_REVIEW_TEMPLATE } from "./code-review"; +import { DUE_DILIGENCE_TEMPLATE } from "./due-diligence"; + +export const WORKBENCH_TEMPLATES: readonly WorkbenchDefinition[] = [ + GTM_TEMPLATE, + CODE_REVIEW_TEMPLATE, + DUE_DILIGENCE_TEMPLATE, +]; + +const templateById = new Map( + WORKBENCH_TEMPLATES.map((template) => [template.id, template]), +); + +export function workbenchTemplate(id: string): WorkbenchDefinition | undefined { + return templateById.get(id); +} + +/** + * Every asset name a definition names — its blocks, and the block behind + * each routine and agent. A caller checking a definition against + * `WORKFLOW_CATALOG` walks this rather than three separate arrays. + */ +export function templateBlockAssetNames( + definition: WorkbenchDefinition, +): readonly string[] { + return definition.blocks.map((block) => block.assetName); +} + +/** The shipped definitions as bench-library seed entries — the ONE + * serialization every seeder uses (`apps/hub`'s boot seed and the eval + * harness's scratch-hub seed), so the two can never drift. */ +export function workbenchTemplateLibraryEntries(): readonly { + readonly id: string; + readonly content: string; +}[] { + return WORKBENCH_TEMPLATES.map((template) => ({ + id: template.id, + content: serializeWorkbenchDefinition(template), + })); +} + +export function serializeWorkbenchDefinition( + definition: WorkbenchDefinition, +): string { + return JSON.stringify(definition, null, 2); +} + +/** + * Parses a seeded library row's content back into a definition, running + * the same cross-reference checks module load runs on the shipped + * constants. Throws on anything malformed — an unreadable library row + * is a seeding defect to surface, never a shape to limp past. + */ +export function parseWorkbenchDefinition(data: unknown): WorkbenchDefinition { + const raw = typeof data === "string" ? JSON.parse(data) : data; + const parsed = WorkbenchDefinitionSchema(raw); + if (parsed instanceof type.errors) { + throw new Error(`workbench definition failed to parse: ${parsed.summary}`); + } + assertValid(parsed); + return parsed; +} + +function assertValid(definition: WorkbenchDefinition): void { + const blockNames = new Set(templateBlockAssetNames(definition)); + const parsedBlocks = WorkbenchTemplateBlock.array()(definition.blocks); + if (parsedBlocks instanceof type.errors) { + throw new Error( + `workbench definition "${definition.id}" has an invalid blocks shape: ${parsedBlocks.summary}`, + ); + } + const parsedRoutines = WorkbenchTemplateRoutine.array()(definition.routines); + if (parsedRoutines instanceof type.errors) { + throw new Error( + `workbench definition "${definition.id}" has an invalid routines shape: ${parsedRoutines.summary}`, + ); + } + const parsedAgents = WorkbenchDefinitionAgent.array()(definition.agents); + if (parsedAgents instanceof type.errors) { + throw new Error( + `workbench definition "${definition.id}" has an invalid agents shape: ${parsedAgents.summary}`, + ); + } + const parsedInputs = WorkbenchTemplateOpenInput.array()( + definition.openInputs, + ); + if (parsedInputs instanceof type.errors) { + throw new Error( + `workbench definition "${definition.id}" has an invalid openInputs shape: ${parsedInputs.summary}`, + ); + } + const parsedWebhookTriggers = WorkbenchTemplateWebhookTrigger.array()( + definition.webhookTriggers, + ); + if (parsedWebhookTriggers instanceof type.errors) { + throw new Error( + `workbench definition "${definition.id}" has an invalid webhookTriggers shape: ${parsedWebhookTriggers.summary}`, + ); + } + const parsedSteps = WorkbenchOnboardingStep.array()( + definition.onboardingSteps, + ); + if (parsedSteps instanceof type.errors) { + throw new Error( + `workbench definition "${definition.id}" has an invalid onboardingSteps shape: ${parsedSteps.summary}`, + ); + } + if (new Set(definition.tools).size !== definition.tools.length) { + throw new Error( + `workbench definition "${definition.id}" names the same tool package more than once`, + ); + } + const routineKeys = new Set( + definition.routines.map((routine) => routine.key), + ); + for (const routine of definition.routines) { + if (!blockNames.has(routine.blockAssetName)) { + throw new Error( + `workbench definition "${definition.id}" routine "${routine.key}" runs "${routine.blockAssetName}", which the definition does not install`, + ); + } + } + const webhookTriggerKeys = new Set( + definition.webhookTriggers.map((trigger) => trigger.key), + ); + for (const trigger of definition.webhookTriggers) { + if (!blockNames.has(trigger.blockAssetName)) { + throw new Error( + `workbench definition "${definition.id}" webhook trigger "${trigger.key}" fires "${trigger.blockAssetName}", which the definition does not install`, + ); + } + } + for (const agent of definition.agents) { + if ( + agent.blockAssetName !== undefined && + !blockNames.has(agent.blockAssetName) + ) { + throw new Error( + `workbench definition "${definition.id}" agent "${agent.handle}" is backed by "${agent.blockAssetName}", which the definition does not install`, + ); + } + } + for (const input of definition.openInputs) { + const appliesToCount = + Number(input.appliesToRoutine !== undefined) + + Number(input.appliesToWebhookTrigger !== undefined); + if (appliesToCount !== 1) { + throw new Error( + `workbench definition "${definition.id}" input "${input.key}" must apply to exactly one of a routine or a webhook trigger`, + ); + } + if ( + input.appliesToRoutine !== undefined && + !routineKeys.has(input.appliesToRoutine) + ) { + throw new Error( + `workbench definition "${definition.id}" input "${input.key}" applies to routine "${input.appliesToRoutine}", which the definition does not create`, + ); + } + if ( + input.appliesToWebhookTrigger !== undefined && + !webhookTriggerKeys.has(input.appliesToWebhookTrigger) + ) { + throw new Error( + `workbench definition "${definition.id}" input "${input.key}" applies to webhook trigger "${input.appliesToWebhookTrigger}", which the definition does not create`, + ); + } + } + assertOnboardingWalkthrough(definition); +} + +/** + * The walkthrough is ordered, so its checks are ordered too: a step can + * only ask for something an earlier step made possible, and a required + * plugin with no step to connect it is a definition that promises setup + * it never asks for. + */ +function assertOnboardingWalkthrough(definition: WorkbenchDefinition): void { + const declaredPlugins = new Set([ + ...definition.plugins.required, + ...definition.plugins.optional, + ]); + const connectedSoFar = new Set(); + const startedTriggerKeys = new Set(); + let pickedRepos = false; + + for (const step of definition.onboardingSteps) { + if (step.kind === "connect-plugin") { + if (!declaredPlugins.has(step.connectorId)) { + throw new Error( + `workbench definition "${definition.id}" onboarding connects "${step.connectorId}", which is not one of its plugins`, + ); + } + connectedSoFar.add(step.connectorId); + continue; + } + if (step.kind === "pick-github-repos") { + if (!connectedSoFar.has("github")) { + throw new Error( + `workbench definition "${definition.id}" asks for repos before its onboarding connects "github"`, + ); + } + pickedRepos = true; + continue; + } + if (!webhookTriggerKeyExists(definition, step.webhookTriggerKey)) { + throw new Error( + `workbench definition "${definition.id}" onboarding starts webhook trigger "${step.webhookTriggerKey}", which the definition does not create`, + ); + } + if (startedTriggerKeys.has(step.webhookTriggerKey)) { + throw new Error( + `workbench definition "${definition.id}" onboarding starts webhook trigger "${step.webhookTriggerKey}" more than once`, + ); + } + if (!pickedRepos) { + throw new Error( + `workbench definition "${definition.id}" starts webhook trigger "${step.webhookTriggerKey}" before its onboarding picks repos`, + ); + } + startedTriggerKeys.add(step.webhookTriggerKey); + } + + for (const connectorId of definition.plugins.required) { + if (!connectedSoFar.has(connectorId)) { + throw new Error( + `workbench definition "${definition.id}" requires plugin "${connectorId}" but its onboarding never asks anyone to connect it`, + ); + } + } + for (const trigger of definition.webhookTriggers) { + if (!startedTriggerKeys.has(trigger.key)) { + throw new Error( + `workbench definition "${definition.id}" webhook trigger "${trigger.key}" is never started by an onboarding step`, + ); + } + } +} + +function webhookTriggerKeyExists( + definition: WorkbenchDefinition, + key: string, +): boolean { + return definition.webhookTriggers.some((trigger) => trigger.key === key); +} + +for (const definition of WORKBENCH_TEMPLATES) { + assertValid(definition); +} diff --git a/templates/instantiate.test.ts b/templates/instantiate.test.ts new file mode 100644 index 000000000..45f084f6f --- /dev/null +++ b/templates/instantiate.test.ts @@ -0,0 +1,288 @@ +import { expect, test } from "bun:test"; +import { CODE_REVIEW_REVIEWERS } from "@corbits/code-review/reviewers"; + +import { + CODE_REVIEW_TEMPLATE, + DUE_DILIGENCE_TEMPLATE, + GTM_TEMPLATE, + type WorkbenchOnboardingStep, +} from "./index"; +import { + instantiateWorkbenchTemplate, + type WorkbenchTemplateInstantiationPorts, +} from "./instantiate"; + +function fakePorts( + existingHandles: readonly string[] = [], + alreadyDeployedBlocks: readonly string[] = [], +): WorkbenchTemplateInstantiationPorts & { + readonly created: string[]; + readonly invited: string[]; + readonly recordedConnections: (readonly string[])[]; + readonly deployedBlocks: string[]; + readonly calls: string[]; + readonly onboardingSteps: (readonly WorkbenchOnboardingStep[])[]; +} { + const created: string[] = []; + const invited: string[] = []; + const recordedConnections: (readonly string[])[] = []; + const deployedBlocks: string[] = []; + const calls: string[] = []; + const onboardingSteps: (readonly WorkbenchOnboardingStep[])[] = []; + return { + created, + invited, + recordedConnections, + deployedBlocks, + calls, + onboardingSteps, + async listAgentHandles() { + return existingHandles.map((handle) => ({ handle, id: `def-${handle}` })); + }, + async createParticipantAgent(request) { + created.push(request.handle); + return { id: `def-${request.handle}` }; + }, + async deployBlockWorkflow(block) { + deployedBlocks.push(block.assetName); + return { created: !alreadyDeployedBlocks.includes(block.assetName) }; + }, + async inviteParticipantAgent(id) { + invited.push(id); + calls.push(`invite:${id}`); + }, + async recordPendingConnections(pendingConnections) { + recordedConnections.push(pendingConnections); + calls.push("recordPendingConnections"); + }, + async beginOnboarding(steps) { + onboardingSteps.push(steps); + calls.push("beginOnboarding"); + }, + }; +} + +test("instantiating the code-review definition creates the three reviewer definitions, never Myra", () => { + const ports = fakePorts(); + return instantiateWorkbenchTemplate(CODE_REVIEW_TEMPLATE, ports).then( + (result) => { + expect(result.createdHandles).toEqual( + CODE_REVIEW_REVIEWERS.map((reviewer) => reviewer.handle), + ); + expect(ports.created).toEqual( + CODE_REVIEW_REVIEWERS.map((reviewer) => reviewer.handle), + ); + expect(ports.created).not.toContain("myra"); + expect(result.skippedHandles).toEqual([]); + }, + ); +}); + +test("instantiating the code-review template records its required connections as pending", async () => { + const ports = fakePorts(); + const result = await instantiateWorkbenchTemplate( + CODE_REVIEW_TEMPLATE, + ports, + ); + expect(result.pendingConnections).toEqual(["github"]); + expect(ports.recordedConnections).toEqual([["github"]]); +}); + +test("instantiating the code-review template skips a reviewer that already exists", async () => { + const ports = fakePorts(["architecture-reviewer"]); + const result = await instantiateWorkbenchTemplate( + CODE_REVIEW_TEMPLATE, + ports, + ); + expect(result.skippedHandles).toEqual(["architecture-reviewer"]); + expect(result.createdHandles).toEqual([ + "correctness-reviewer", + "release-risk-reviewer", + ]); + expect(ports.created).not.toContain("architecture-reviewer"); +}); + +// A reviewer whose agent-directory definition already exists (a second +// workbench from the same template) still has to become a participant +// of THIS new room — an existing definition is not an existing +// invitation, so skipping the create must never skip the invite too. +test("instantiating the code-review definition invites every reviewer into the room, created or skipped alike", async () => { + const ports = fakePorts(["architecture-reviewer"]); + const result = await instantiateWorkbenchTemplate( + CODE_REVIEW_TEMPLATE, + ports, + ); + expect(result.invitedHandles).toEqual( + CODE_REVIEW_REVIEWERS.map((reviewer) => reviewer.handle), + ); + expect(ports.invited).toEqual( + expect.arrayContaining([ + "def-architecture-reviewer", + "def-correctness-reviewer", + "def-release-risk-reviewer", + ]), + ); + expect(ports.invited).toHaveLength(3); +}); + +test("the code-review definition never invites or creates Myra — nobody hosts a template room", async () => { + const ports = fakePorts(["assistant"]); + const result = await instantiateWorkbenchTemplate( + CODE_REVIEW_TEMPLATE, + ports, + ); + expect(ports.invited).not.toContain("def-assistant"); + expect(ports.created).not.toContain("myra"); + expect(ports.created).not.toContain("assistant"); + expect(result.invitedHandles).not.toContain("myra"); +}); + +test("the due-diligence definition still invites an existing assistant as Myra", async () => { + const ports = fakePorts(["assistant"]); + const result = await instantiateWorkbenchTemplate( + DUE_DILIGENCE_TEMPLATE, + ports, + ); + expect(ports.invited).toContain("def-assistant"); + expect(ports.created).not.toContain("myra"); + expect(result.invitedHandles).toEqual(["myra", "scout"]); +}); + +test("instantiating the code-review definition begins its walkthrough, in definition order, once everything else is in place", async () => { + const ports = fakePorts(); + const result = await instantiateWorkbenchTemplate( + CODE_REVIEW_TEMPLATE, + ports, + ); + expect(ports.onboardingSteps).toEqual([CODE_REVIEW_TEMPLATE.onboardingSteps]); + expect(result.onboardingSteps).toEqual(CODE_REVIEW_TEMPLATE.onboardingSteps); + expect(ports.calls.at(-1)).toBe("beginOnboarding"); + expect(ports.calls.at(-2)).toBe("recordPendingConnections"); + expect(ports.calls.filter((call) => call.startsWith("invite:"))).toHaveLength( + 3, + ); +}); + +test("a definition with no onboarding steps never begins a walkthrough", async () => { + const ports = fakePorts(); + const result = await instantiateWorkbenchTemplate( + DUE_DILIGENCE_TEMPLATE, + ports, + ); + expect(ports.onboardingSteps).toEqual([]); + expect(ports.calls).not.toContain("beginOnboarding"); + expect(result.onboardingSteps).toEqual([]); +}); + +test("instantiating the code-review template deploys its referenced code-review block workflow", async () => { + const ports = fakePorts(); + const result = await instantiateWorkbenchTemplate( + CODE_REVIEW_TEMPLATE, + ports, + ); + expect(ports.deployedBlocks).toEqual(["code-review"]); + expect(result.deployedBlockAssetNames).toEqual(["code-review"]); + expect(result.skippedBlockAssetNames).toEqual([]); +}); + +test("a block workflow the tenant already deployed is reported as skipped, never re-deployed", async () => { + const ports = fakePorts([], ["code-review"]); + const result = await instantiateWorkbenchTemplate( + CODE_REVIEW_TEMPLATE, + ports, + ); + expect(result.deployedBlockAssetNames).toEqual([]); + expect(result.skippedBlockAssetNames).toEqual(["code-review"]); +}); + +test("instantiating a definition with an agent outside the reviewer roster throws rather than silently skipping it", () => { + return expect( + instantiateWorkbenchTemplate(GTM_TEMPLATE, fakePorts()), + ).rejects.toThrow(/has no known create-agent request/); +}); + +test("instantiating the due-diligence template creates Scout, never Myra, with no credential connected", async () => { + const ports = fakePorts(); + const result = await instantiateWorkbenchTemplate( + DUE_DILIGENCE_TEMPLATE, + ports, + ); + expect(result.createdHandles).toEqual(["scout"]); + expect(ports.created).toEqual(["scout"]); + expect(ports.created).not.toContain("myra"); + expect(result.skippedHandles).toEqual([]); + // No block to deploy and nothing required up front: seeding never + // fails for want of a connected credential (Exa's MCP preset is + // keyless, and this template requires nothing at all). + expect(ports.deployedBlocks).toEqual([]); + expect(result.pendingConnections).toEqual([]); +}); + +test("instantiating the due-diligence template twice never creates Scout a second time", async () => { + const first = fakePorts(); + await instantiateWorkbenchTemplate(DUE_DILIGENCE_TEMPLATE, first); + const second = fakePorts(["scout"]); + const result = await instantiateWorkbenchTemplate( + DUE_DILIGENCE_TEMPLATE, + second, + ); + expect(result.createdHandles).toEqual([]); + expect(result.skippedHandles).toEqual(["scout"]); + expect(second.created).toEqual([]); +}); + +test("Scout's create request carries its tool package pins", async () => { + const requests: { handle: string; toolPackagePins?: readonly string[] }[] = + []; + const ports: WorkbenchTemplateInstantiationPorts = { + async listAgentHandles() { + return []; + }, + async createParticipantAgent(request) { + requests.push(request); + return { id: `def-${request.handle}` }; + }, + async deployBlockWorkflow() { + return { created: true }; + }, + async inviteParticipantAgent() { + /* noop */ + }, + async recordPendingConnections() { + /* noop */ + }, + async beginOnboarding() { + /* noop */ + }, + }; + await instantiateWorkbenchTemplate(DUE_DILIGENCE_TEMPLATE, ports); + const scout = requests.find((request) => request.handle === "scout"); + expect(scout?.toolPackagePins).toEqual( + expect.arrayContaining([ + "@corbits/memory-tools", + "@corbits/web-search-tools", + "@corbits/scout-agent", + ]), + ); +}); + +// No shipped definition names Jimmy (he is not a "kind of workbench"); +// `@corbits/chat-ui`'s "Add Jimmy" quick-create row calls +// `jimmyAgentRequest()` directly instead of going through one. This +// proves `instantiateWorkbenchTemplate`'s request map still resolves his +// handle, so a future template naming him works with no new plumbing. +test("a definition naming Jimmy's handle still resolves and creates him", async () => { + const definitionNamingJimmy = { + ...DUE_DILIGENCE_TEMPLATE, + agents: [ + { handle: "jimmy", displayName: "Jimmy", role: "Replies with a GIF." }, + ], + }; + const ports = fakePorts(); + const result = await instantiateWorkbenchTemplate( + definitionNamingJimmy, + ports, + ); + expect(result.createdHandles).toEqual(["jimmy"]); + expect(ports.created).toEqual(["jimmy"]); +}); diff --git a/templates/instantiate.ts b/templates/instantiate.ts new file mode 100644 index 000000000..a46b748a7 --- /dev/null +++ b/templates/instantiate.ts @@ -0,0 +1,203 @@ +// Turns a picked workbench definition into the state a freshly minted +// workbench needs: the agent definitions that don't already exist, the +// block workflows behind them, the still-pending plugin list the room +// persists, and the definition's ordered onboarding walkthrough handed +// to whatever surface runs it. Pure orchestration over injected ports — +// no HTTP, no store — so a host (today, `apps/web`'s +// `instant-agent-create.ts`) can bind the ports to its own REST clients +// and this stays testable with plain fakes. +// +// This resolves a definition whose non-Myra agents are backed by either +// `@corbits/code-review`'s reviewer roster (`CODE_REVIEW_TEMPLATE`) or a +// standalone chat agent this catalog installs the same way — Scout, for +// `DUE_DILIGENCE_TEMPLATE` (see `./participant-agent-requests.ts`). +// Jimmy resolves through the identical `ParticipantAgentRequest` shape +// (`jimmyAgentRequest()`), but no shipped definition names his handle +// today — he is not a "kind of workbench"; `@corbits/chat-ui`'s "Add +// Jimmy" quick-create row calls `jimmyAgentRequest()` directly instead. +// Kept registered here too so a future definition naming his handle +// resolves without new plumbing. +// +// A definition like `GTM_TEMPLATE`, whose agents are backed by their own +// deployed workflow definitions rather than an agent-directory create +// request, needs its own resolution path — calling this function against +// such a definition throws rather than silently doing nothing. +import { + codeReviewAgentRequests, + type CodeReviewAgentRequest, +} from "@corbits/code-review/agent-requests"; +import { + jimmyAgentRequest, + scoutAgentRequest, +} from "./participant-agent-requests"; + +import type { + WorkbenchDefinition, + WorkbenchOnboardingStep, + WorkbenchTemplateBlock, +} from "./index"; + +/** The agent-directory create-request shape every agent resolves + * to: `CodeReviewAgentRequest`'s own fields, plus the tool-package pins + * a tool-calling agent (Scout, Jimmy) needs and a pure-text + * reviewer does not. */ +export type ParticipantAgentRequest = CodeReviewAgentRequest & { + readonly toolPackagePins?: readonly string[]; +}; + +export interface WorkbenchTemplateInstantiationPorts { + /** Every agent definition already deployed in the bench, as + * `{handle, id}` pairs. Doubles as the idempotency check (re-running + * instantiation — a retried create, a second workbench from the same + * template — never double-creates a reviewer) and as the id source + * for `inviteParticipantAgent` below: an agent definition the tenant + * already has is not yet a member of a freshly minted room, so + * its id still has to reach the invite call even when this function + * skips creating it. */ + listAgentHandles(): Promise< + readonly { readonly handle: string; readonly id: string }[] + >; + /** The agent-directory create path (`POST /agent-definitions`), or a + * fake of it in tests. */ + createParticipantAgent( + request: ParticipantAgentRequest, + ): Promise<{ readonly id: string }>; + /** Deploys one of the definition's referenced block workflows through + * the same source-form deploy the agent definitions use + * (`POST /template-blocks/:assetName/deploy` — see + * `./template-block-routes.ts`), or a fake of it in tests. `created` + * is `false` when the tenant already carries a deployed definition + * under this asset name, so a retried instantiation never + * double-deploys. */ + deployBlockWorkflow( + block: WorkbenchTemplateBlock, + ): Promise<{ readonly created: boolean }>; + /** Adds one agent definition to the newly created + * workbench's room (`POST /workbenches/:id/invite` — + * `@corbits/chat-ui`'s `inviteAgent`), or a fake of it in tests. This + * is what makes a definition's roster actually present in the room + * rather than merely registered in the agent directory. Called for + * Myra too: she is an existing principal invited after mint, not + * the room's host `definitionId`. */ + inviteParticipantAgent(id: string): Promise; + /** Persists the room's still-needed connections — the workbench + * settings `template/pendingConnections` key today; see + * `apps/web/src/instant-agent-create.ts`. */ + recordPendingConnections( + pendingConnections: readonly string[], + ): Promise; + /** Runs the definition's onboarding walkthrough in the freshly minted + * room — `apps/web` posts the first step's card through + * `postWorkbenchOnboardingStep`. Called last, and only when the + * definition has steps at all. */ + beginOnboarding(steps: readonly WorkbenchOnboardingStep[]): Promise; +} + +export interface WorkbenchTemplateInstantiationResult { + readonly createdHandles: readonly string[]; + readonly skippedHandles: readonly string[]; + /** Every non-Myra agent handle actually added to the room — + * `createdHandles` and `skippedHandles` combined, in definition order. + * A caller proving the roster a definition promises is really present + * checks this, not just that the definitions exist. */ + readonly invitedHandles: readonly string[]; + /** Block workflows `deployBlockWorkflow` actually deployed on this + * run, by asset name; a block the tenant already carried lands in + * `skippedBlockAssetNames` instead. */ + readonly deployedBlockAssetNames: readonly string[]; + readonly skippedBlockAssetNames: readonly string[]; + readonly pendingConnections: readonly string[]; + /** The ordered walkthrough handed to `beginOnboarding` — empty for a + * definition with no onboarding at all. */ + readonly onboardingSteps: readonly WorkbenchOnboardingStep[]; +} + +/** + * Resolves `definition` against the bench: creates the agent + * definitions that don't already exist (Myra is never re-created — she + * is the bench's seeded default setup agent, reused as-is and invited + * into the new channel), records the definition's required plugins as + * still pending, and then begins its onboarding walkthrough. Never + * registers a live webhook trigger itself: that is what the + * walkthrough's own start-reviewing step does, once the person has + * picked repos. + */ +export async function instantiateWorkbenchTemplate( + definition: WorkbenchDefinition, + ports: WorkbenchTemplateInstantiationPorts, +): Promise { + const existingIdsByHandle = new Map( + (await ports.listAgentHandles()).map((agent) => [agent.handle, agent.id]), + ); + const assistantId = existingIdsByHandle.get("assistant"); + if (assistantId !== undefined && !existingIdsByHandle.has("myra")) { + existingIdsByHandle.set("myra", assistantId); + } + const requestsByHandle = new Map( + [ + ...codeReviewAgentRequests(), + scoutAgentRequest(), + jimmyAgentRequest(), + ].map((request) => [request.handle, request]), + ); + + // The definition's referenced block workflows deploy first: an agent + // is a lens over a block, and the connect card's + // start-reviewing step resolves the deployed block definition by + // name, so a bench must never end up with reviewers but no + // `code-review` workflow behind them. + const deployedBlockAssetNames: string[] = []; + const skippedBlockAssetNames: string[] = []; + for (const block of definition.blocks) { + const outcome = await ports.deployBlockWorkflow(block); + (outcome.created ? deployedBlockAssetNames : skippedBlockAssetNames).push( + block.assetName, + ); + } + + const createdHandles: string[] = []; + const skippedHandles: string[] = []; + const invitedHandles: string[] = []; + for (const agent of definition.agents) { + const existingId = existingIdsByHandle.get(agent.handle); + let agentId: string; + if (existingId !== undefined) { + skippedHandles.push(agent.handle); + agentId = existingId; + } else if (agent.handle === "myra") { + // Seeded principal (`name: "assistant"`), never minted from a + // definition's roster. The create path already gates on + // `findMyraDefinition`; skip rather than throw. + continue; + } else { + const request = requestsByHandle.get(agent.handle); + if (request === undefined) { + throw new Error( + `workbench definition "${definition.id}" agent "${agent.handle}" ` + + "has no known create-agent request to instantiate it from", + ); + } + const created = await ports.createParticipantAgent(request); + createdHandles.push(agent.handle); + agentId = created.id; + } + await ports.inviteParticipantAgent(agentId); + invitedHandles.push(agent.handle); + } + + await ports.recordPendingConnections(definition.plugins.required); + + if (definition.onboardingSteps.length > 0) { + await ports.beginOnboarding(definition.onboardingSteps); + } + + return { + createdHandles, + skippedHandles, + invitedHandles, + deployedBlockAssetNames, + skippedBlockAssetNames, + pendingConnections: definition.plugins.required, + onboardingSteps: definition.onboardingSteps, + }; +} diff --git a/templates/package.json b/templates/package.json new file mode 100644 index 000000000..f84b96ad7 --- /dev/null +++ b/templates/package.json @@ -0,0 +1,29 @@ +{ + "name": "@workbench/templates", + "private": true, + "description": "Workbench templates: the shipped WorkbenchDefinition data (one directory per template, peer of workflows/) plus the pure logic that instantiates one against a bench over injected ports", + "version": "0.0.1", + "license": "LGPL-2.1-or-later", + "type": "module", + "exports": { + ".": "./index.ts", + "./catalog": "./catalog.ts", + "./instantiate": "./instantiate.ts", + "./settings": "./settings.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "bun test" + }, + "dependencies": { + "@corbits/code-review": "workspace:*", + "@corbits/jimmy-agent": "workspace:*", + "@corbits/scout-agent": "workspace:*", + "arktype": "catalog:" + }, + "devDependencies": { + "@types/bun": "catalog:", + "@workbench/connections": "workspace:*", + "typescript": "catalog:" + } +} diff --git a/templates/participant-agent-requests.ts b/templates/participant-agent-requests.ts new file mode 100644 index 000000000..367f54bb6 --- /dev/null +++ b/templates/participant-agent-requests.ts @@ -0,0 +1,49 @@ +// Request-shaped views of this catalog's two standalone chat-agent +// participants (Scout, Jimmy), in the shape `./instantiate.ts`'s +// `createParticipantAgent` port takes — the same shape +// `@corbits/code-review/agent-requests`' `codeReviewAgentRequests` +// establishes for the reviewer roster, extended with `toolPackagePins` +// since neither Scout nor Jimmy is a pure-text agent. Built here, from +// each package's own exported definition, rather than inside +// `packages/scout-agent`/`packages/jimmy-agent` themselves: this +// package already owns "what a template participant needs to become an +// agent-directory create request" (it owns `codeReviewAgentRequests`' +// caller), so the mapping belongs beside it, not duplicated into every +// agent package that wants to be installable this way. +import { SCOUT_AGENT_DEFINITION } from "@corbits/scout-agent/definition"; +import { + JIMMY_AGENT_ID, + JIMMY_DESCRIPTION, + JIMMY_DISPLAY_NAME, + JIMMY_SYSTEM_PROMPT, + JIMMY_TOOL_PACKAGE_PINS, +} from "@corbits/jimmy-agent/metadata"; + +import type { ParticipantAgentRequest } from "./instantiate"; + +export function scoutAgentRequest(): ParticipantAgentRequest { + return { + name: SCOUT_AGENT_DEFINITION.displayName, + handle: SCOUT_AGENT_DEFINITION.handle, + description: SCOUT_AGENT_DEFINITION.description, + systemPrompt: SCOUT_AGENT_DEFINITION.systemPrompt, + toolPackagePins: SCOUT_AGENT_DEFINITION.toolPackagePins.map( + (pin) => pin.name, + ), + }; +} + +/** Jimmy's own package exports an `@intx/agent`-native `AgentDefinition` + * builder (`buildJimmyAgent`), not the `{handle, displayName, ...}` + * plain-data shape the agent-directory create path takes — it has no + * handle or display name of its own at all. `JIMMY_AGENT_ID` ("jimmy") + * is both his id and the handle a person types to reach him. */ +export function jimmyAgentRequest(): ParticipantAgentRequest { + return { + name: JIMMY_DISPLAY_NAME, + handle: JIMMY_AGENT_ID, + description: JIMMY_DESCRIPTION, + systemPrompt: JIMMY_SYSTEM_PROMPT, + toolPackagePins: JIMMY_TOOL_PACKAGE_PINS.map((pin) => pin.name), + }; +} diff --git a/templates/schema.test.ts b/templates/schema.test.ts new file mode 100644 index 000000000..60cd2683d --- /dev/null +++ b/templates/schema.test.ts @@ -0,0 +1,306 @@ +import { expect, test } from "bun:test"; +import { CONNECTOR_REGISTRY } from "@workbench/connections/registry"; +import { MCP_PRESETS } from "@workbench/connections/mcp-presets"; +import { SCOUT_TOOL_PACKAGE_PINS } from "@corbits/scout-agent/definition"; + +import { + CODE_REVIEW_TEMPLATE, + DUE_DILIGENCE_TEMPLATE, + GTM_TEMPLATE, + WORKBENCH_TEMPLATES, + WORKFLOW_CATALOG, + templateBlockAssetNames, + workbenchTemplate, + parseWorkbenchDefinition, + serializeWorkbenchDefinition, + workflowCatalogEntry, +} from "./index"; + +test("every template's blocks are real catalog workflows", () => { + const known = new Set(WORKFLOW_CATALOG.map((entry) => entry.assetName)); + for (const template of WORKBENCH_TEMPLATES) { + for (const assetName of templateBlockAssetNames(template)) { + expect(known).toContain(assetName); + } + } +}); + +test("every connector a template names is a real connector or MCP preset", () => { + const known = new Set([ + ...Object.keys(CONNECTOR_REGISTRY), + ...MCP_PRESETS.map((preset) => preset.slug), + ]); + for (const template of WORKBENCH_TEMPLATES) { + for (const connector of [ + ...template.plugins.required, + ...template.plugins.optional, + ]) { + expect(known).toContain(connector); + } + } +}); + +test("a template never both requires and merely offers the same connector", () => { + for (const template of WORKBENCH_TEMPLATES) { + const required = new Set(template.plugins.required); + for (const optional of template.plugins.optional) { + expect(required).not.toContain(optional); + } + } +}); + +test("only automatable blocks get put on a schedule", () => { + for (const template of WORKBENCH_TEMPLATES) { + for (const routine of template.routines) { + expect(workflowCatalogEntry(routine.blockAssetName)?.automatable).toBe( + true, + ); + } + } +}); + +test("the GTM template installs the four ported v1 workflows plus the call write-up backbone", () => { + expect(workbenchTemplate("gtm")).toBe(GTM_TEMPLATE); + expect(templateBlockAssetNames(GTM_TEMPLATE)).toEqual([ + "granola-call", + "process-granola-call", + "attio-task-agent", + "exa-topic-watch", + "pain-point-collateral", + ]); +}); + +test("the GTM template blocks the create on the two connectors nothing works without", () => { + expect(GTM_TEMPLATE.plugins.required).toEqual(["attio", "exa"]); + expect(GTM_TEMPLATE.plugins.optional).toEqual(["granola"]); +}); + +test("the GTM template schedules call discovery and the web watch, and nothing else", () => { + expect(GTM_TEMPLATE.routines.map((routine) => routine.key)).toEqual([ + "call-discovery", + "topic-watch", + ]); + expect( + GTM_TEMPLATE.routines.map((routine) => routine.blockAssetName), + ).toEqual(["granola-call", "exa-topic-watch"]); +}); + +test("the watch's one open input reaches the routine that needs it", () => { + expect(GTM_TEMPLATE.openInputs).toHaveLength(1); + const [topic] = GTM_TEMPLATE.openInputs; + expect(topic?.key).toBe("topic"); + expect(topic?.required).toBe(true); + expect(topic?.appliesToRoutine).toBe("topic-watch"); +}); + +test("every routine-scoped open input names a field the block's own trigger contract carries", () => { + for (const template of WORKBENCH_TEMPLATES) { + for (const input of template.openInputs) { + if (input.appliesToRoutine === undefined) continue; + const routine = template.routines.find( + (candidate) => candidate.key === input.appliesToRoutine, + ); + const fields = + workflowCatalogEntry(routine?.blockAssetName ?? "")?.triggerFields ?? + []; + expect(fields.map((field) => field.key)).toContain(input.key); + } + } +}); + +test("every webhook-trigger-scoped open input names a real webhook trigger", () => { + for (const template of WORKBENCH_TEMPLATES) { + for (const input of template.openInputs) { + if (input.appliesToWebhookTrigger === undefined) continue; + const trigger = template.webhookTriggers.find( + (candidate) => candidate.key === input.appliesToWebhookTrigger, + ); + expect(trigger).toBeDefined(); + } + } +}); + +test("every webhook trigger's field names a field the block's own trigger contract carries", () => { + for (const template of WORKBENCH_TEMPLATES) { + for (const trigger of template.webhookTriggers) { + const fields = + workflowCatalogEntry(trigger.blockAssetName)?.triggerFields ?? []; + expect(fields.map((field) => field.key)).toContain( + trigger.triggerFieldKey, + ); + } + } +}); + +test("the code-review template installs the code-review workflow and requires github", () => { + expect(workbenchTemplate("code-review")).toBe(CODE_REVIEW_TEMPLATE); + expect(templateBlockAssetNames(CODE_REVIEW_TEMPLATE)).toEqual([ + "code-review", + ]); + expect(CODE_REVIEW_TEMPLATE.plugins.required).toEqual(["github"]); +}); + +test("the code-review template fires from a pull-request webhook, not a clock", () => { + expect(CODE_REVIEW_TEMPLATE.routines).toEqual([]); + expect(CODE_REVIEW_TEMPLATE.webhookTriggers).toHaveLength(1); + const [trigger] = CODE_REVIEW_TEMPLATE.webhookTriggers; + expect(trigger?.key).toBe("pull-request-opened"); + expect(trigger?.blockAssetName).toBe("code-review"); + expect(trigger?.triggerFieldKey).toBe("pullRequestUrl"); +}); + +test("the code-review definition's agents are the three reviewers and nobody else", () => { + expect(CODE_REVIEW_TEMPLATE.agents.map((agent) => agent.handle)).toEqual([ + "correctness-reviewer", + "architecture-reviewer", + "release-risk-reviewer", + ]); +}); + +test("the code-review definition names the GitHub tool package its reviewers need", () => { + expect(CODE_REVIEW_TEMPLATE.tools).toEqual(["@corbits/github-tools"]); +}); + +test("the code-review walkthrough is connect GitHub, pick repos, start reviewing — in that order", () => { + expect(CODE_REVIEW_TEMPLATE.onboardingSteps.map((step) => step.kind)).toEqual( + ["connect-plugin", "pick-github-repos", "start-webhook-trigger"], + ); + for (const step of CODE_REVIEW_TEMPLATE.onboardingSteps) { + expect(step.title.length).toBeGreaterThan(0); + expect(step.why.length).toBeGreaterThan(0); + } +}); + +test("agents are addressable by a distinct handle", () => { + for (const template of WORKBENCH_TEMPLATES) { + const handles = template.agents.map((agent) => agent.handle); + expect(new Set(handles).size).toBe(handles.length); + } +}); + +test("a definition never names the same tool package twice", () => { + for (const template of WORKBENCH_TEMPLATES) { + expect(new Set(template.tools).size).toBe(template.tools.length); + } +}); + +test("the due-diligence definition's agents are Myra and Scout, neither backed by a block", () => { + expect(workbenchTemplate("due-diligence")).toBe(DUE_DILIGENCE_TEMPLATE); + expect(DUE_DILIGENCE_TEMPLATE.agents.map((agent) => agent.handle)).toEqual([ + "myra", + "scout", + ]); + expect(templateBlockAssetNames(DUE_DILIGENCE_TEMPLATE)).toEqual([]); + for (const agent of DUE_DILIGENCE_TEMPLATE.agents) { + expect(agent.blockAssetName).toBeUndefined(); + } +}); + +test("the due-diligence definition names Scout's tool packages and needs no walkthrough", () => { + expect(DUE_DILIGENCE_TEMPLATE.tools).toEqual( + SCOUT_TOOL_PACKAGE_PINS.map((pin) => pin.name), + ); + expect(DUE_DILIGENCE_TEMPLATE.onboardingSteps).toEqual([]); +}); + +test("the due-diligence template blocks the create on nothing — Exa is offered, never required", () => { + expect(DUE_DILIGENCE_TEMPLATE.plugins.required).toEqual([]); + expect(DUE_DILIGENCE_TEMPLATE.plugins.optional).toEqual(["exa"]); +}); + +test("Jimmy is not a workbench template — the picker offers no such kind of workbench", () => { + expect(workbenchTemplate("default-teammates")).toBeUndefined(); + for (const template of WORKBENCH_TEMPLATES) { + expect(template.agents.some((agent) => agent.handle === "jimmy")).toBe( + false, + ); + } +}); + +test("every shipped definition survives the seed round trip verbatim", () => { + for (const template of WORKBENCH_TEMPLATES) { + const parsed = parseWorkbenchDefinition( + serializeWorkbenchDefinition(template), + ); + expect(parsed).toEqual(template); + } +}); + +test("a malformed library row fails to parse instead of half-loading", () => { + expect(() => parseWorkbenchDefinition('{"id":"code-review"}')).toThrow( + /failed to parse/, + ); + const firstRoutine = GTM_TEMPLATE.routines[0]; + if (!firstRoutine) throw new Error("GTM_TEMPLATE has no routines"); + const orphanRoutine = { + ...GTM_TEMPLATE, + routines: [{ ...firstRoutine, blockAssetName: "not-installed" }], + }; + expect(() => + parseWorkbenchDefinition(serializeWorkbenchDefinition(orphanRoutine)), + ).toThrow(/does not install/); +}); + +test("a required plugin with no step to connect it fails loud, naming the plugin", () => { + const unaskedPlugin = { + ...DUE_DILIGENCE_TEMPLATE, + plugins: { required: ["exa"], optional: [] }, + }; + expect(() => + parseWorkbenchDefinition(serializeWorkbenchDefinition(unaskedPlugin)), + ).toThrow(/requires plugin "exa" but its onboarding never asks/); +}); + +test("a walkthrough step naming a connector outside the definition's plugins fails loud", () => { + const strayConnector = { + ...DUE_DILIGENCE_TEMPLATE, + onboardingSteps: [ + { + kind: "connect-plugin" as const, + connectorId: "attio", + title: "Connect Attio", + why: "Nothing here reads Attio.", + }, + ], + }; + expect(() => + parseWorkbenchDefinition(serializeWorkbenchDefinition(strayConnector)), + ).toThrow(/connects "attio", which is not one of its plugins/); +}); + +test("a start-webhook-trigger step naming an unknown trigger fails loud", () => { + const unknownTrigger = { + ...CODE_REVIEW_TEMPLATE, + onboardingSteps: [ + ...CODE_REVIEW_TEMPLATE.onboardingSteps.slice(0, 2), + { + kind: "start-webhook-trigger" as const, + webhookTriggerKey: "issue-opened", + title: "Start reviewing", + why: "Nothing fires on issues.", + }, + ], + }; + expect(() => + parseWorkbenchDefinition(serializeWorkbenchDefinition(unknownTrigger)), + ).toThrow(/"issue-opened", which the definition does not create/); +}); + +test("picking repos before GitHub is connected fails loud — the walkthrough is ordered", () => { + const outOfOrder = { + ...CODE_REVIEW_TEMPLATE, + onboardingSteps: [ + { + kind: "pick-github-repos" as const, + title: "Pick your repos", + why: "Only the repositories you choose get reviewed.", + }, + ...CODE_REVIEW_TEMPLATE.onboardingSteps.filter( + (step) => step.kind !== "pick-github-repos", + ), + ], + }; + expect(() => + parseWorkbenchDefinition(serializeWorkbenchDefinition(outOfOrder)), + ).toThrow(/asks for repos before its onboarding connects "github"/); +}); diff --git a/templates/settings.test.ts b/templates/settings.test.ts new file mode 100644 index 000000000..a5c769423 --- /dev/null +++ b/templates/settings.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from "bun:test"; + +import { templateSettingsPatch } from "./settings"; + +test("builds a valid template/* settings patch", () => { + expect(templateSettingsPatch("code-review", ["github"])).toEqual({ + "template/id": "code-review", + "template/pendingConnections": ["github"], + }); +}); + +test("rejects an empty template id", () => { + expect(() => templateSettingsPatch("", ["github"])).toThrow( + /invalid template settings patch/, + ); +}); diff --git a/templates/settings.ts b/templates/settings.ts new file mode 100644 index 000000000..e36c5c0d0 --- /dev/null +++ b/templates/settings.ts @@ -0,0 +1,71 @@ +// The `template/*` workbench-settings vocabulary: what a room minted +// from a template persists about that fact, so the shell can render +// its setup state (which connections still need connecting) without a +// second round trip. `@corbits/chat`'s settings route validates only +// its own `chat/*` keys and passes any other namespace through opaquely +// (see `packages/chat/src/workbench-settings.ts`) — this schema is the +// boundary check a caller runs before a `template/*` patch ever leaves +// the client, matching the "parse at every trust boundary" rule for a +// namespace `chat` deliberately leaves to its owner. +import { type } from "arktype"; + +export const TemplateSettingsPatch = type({ + "template/id": "string > 0", + "template/pendingConnections": "string[]", +}); +export type TemplateSettingsPatch = typeof TemplateSettingsPatch.infer; + +/** Builds and validates the settings patch a freshly instantiated + * template writes onto its workbench. Throws on a malformed id or + * connections list rather than sending bad data to the wire. */ +export function templateSettingsPatch( + templateId: string, + pendingConnections: readonly string[], +): TemplateSettingsPatch { + const patch = { + "template/id": templateId, + "template/pendingConnections": [...pendingConnections], + }; + const parsed = TemplateSettingsPatch(patch); + if (parsed instanceof type.errors) { + throw new Error(`invalid template settings patch: ${parsed.summary}`); + } + return parsed; +} + +/** + * The `github` connect card's own settled state (CL-6345): once a + * person has picked which repos to review, `./connect-github-setup.ts` + * writes both keys in the same patch — `pendingConnections` with + * `"github"` removed (this template needs nothing else, so that leaves + * it empty) and `selectedRepos` naming exactly what got a live webhook + * trigger. This patch rides the same `chat/*` settings PATCH route + * every other `template/*` write does; the card itself refetches its + * state as the direct consequence of the `start-reviewing` call that + * triggers this patch, same as it does for its own PAT submit + * (`ConnectGithubBlockContainer`) — never a fold off this stream event. + */ +export const TemplateReposSettingsPatch = type({ + "template/pendingConnections": "string[]", + "template/selectedRepos": "string[]", +}); +export type TemplateReposSettingsPatch = + typeof TemplateReposSettingsPatch.infer; + +/** Builds and validates the settings patch that records which repos a + * room's GitHub connect card started reviewing. Throws on a malformed + * repo id list rather than sending bad data to the wire. */ +export function templateReposSettingsPatch( + pendingConnections: readonly string[], + selectedRepoIds: readonly string[], +): TemplateReposSettingsPatch { + const patch = { + "template/pendingConnections": [...pendingConnections], + "template/selectedRepos": [...selectedRepoIds], + }; + const parsed = TemplateReposSettingsPatch(patch); + if (parsed instanceof type.errors) { + throw new Error(`invalid template repos settings patch: ${parsed.summary}`); + } + return parsed; +} diff --git a/templates/tsconfig.json b/templates/tsconfig.json new file mode 100644 index 000000000..8b1da8f02 --- /dev/null +++ b/templates/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["**/*.ts"], + "compilerOptions": { + "types": ["bun"], + "noEmit": true + } +} diff --git a/workflows/diligence-brief/README.md b/workflows/diligence-brief/README.md index 028afa46d..41b1fea04 100644 --- a/workflows/diligence-brief/README.md +++ b/workflows/diligence-brief/README.md @@ -87,7 +87,7 @@ round-trip. `bun run typecheck` passes clean. ## Registration -Registered in `packages/workflow-catalog`'s `WORKFLOW_CATALOG` (asset +Registered in `@corbits/workflows`'s `WORKFLOW_CATALOG` (asset name `diligence-brief`) alongside this package's own `corbits.workflow` block, so it shows up wherever that catalog is read: the seed step and the web Routines picker. See [`workflows/README.md`](../README.md#status-note) diff --git a/workflows/last-30-days-research/README.md b/workflows/last-30-days-research/README.md index 1093aa918..239bb2622 100644 --- a/workflows/last-30-days-research/README.md +++ b/workflows/last-30-days-research/README.md @@ -174,7 +174,7 @@ gathering steps to reach real data. Every run is triggered by mail to the deployment's address (`triggerAddress` above); the `ground`/`gather` steps read two fields off -the intake signal payload, matching `packages/workflow-catalog`'s +the intake signal payload, matching `@corbits/workflows`'s `triggerFields` for this workflow exactly: - `topic` (required) — the free-text subject to research. diff --git a/workflows/last-30-days-research/src/index.ts b/workflows/last-30-days-research/src/index.ts index 226ee7c66..c965de2cf 100644 --- a/workflows/last-30-days-research/src/index.ts +++ b/workflows/last-30-days-research/src/index.ts @@ -61,7 +61,7 @@ // provider the OG used) and `github_activity` (`@corbits/github-tools`, // GitHub's public REST search) — the same scope this repo's prior ports // already carried and the same `topic`/`focus` intake shape the OG's own -// `INTAKE_FIELDS` and this repo's `workflow-catalog` entry already agree +// `INTAKE_FIELDS` and this repo's catalog entry already agree // on. The remaining five stay named as "not yet connected" rather than // silently dropped (see `LAST_30_DAYS_RESEARCH_PENDING_SOURCES`). // diff --git a/workflows/pain-point-collateral/src/finalize-tool.ts b/workflows/pain-point-collateral/src/finalize-tool.ts index 483dd7acf..1271aa823 100644 --- a/workflows/pain-point-collateral/src/finalize-tool.ts +++ b/workflows/pain-point-collateral/src/finalize-tool.ts @@ -3,7 +3,7 @@ // a shared tool package — this behavior (what "finalize" means for a // pain-point-collateral run) is specific to this workflow, not a // reusable integration, so it stays folded into the definition per the -// workflow-catalog's "workflow-specific logic lives in the definition" +// @corbits/workflows's "workflow-specific logic lives in the definition" // convention (only genuinely reusable integrations, like // `@corbits/granola-tools`, live outside it). // diff --git a/workflows/process-granola-call/src/finalize-tool.ts b/workflows/process-granola-call/src/finalize-tool.ts index 326c4bbe8..9e8449057 100644 --- a/workflows/process-granola-call/src/finalize-tool.ts +++ b/workflows/process-granola-call/src/finalize-tool.ts @@ -5,7 +5,7 @@ // rather than a shared tool package — this behavior (what "finalize" // means for a process-granola-call run) is specific to this workflow, not // a reusable integration, so it stays folded into the definition per the -// workflow-catalog's "workflow-specific logic lives in the definition" +// @corbits/workflows's "workflow-specific logic lives in the definition" // convention. // // Approval mechanics: `defineTool`'s `definitions` array marks this diff --git a/workflows/process-granola-call/src/index.ts b/workflows/process-granola-call/src/index.ts index bf3203f19..b50c95809 100644 --- a/workflows/process-granola-call/src/index.ts +++ b/workflows/process-granola-call/src/index.ts @@ -7,7 +7,7 @@ // one run per unprocessed call — never scheduled directly, and never // exposed as an independent Routine (see this package's `automatable: // false` in `package.json`'s `corbits.workflow` block, mirrored into -// `@corbits/workflow-catalog`). +// `@corbits/workflows`). // // This package is installable data. It imports only published platform // packages, and nothing imports it statically: a host publishes the From 9be92110c6645754df1cad3e359eee47cfd190f8 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 2 Sep 2026 09:21:21 -0700 Subject: [PATCH 2/7] Load WORKFLOW_CATALOG from each workflow's corbits.workflow block (CL-7373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit templates/catalog.ts reads assetName/displayName/automatable straight off each workflows/*/package.json corbits.workflow block via a static JSON import (resolved at build time, so it stays importable from the browser bundle) instead of hand-copying them into a parallel array. The remaining catalog fields (whatItDoes, requiredConnections, exampleOutput, ...) have no npm-visible home of their own, so they stay hand-authored here — there's no second copy of them to drift. --- templates/catalog.test.ts | 373 +++++++++++++++++++++++++++ templates/catalog.ts | 528 ++++++++++++++++++++++++++++++++++++++ workflows/README.md | 11 +- 3 files changed, 908 insertions(+), 4 deletions(-) create mode 100644 templates/catalog.test.ts create mode 100644 templates/catalog.ts diff --git a/templates/catalog.test.ts b/templates/catalog.test.ts new file mode 100644 index 000000000..bf6c36d1b --- /dev/null +++ b/templates/catalog.test.ts @@ -0,0 +1,373 @@ +import { type } from "arktype"; +import { describe, expect, test } from "bun:test"; + +import { CONNECTOR_REGISTRY } from "@workbench/connections/registry"; +import { MCP_PRESETS } from "@workbench/connections/mcp-presets"; + +import { + deliveryWorkbenchRequiredForWorkflowName, + isAutomatableWorkflowName, + isConversationalWorkflowName, + validateTriggerFieldsAtCreate, + workflowDisplayName, + workflowCatalogEntry, + WorkflowTriggerField, + WORKFLOW_CATALOG, +} from "./catalog"; + +describe("workflow catalog", () => { + test("marks workbench-digest, heartbeat, and morning-brief automatable, not echo or assistant", () => { + expect(isAutomatableWorkflowName("workbench-digest")).toBe(true); + expect(isAutomatableWorkflowName("heartbeat")).toBe(true); + expect(isAutomatableWorkflowName("morning-brief")).toBe(true); + expect(isAutomatableWorkflowName("echo")).toBe(false); + expect(isAutomatableWorkflowName("assistant")).toBe(false); + }); + + test("marks the granola-call parent automatable, not its process-granola-call child", () => { + // The parent is schedule-attachable; the child is spawned per call and + // must never appear as an independent Routines-picker option. + expect(isAutomatableWorkflowName("granola-call")).toBe(true); + expect(isAutomatableWorkflowName("process-granola-call")).toBe(false); + }); + + test("prefers the catalog display name for the granola-call workflows", () => { + expect(workflowDisplayName("granola-call")).toBe("Granola call notes"); + expect(workflowDisplayName("process-granola-call")).toBe( + "Process Granola call", + ); + }); + + test("does not mark pain-point-collateral automatable — its approval gate is a poor fit for unattended scheduling", () => { + expect(isAutomatableWorkflowName("pain-point-collateral")).toBe(false); + expect(workflowDisplayName("pain-point-collateral")).toBe( + "Pain-point collateral", + ); + }); + + test("does not mark collateral-generation automatable — on-demand only, gated behind its final approval", () => { + expect(isAutomatableWorkflowName("collateral-generation")).toBe(false); + expect(workflowDisplayName("collateral-generation")).toBe( + "Collateral generation", + ); + }); + + test("marks reddit-opportunity-scanner on-demand — its approval gates make it a poor unattended fit", () => { + expect(isAutomatableWorkflowName("reddit-opportunity-scanner")).toBe(false); + expect(workflowDisplayName("reddit-opportunity-scanner")).toBe( + "Reddit opportunity scanner", + ); + }); + + test("does not mark last-30-days-research automatable — on-demand only, gated behind a human-supplied topic per run", () => { + expect(isAutomatableWorkflowName("last-30-days-research")).toBe(false); + expect(workflowDisplayName("last-30-days-research")).toBe( + "Last 30 days research report", + ); + }); + + test("rejects agent handles and workbench-host names as automatable", () => { + expect(isAutomatableWorkflowName("my-researcher")).toBe(false); + expect(isAutomatableWorkflowName("workbench-host-abc")).toBe(false); + expect(isAutomatableWorkflowName("wfd_deadbeef")).toBe(false); + }); + + test("prefers catalog display names over raw asset names", () => { + expect(workflowDisplayName("workbench-digest")).toBe("Workbench digest"); + expect(workflowDisplayName("heartbeat")).toBe("Heartbeat"); + expect(workflowDisplayName("morning-brief")).toBe("Morning brief"); + expect(workflowDisplayName("echo")).toBe("Echo"); + expect(workflowDisplayName("assistant")).toBe("Myra"); + }); + + test("productizes the seeded assistant under the Myra display name", () => { + // The assistant workflow ships in DEFAULT_WORKFLOWS for every personal + // bench. Its catalog display name is the productized label Myra, not + // the generic "Assistant" — the routines picker and seeded asset both + // read it from here. + expect(workflowDisplayName("assistant")).toBe("Myra"); + const entry = WORKFLOW_CATALOG.find((e) => e.assetName === "assistant"); + expect(entry?.displayName).toBe("Myra"); + }); + + test("falls back to description, then humanized name — never blank", () => { + expect(workflowDisplayName("unknown-flow", " Weekly brief ")).toBe( + "Weekly brief", + ); + expect(workflowDisplayName("last-30-days")).toBe("Last 30 Days"); + }); + + describe("isConversationalWorkflowName", () => { + test("marks only the seeded assistant/Myra definition conversational", () => { + expect(isConversationalWorkflowName("assistant")).toBe(true); + }); + + test("marks every workflow utility, automatable or not, non-conversational", () => { + expect(isConversationalWorkflowName("echo")).toBe(false); + expect(isConversationalWorkflowName("workbench-digest")).toBe(false); + expect(isConversationalWorkflowName("last-30-days-research")).toBe(false); + expect(isConversationalWorkflowName("heartbeat")).toBe(false); + expect(isConversationalWorkflowName("process-granola-call")).toBe(false); + }); + + test("treats a name absent from the catalog as conversational — a runtime agent-directory definition", () => { + expect(isConversationalWorkflowName("my-researcher")).toBe(true); + expect(isConversationalWorkflowName("wfd_deadbeef")).toBe(true); + }); + + // CL-6649: `echo` and `last-30-days-research` are both non-automatable + // AND non-conversational — the exact combination that let a picker + // gated on `!isAutomatableWorkflowName` alone (rather than + // `isConversationalWorkflowName`) mistake a routine's delivery + // workflow, and the Echo wiring check, for an invitable chat agent. + test("a non-automatable utility is still non-conversational (the CL-6649 trap)", () => { + for (const name of ["echo", "last-30-days-research"]) { + expect(isAutomatableWorkflowName(name)).toBe(false); + expect(isConversationalWorkflowName(name)).toBe(false); + } + }); + + test("every catalog entry declares a conversational flag", () => { + for (const entry of WORKFLOW_CATALOG) { + expect(typeof entry.conversational).toBe("boolean"); + } + }); + }); + + test("every catalog entry has a non-empty display name", () => { + for (const entry of WORKFLOW_CATALOG) { + expect(entry.displayName.trim().length).toBeGreaterThan(0); + expect(entry.assetName).toMatch(/^[a-z0-9-]+$/); + } + }); + + describe("deliveryMode", () => { + test("every catalog entry declares a delivery mode", () => { + for (const entry of WORKFLOW_CATALOG) { + expect(["workbench", "inbox"]).toContain(entry.deliveryMode); + } + }); + + test("no catalog entry currently delivers to inbox", () => { + const inboxEntries = WORKFLOW_CATALOG.filter( + (entry) => entry.deliveryMode === "inbox", + ); + expect(inboxEntries).toEqual([]); + }); + + test("deliveryWorkbenchRequiredForWorkflowName is true for every known catalog entry", () => { + for (const entry of WORKFLOW_CATALOG) { + expect(deliveryWorkbenchRequiredForWorkflowName(entry.assetName)).toBe( + true, + ); + } + }); + + test("an unknown workflow name defaults to workbench-required", () => { + expect(deliveryWorkbenchRequiredForWorkflowName("unknown-workflow")).toBe( + true, + ); + }); + }); + + test("every catalog entry carries honest demo-card copy", () => { + for (const entry of WORKFLOW_CATALOG) { + expect(entry.whatItDoes.trim().length).toBeGreaterThan(0); + expect(entry.exampleOutput.trim().length).toBeGreaterThan(0); + expect(entry.typicalDuration.trim().length).toBeGreaterThan(0); + // No fake precision or invented metrics dressed up as facts. + expect(entry.whatItDoes).not.toMatch(/%|\$\d/); + } + }); + + test("every exampleOutput is a single capitalized readout fragment with no trailing period", () => { + for (const entry of WORKFLOW_CATALOG) { + expect(entry.exampleOutput).not.toContain("\n"); + expect(entry.exampleOutput.endsWith(".")).toBe(false); + expect(entry.exampleOutput[0]).toBe( + entry.exampleOutput[0]?.toUpperCase(), + ); + } + }); + + test("every requiredConnections id names a connector a person can actually connect", () => { + // Either a native connector (its own credential plugin and tool + // package) or an MCP preset a person connects under Plugins — Attio + // is only ever the latter here, reached through `@corbits/mcp-tools`. + const connectable = new Set([ + ...Object.keys(CONNECTOR_REGISTRY), + ...MCP_PRESETS.map((preset) => preset.slug), + ]); + for (const entry of WORKFLOW_CATALOG) { + for (const connectorId of entry.requiredConnections) { + expect(connectable.has(connectorId)).toBe(true); + } + } + }); + + test("pins the GTM ports to the connectors their tool packages actually need", () => { + const byAssetName = new Map( + WORKFLOW_CATALOG.map((entry) => [entry.assetName, entry]), + ); + expect(byAssetName.get("granola-call")?.requiredConnections).toEqual([ + "granola", + ]); + expect( + byAssetName.get("process-granola-call")?.requiredConnections, + ).toEqual(["granola"]); + expect(byAssetName.get("morning-brief")?.requiredConnections).toEqual([ + "granola", + "linear", + ]); + expect( + byAssetName.get("pain-point-collateral")?.requiredConnections, + ).toEqual(["granola"]); + expect( + byAssetName.get("collateral-generation")?.requiredConnections, + ).toEqual(["granola", "linear"]); + expect( + byAssetName.get("reddit-opportunity-scanner")?.requiredConnections, + ).toEqual(["scrapecreators"]); + expect( + byAssetName.get("last-30-days-research")?.requiredConnections, + ).toEqual(["exa"]); + }); + + test("workflows with no external connector requirement declare an empty list", () => { + const byAssetName = new Map( + WORKFLOW_CATALOG.map((entry) => [entry.assetName, entry]), + ); + expect(byAssetName.get("echo")?.requiredConnections).toEqual([]); + expect(byAssetName.get("assistant")?.requiredConnections).toEqual([]); + expect(byAssetName.get("heartbeat")?.requiredConnections).toEqual([]); + expect(byAssetName.get("workbench-digest")?.requiredConnections).toEqual( + [], + ); + }); + + describe("triggerFields", () => { + test("every declared triggerFields entry matches the WorkflowTriggerField shape", () => { + for (const entry of WORKFLOW_CATALOG) { + if (entry.triggerFields === undefined) continue; + const parsed = WorkflowTriggerField.array()(entry.triggerFields); + expect(parsed instanceof type.errors).toBe(false); + } + }); + + test("every triggerFields key is unique within its entry and non-blank labeled", () => { + for (const entry of WORKFLOW_CATALOG) { + if (entry.triggerFields === undefined) continue; + const keys = entry.triggerFields.map((field) => field.key); + expect(new Set(keys).size).toBe(keys.length); + for (const field of entry.triggerFields) { + expect(field.label.trim().length).toBeGreaterThan(0); + } + } + }); + + test("last-30-days-research declares a required topic and an optional focus", () => { + const entry = workflowCatalogEntry("last-30-days-research"); + expect(entry?.triggerFields).toEqual([ + { + key: "topic", + kind: "text", + label: "Topic", + placeholder: "AI coding agents", + required: true, + help: "What to research over the last 30 days.", + }, + { + key: "focus", + kind: "text", + label: "Focus", + placeholder: "Competing launches", + required: false, + help: "Optional — narrows which angle of the topic to chase.", + }, + ]); + }); + + test("pain-point-collateral declares its either/or transcript and noteId as optional", () => { + // Neither field is required on its own — the workflow's intake tool + // accepts either one (or neither, and teaches what to send next); see + // workflows/pain-point-collateral/src/intake-tool.ts's IntakeArgs. + const entry = workflowCatalogEntry("pain-point-collateral"); + expect(entry?.triggerFields?.map((f) => f.key)).toEqual([ + "transcript", + "noteId", + ]); + for (const field of entry?.triggerFields ?? []) { + expect(field.required).toBe(false); + } + }); + + test("workflows with no named trigger inputs declare no triggerFields", () => { + // Heartbeat and workbench-digest take no human-supplied content at + // create time — heartbeat ignores its trigger entirely, and + // workbench-digest's content is computed server-side by the scheduler, + // not typed in by a person. + expect(workflowCatalogEntry("heartbeat")?.triggerFields).toBeUndefined(); + expect( + workflowCatalogEntry("workbench-digest")?.triggerFields, + ).toBeUndefined(); + }); + }); + + // Two required fields, one "agent"-kind and one "text"-kind — an + // explicit local fixture, not pulled from any catalog entry, so this + // block's assertions about required/blank/non-string handling stay + // meaningful regardless of which entries the catalog happens to carry. + const AGENT_AND_PROMPT_FIELDS: readonly WorkflowTriggerField[] = [ + { key: "agent", kind: "agent", label: "Agent", required: true }, + { key: "prompt", kind: "text", label: "Prompt", required: true }, + ]; + + // CL-6358: inputs bind at USE, never at creation — a routine (or a + // seed preset) must be creatable with a required trigger field left + // entirely unbound. `validateTriggerFieldsAtCreate` is the boundary + // check `@corbits/routines`' create route actually applies now: + // absence of a required field is never rejected, only a value the + // caller explicitly provided but left malformed is. + describe("validateTriggerFieldsAtCreate", () => { + const fields = AGENT_AND_PROMPT_FIELDS; + + test("a required field left entirely unbound passes at create time", () => { + expect( + validateTriggerFieldsAtCreate(fields, { prompt: "Do it" }), + ).toEqual({ ok: true }); + }); + + test("last-30-days-research seeds with Topic unbound", () => { + const researchFields = workflowCatalogEntry("last-30-days-research") + ?.triggerFields as readonly WorkflowTriggerField[]; + expect(validateTriggerFieldsAtCreate(researchFields, {})).toEqual({ + ok: true, + }); + }); + + test("a provided-but-blank required field still fails: a caller who sets it must set it honestly", () => { + const result = validateTriggerFieldsAtCreate(fields, { + agent: " ", + prompt: "Do it", + }); + expect(result.ok).toBe(false); + }); + + test("a provided non-string value for a required field still fails", () => { + const result = validateTriggerFieldsAtCreate(fields, { + agent: 12345, + prompt: "Do it", + }); + expect(result.ok).toBe(false); + }); + + test("a fully valid input still passes", () => { + expect( + validateTriggerFieldsAtCreate(fields, { + agent: "wfd_1", + prompt: "Do it", + }), + ).toEqual({ ok: true }); + }); + }); +}); diff --git a/templates/catalog.ts b/templates/catalog.ts new file mode 100644 index 000000000..e6ac9fe5b --- /dev/null +++ b/templates/catalog.ts @@ -0,0 +1,528 @@ +// Deploy-layer metadata for every seeded workflow package: display name, +// whether it's schedulable as a routine, where its result actually lands +// (a channel vs. the creator's Inbox only), required connections, and +// optional trigger-input fields. Interchange's `defineWorkflow` has no +// automatable/display-name concept of its own. +// +// `assetName`/`displayName`/`automatable` are read straight off each +// `workflows/*/package.json`'s `corbits.workflow` block via a static JSON +// import below — resolved at build time (Vite, tsc, and Bun all support +// `resolveJsonModule`), never at runtime, so this stays importable from +// the browser bundle. There is nothing left to keep "in lockstep": the +// package.json block IS the value this module reads, not a copy of it. +// The remaining fields (`conversational`, `deliveryMode`, `whatItDoes`, +// `requiredConnections`, `exampleOutput`, `typicalDuration`, +// `triggerFields`) have no npm-visible home of their own — they exist +// only here, so there is no second copy for them to drift from. +import { type } from "arktype"; + +import echoPkg from "../workflows/echo/package.json"; +import codeReviewPkg from "../workflows/code-review/package.json"; +import assistantPkg from "../workflows/assistant/package.json"; +import heartbeatPkg from "../workflows/heartbeat/package.json"; +import workbenchDigestPkg from "../workflows/workbench-digest/package.json"; +import granolaCallPkg from "../workflows/granola-call/package.json"; +import processGranolaCallPkg from "../workflows/process-granola-call/package.json"; +import morningBriefPkg from "../workflows/morning-brief/package.json"; +import painPointCollateralPkg from "../workflows/pain-point-collateral/package.json"; +import collateralGenerationPkg from "../workflows/collateral-generation/package.json"; +import redditOpportunityScannerPkg from "../workflows/reddit-opportunity-scanner/package.json"; +import last30DaysResearchPkg from "../workflows/last-30-days-research/package.json"; +import diligenceBriefPkg from "../workflows/diligence-brief/package.json"; +import exaTopicWatchPkg from "../workflows/exa-topic-watch/package.json"; +import attioTaskAgentPkg from "../workflows/attio-task-agent/package.json"; + +const CorbitsWorkflowBlock = type({ + assetName: "string > 0", + displayName: "string > 0", + automatable: "boolean", +}); +type CorbitsWorkflowBlock = typeof CorbitsWorkflowBlock.infer; + +/** Reads one workflow package's `corbits.workflow` block — the only + * source of truth for `assetName`/`displayName`/`automatable`. Throws on + * a malformed block rather than seeding a broken catalog entry. */ +function workflowBlock(pkg: { + readonly corbits?: { readonly workflow?: unknown }; +}): CorbitsWorkflowBlock { + const parsed = CorbitsWorkflowBlock(pkg.corbits?.workflow); + if (parsed instanceof type.errors) { + throw new Error(`invalid corbits.workflow block: ${parsed.summary}`); + } + return parsed; +} + +/** + * One named field a mail trigger reads by name — the create-time UI's only + * source of truth for what a workflow's trigger actually expects (see each + * workflow's own system prompt / intake tool for the underlying contract + * this mirrors, e.g. `workflows/last-30-days-research/src/index.ts`'s + * "the trigger carries a `topic` and an optional `focus`", or + * `workflows/pain-point-collateral/src/intake-tool.ts`'s `IntakeArgs`). + * `key` is the exact field name a trigger payload carries — never + * relabeled or humanized before it reaches the workflow. + */ +export const WorkflowTriggerField = type({ + key: "/^[a-zA-Z][a-zA-Z0-9]*$/", + // Explicit, never defaulted: `"text"` renders a plain input and + // accepts any non-empty string; `"agent"` renders a picker of + // taskable agent definitions (the routines create stepper reuses the + // task composer's own listing) and its value must resolve to a real + // taskable definition at create time — see + // `packages/routines/src/routes.ts`'s trigger-input validation. + kind: "'text' | 'agent'", + label: "string > 0", + "placeholder?": "string", + required: "boolean", + "default?": "string", + "help?": "string", +}); +export type WorkflowTriggerField = typeof WorkflowTriggerField.infer; + +export type WorkflowCatalogEntry = { + readonly assetName: string; + readonly displayName: string; + /** Schedulable as a Routine. False for conversational agents / chat hosts. */ + readonly automatable: boolean; + /** + * A real chat partner a person can open a DM with and converse + * freely — as opposed to a mail-triggered utility whose only sane + * input is its declared trigger contract (a topic, a transcript, a + * scheduler-computed digest line, …). `automatable` alone can't tell + * these apart: several utilities (`echo`, `last-30-days-research`, + * `pain-point-collateral`, …) are non-automatable on-demand runs, not + * conversational agents. Only the seeded `assistant`/Myra definition + * is `true` today; every other catalog entry — the whole reason this + * catalog exists — is a workflow utility, never `true`. + */ + readonly conversational: boolean; + /** + * Where a run's result actually lands — the honest end-to-end + * contract a routine's "Deliver results to" step depends on. Every + * entry states this plainly, whether or not it is automatable: + * `"workbench"` posts into the picked delivery workbench's thread — + * every catalog entry today; `"inbox"` never posts to a workbench at + * all — its result reaches only the creator's Inbox, so a create/run + * flow for it must never collect or require a deliveryWorkbenchId that + * would otherwise be silently discarded. + */ + readonly deliveryMode: "workbench" | "inbox"; + /** One honest sentence: what this workflow actually does. No metrics, no hype. */ + readonly whatItDoes: string; + /** + * Connector ids this workflow's tool packages call — either a native + * connector (`@workbench/connections/registry`'s `CONNECTOR_REGISTRY`) + * or an MCP preset slug a person connects under Plugins + * (`@workbench/connections/mcp-presets`' `MCP_PRESETS`), which is the + * only way some integrations (Attio) are reachable here at all. Empty + * for workflows with no external connector dependency. + */ + readonly requiredConnections: readonly string[]; + /** A short, honest one-line readout of what a run actually produces — + * capitalized, no trailing period, same shape across every entry. */ + readonly exampleOutput: string; + /** A short, honestly-hedged hint — never fabricated precision. */ + readonly typicalDuration: string; + /** + * Named trigger inputs a person can fill in at create time (the routine + * create stepper) or on a manual run — omitted entirely for workflows + * whose trigger carries no human-supplied content (see each entry's own + * comment for why). Order is display order. + */ + readonly triggerFields?: readonly WorkflowTriggerField[]; +}; + +/** + * Every known workbench workflow package, keyed by the asset name seed + * deploys under. Agent definitions created at runtime are never listed + * here, so they cannot pass the automatable filter by accident. + */ +export const WORKFLOW_CATALOG: readonly WorkflowCatalogEntry[] = [ + { + ...workflowBlock(echoPkg), + conversational: false, + deliveryMode: "workbench", + whatItDoes: + "Replies with the exact text it received — a wiring check for the mail-triggered contract, not a real assistant.", + requiredConnections: [], + exampleOutput: "Echoed back: Testing 1 2 3", + typicalDuration: "a few seconds", + }, + { + ...workflowBlock(codeReviewPkg), + conversational: false, + deliveryMode: "workbench", + whatItDoes: + "Reads a pull request's diff, reviews it for correctness, architecture, and release risk, and posts one review back on the pull request.", + requiredConnections: ["github"], + exampleOutput: "One review posted: 1 blocking, 2 worth fixing, 1 for later", + typicalDuration: "a minute or two", + triggerFields: [ + { + key: "pullRequestUrl", + kind: "text", + label: "Pull request URL", + placeholder: "https://github.com/owner/repo/pull/123", + required: true, + help: "The pull request to review. A GitHub webhook fills this in on its own for every new pull request.", + }, + ], + }, + { + ...workflowBlock(assistantPkg), + conversational: true, + deliveryMode: "workbench", + whatItDoes: + "A general-purpose assistant for the workspace — answers questions, drafts text, and reasons through problems in conversation.", + requiredConnections: [], + exampleOutput: "Drafted a short, polite decline you can send as-is", + typicalDuration: "varies with the conversation", + }, + { + ...workflowBlock(heartbeatPkg), + conversational: false, + deliveryMode: "workbench", + whatItDoes: + "Completes immediately on every trigger with no real reply — a lightweight target for testing scheduling and mail triggers.", + requiredConnections: [], + exampleOutput: "Completed at the trigger time, no reply content", + typicalDuration: "a few seconds", + }, + { + ...workflowBlock(workbenchDigestPkg), + conversational: false, + deliveryMode: "workbench", + whatItDoes: + "Relays a scheduler-computed digest line straight into a workbench, unchanged — the digest content itself comes entirely from its trigger.", + requiredConnections: [], + exampleOutput: "Digest of 12 messages since yesterday · last post 9:41am", + typicalDuration: "a few seconds", + }, + { + ...workflowBlock(granolaCallPkg), + conversational: false, + deliveryMode: "workbench", + whatItDoes: + "Polls Granola for recent calls and starts one process-granola-call run per call that doesn't yet have published notes.", + requiredConnections: ["granola"], + exampleOutput: "Checked 10 calls, started 2 new process-granola-call runs", + typicalDuration: "a few seconds", + }, + { + ...workflowBlock(processGranolaCallPkg), + conversational: false, + deliveryMode: "workbench", + whatItDoes: + "Fetches one call's transcript and publishes five-section working notes — Participants, Summary, Pain points, Decisions, Action items — grounded in the transcript.", + requiredConnections: ["granola"], + exampleOutput: + "Participants, summary, and action items from the call transcript", + typicalDuration: "1-2 minutes", + }, + { + ...workflowBlock(morningBriefPkg), + conversational: false, + deliveryMode: "workbench", + whatItDoes: + "Pulls the sender's recent Granola calls and Linear issues and writes a three-section daily brief: what happened, what needs attention, and suggested next actions.", + requiredConnections: ["granola", "linear"], + exampleOutput: + "Brief covering 2 calls, 3 issue updates, one blocked review", + typicalDuration: "under a minute", + }, + { + ...workflowBlock(painPointCollateralPkg), + conversational: false, + deliveryMode: "workbench", + whatItDoes: + "Extracts a customer's real pain points from a call transcript and drafts one piece of targeted collateral, held for approval before it's finalized.", + requiredConnections: ["granola"], + exampleOutput: "Drafted a one-pager on the onboarding-speed pain point", + typicalDuration: "a few minutes, plus the time to approve", + // Neither field is required on its own: the workflow's intake tool + // (workflows/pain-point-collateral/src/intake-tool.ts) accepts a + // pasted transcript OR a Granola note id — transcript wins if both are + // given, and neither given is a valid "teach me what to send" path, + // not an error. Both are worth collecting at create/run time even + // though neither is required: a manual run with no input at all would + // just re-teach the same instructions the workflow already gives. + triggerFields: [ + { + key: "transcript", + kind: "text", + label: "Transcript", + placeholder: "Paste the call transcript", + required: false, + help: "Or leave blank and give a Granola note id below.", + }, + { + key: "noteId", + kind: "text", + label: "Granola note ID", + placeholder: "note_abc123", + required: false, + help: "Used only if no transcript is pasted above.", + }, + ], + }, + { + ...workflowBlock(collateralGenerationPkg), + conversational: false, + deliveryMode: "workbench", + whatItDoes: + "Drafts marketing collateral across picked content types from Granola notes, Linear issues, or pasted text, with a swipe review on every draft and one approval on the final set.", + requiredConnections: ["granola", "linear"], + exampleOutput: + "Drafts ready to review: LinkedIn post, blog, Twitter thread", + typicalDuration: "several minutes, plus review and approval time", + }, + { + ...workflowBlock(redditOpportunityScannerPkg), + conversational: false, + deliveryMode: "workbench", + whatItDoes: + "Scores Reddit posts as outreach opportunities for a target website, after a review of the search plan and one approval on the final list.", + requiredConnections: ["scrapecreators"], + exampleOutput: "Ranked 6 opportunities, 2 scored 5/5 on buying signals", + typicalDuration: "a few minutes, plus review and approval time", + }, + { + ...workflowBlock(last30DaysResearchPkg), + conversational: false, + deliveryMode: "workbench", + whatItDoes: + "Researches a topic over the last 30 days across web search and GitHub, and writes a cited report with sourced findings.", + requiredConnections: ["exa"], + exampleOutput: "Cited report: 3 new competing launches this month", + typicalDuration: "1-2 minutes", + // Mirrors workflows/last-30-days-research/src/index.ts's system prompt + // exactly: "the trigger carries a `topic` and an optional `focus`" — + // topic is required (the prompt refuses to invent one), focus narrows + // which angle to chase and is skippable. + triggerFields: [ + { + key: "topic", + kind: "text", + label: "Topic", + placeholder: "AI coding agents", + required: true, + help: "What to research over the last 30 days.", + }, + { + key: "focus", + kind: "text", + label: "Focus", + placeholder: "Competing launches", + required: false, + help: "Optional — narrows which angle of the topic to chase.", + }, + ], + }, + { + ...workflowBlock(diligenceBriefPkg), + conversational: false, + deliveryMode: "workbench", + whatItDoes: + "Researches a company across web search and firm memory, and writes a cited diligence brief across five fixed sections, held for approval before it's saved.", + requiredConnections: ["exa"], + exampleOutput: + "Diligence brief: 5 sections, 2 flagged as insufficient evidence", + typicalDuration: "1-2 minutes, plus review and approval time", + // Mirrors workflows/diligence-brief/src/index.ts's system prompt + // exactly: "the trigger carries a `company` and an optional `focus`" — + // company is required (the prompt refuses to draft with no subject), + // focus narrows which angle to dig into and is skippable. + triggerFields: [ + { + key: "company", + kind: "text", + label: "Company", + placeholder: "Acme Corp", + required: true, + help: "The company this brief is about.", + }, + { + key: "focus", + kind: "text", + label: "Focus", + placeholder: "Founder track record", + required: false, + help: "Optional — narrows which angle to dig into.", + }, + ], + }, + { + ...workflowBlock(exaTopicWatchPkg), + conversational: false, + deliveryMode: "workbench", + whatItDoes: + "Searches the live web for one topic each run and publishes a short digest of what moved, held for approval before it's saved.", + requiredConnections: ["exa"], + exampleOutput: "Weekly digest: 4 takeaways, 2 worth a closer look", + typicalDuration: "a minute or two, plus the time to approve", + // Mirrors workflows/exa-topic-watch/src/index.ts's system prompt: the + // trigger carries a `topic` and nothing else, and the prompt refuses + // to invent or widen one, so it is the single required field. + triggerFields: [ + { + key: "topic", + kind: "text", + label: "Topic", + placeholder: "AI coding agents for go-to-market", + required: true, + help: "What to watch on the web each run.", + }, + ], + }, + { + ...workflowBlock(attioTaskAgentPkg), + conversational: false, + deliveryMode: "workbench", + whatItDoes: + "Works one Attio task: reads the record and the surrounding context, drafts what the task needs, and writes back to Attio only once you approve it.", + requiredConnections: ["attio"], + exampleOutput: + "Two drafts ready to review, plus a CRM note awaiting your OK", + typicalDuration: "a few minutes, plus review and approval time", + // Mirrors workflows/attio-task-agent/src/index.ts's system prompt: + // the trigger names the task by id, or names whose task list to look + // in. Neither is required on its own — with neither, the run lists + // the open tasks it can see and asks which one, which is a valid + // start, not an error. + triggerFields: [ + { + key: "taskId", + kind: "text", + label: "Attio task ID", + placeholder: "The task to work", + required: false, + help: "Or leave blank and name whose tasks to look at below.", + }, + { + key: "assignee", + kind: "text", + label: "Assignee", + placeholder: "Whose task list to look in", + required: false, + help: "Used only if no task ID is given above.", + }, + ], + }, +]; + +for (const entry of WORKFLOW_CATALOG) { + if (entry.triggerFields === undefined) continue; + const parsed = WorkflowTriggerField.array()(entry.triggerFields); + if (parsed instanceof type.errors) { + throw new Error( + `workflow-catalog entry "${entry.assetName}" has an invalid ` + + `triggerFields shape: ${parsed.summary}`, + ); + } +} + +const byAssetName = new Map( + WORKFLOW_CATALOG.map((entry) => [entry.assetName, entry]), +); + +export function isAutomatableWorkflowName(name: string): boolean { + return byAssetName.get(name)?.automatable === true; +} + +/** + * Whether a workflow definition name is fit to offer as a DM/chat target + * (the sidebar's agent rows, a taskable-agent picker, …) rather than a + * triggered automation utility. A name absent from this catalog is an + * agent-directory-created definition — "Agent definitions created at + * runtime are never listed here" (see `WORKFLOW_CATALOG`'s own comment) — + * and is always conversational; a name present here is conversational + * only when its entry says so (`assistant`/Myra today). + */ +export function isConversationalWorkflowName(name: string): boolean { + const entry = byAssetName.get(name); + return entry === undefined || entry.conversational; +} + +/** + * Whether a routine on this workflow needs a `deliveryWorkbenchId` at + * all — `false` only for a known `"inbox"`-delivering entry (see + * `WorkflowCatalogEntry.deliveryMode`). An unknown name defaults `true` + * (workbench required): the safe, prior-behavior default when a workflow + * isn't catalog-known at all. + */ +export function deliveryWorkbenchRequiredForWorkflowName( + name: string, +): boolean { + return byAssetName.get(name)?.deliveryMode !== "inbox"; +} + +/** The full catalog entry for an asset name, or `undefined` if it isn't + * a known workflow — the demo-card fields (`whatItDoes`, `exampleOutput`, + * etc.) live here alongside the display name. */ +export function workflowCatalogEntry( + name: string, +): WorkflowCatalogEntry | undefined { + return byAssetName.get(name); +} + +/** + * Friendly label for a workflow definition. Prefer the catalog display + * name, then a non-empty description, then a humanized asset name — never + * a raw definition id. + */ +export function workflowDisplayName( + name: string, + description?: string | null, +): string { + const entry = byAssetName.get(name); + if (entry !== undefined) return entry.displayName; + if (description !== undefined && description !== null) { + const trimmed = description.trim(); + if (trimmed.length > 0) return trimmed; + } + return humanizeAssetName(name); +} + +function humanizeAssetName(name: string): string { + return name + .split(/[-_]/) + .filter((part) => part.length > 0) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); +} + +export type TriggerFieldsValidation = + { readonly ok: true } | { readonly ok: false; readonly message: string }; + +/** + * The create-time boundary check for a routine's stored `input` + * (CL-6358): inputs bind at USE, never at creation, so a required + * field with no value at all in `input` is never a create-time + * rejection — the seeded last-30-days-research preset's whole reason + * for existing is a required "Topic" left open until someone actually + * runs it. Only a value the caller explicitly provided gets checked, + * and only for basic shape (a non-empty string) — a key present but + * blank is a caller bug, not an open input, and still rejected. + * `kind: "agent"` resolution (does a provided value name a real + * taskable definition) is a separate, still-eager check a host layers + * on top (`apps/hub/src/index.ts`'s `routineInputValid`) since it + * needs a tenant DB lookup this function can't do. Fire-time + * validation (a host's own launcher definition checks) remains the + * authoritative required-field gate. + */ +export function validateTriggerFieldsAtCreate( + fields: readonly WorkflowTriggerField[], + input: Record, +): TriggerFieldsValidation { + for (const field of fields) { + if (!(field.key in input)) continue; + const value = input[field.key]; + if (typeof value !== "string" || value.trim() === "") { + return { + ok: false, + message: `"${field.label}" must not be blank`, + }; + } + } + return { ok: true }; +} diff --git a/workflows/README.md b/workflows/README.md index 1f4d5302a..9da6f5dbf 100644 --- a/workflows/README.md +++ b/workflows/README.md @@ -2,10 +2,13 @@ Each subdirectory is a deployable workflow package: an Interchange `defineWorkflow` plus a `corbits.workflow` block in its `package.json` -that mirrors into `packages/workflow-catalog`'s `WORKFLOW_CATALOG` — the -deploy-layer metadata the seed step and the web Routines picker both -read (see that package's own doc comment for why the two must stay in -lockstep). +(`assetName`, `displayName`, `automatable`). `@workbench/templates`'s +`WORKFLOW_CATALOG` (`templates/catalog.ts`) reads that block straight off +each `package.json` at build time — via a static JSON import, not a +runtime file read, so it stays importable from the browser bundle — and +layers on the fields with no npm-visible home of their own (`whatItDoes`, +`requiredConnections`, trigger fields, …). There is nothing to keep in +lockstep: the `package.json` block IS the value the catalog reads. ## Status note From 6423860e2421e65513368d8f4cecd0598db310f8 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 2 Sep 2026 09:21:31 -0700 Subject: [PATCH 3/7] Delete packages/workflow-catalog (CL-7373) Every export moved in the prior two commits (templates/, @workbench/ connections, apps/hub) or was superseded (the WORKFLOW_CATALOG mirror). Updates docs that named the old package (workflows/README.md already covered), regenerates the report-error baseline for the moved files' new paths, and fixes the two structural checks (error-envelope's allowlist, db-setup.ts's import) that referenced the old location. --- IMPLEMENTATION.md | 2 +- bun.lock | 231 +++---- docs/GLOSSARY.md | 2 +- docs/workflow-detail.md | 2 +- package.json | 4 +- packages/workflow-catalog/LICENSE | 176 ----- packages/workflow-catalog/README.md | 68 -- packages/workflow-catalog/package.json | 39 -- .../workflow-catalog/src/block-workflows.ts | 63 -- .../connect-github-credential-link.test.ts | 210 ------ .../src/connect-github-routes.test.ts | 323 --------- .../src/connect-github-routes.ts | 403 ----------- .../src/connect-github-setup.test.ts | 248 ------- .../src/connect-github-setup.ts | 190 ----- packages/workflow-catalog/src/index.ts | 560 --------------- packages/workflow-catalog/src/instantiate.ts | 203 ------ .../src/participant-agent-requests.ts | 49 -- .../src/reconcile-duplicate-repo-grants.ts | 55 -- packages/workflow-catalog/src/settings.ts | 71 -- .../src/template-block-routes.test.ts | 134 ---- .../src/template-block-routes.ts | 108 --- packages/workflow-catalog/src/templates.ts | 647 ------------------ .../workflow-catalog/test/catalog.test.ts | 373 ---------- .../test/connect-github-setup.race.test.ts | 219 ------ .../workflow-catalog/test/instantiate.test.ts | 288 -------- .../reconcile-duplicate-repo-grants.test.ts | 136 ---- .../workflow-catalog/test/settings.test.ts | 16 - .../workflow-catalog/test/templates.test.ts | 306 --------- packages/workflow-catalog/tsconfig.json | 8 - scripts/checks/error-envelope.ts | 7 +- scripts/checks/report-error-baseline.txt | 8 +- scripts/db-setup.ts | 2 +- 32 files changed, 126 insertions(+), 5025 deletions(-) delete mode 100644 packages/workflow-catalog/LICENSE delete mode 100644 packages/workflow-catalog/README.md delete mode 100644 packages/workflow-catalog/package.json delete mode 100644 packages/workflow-catalog/src/block-workflows.ts delete mode 100644 packages/workflow-catalog/src/connect-github-credential-link.test.ts delete mode 100644 packages/workflow-catalog/src/connect-github-routes.test.ts delete mode 100644 packages/workflow-catalog/src/connect-github-routes.ts delete mode 100644 packages/workflow-catalog/src/connect-github-setup.test.ts delete mode 100644 packages/workflow-catalog/src/connect-github-setup.ts delete mode 100644 packages/workflow-catalog/src/index.ts delete mode 100644 packages/workflow-catalog/src/instantiate.ts delete mode 100644 packages/workflow-catalog/src/participant-agent-requests.ts delete mode 100644 packages/workflow-catalog/src/reconcile-duplicate-repo-grants.ts delete mode 100644 packages/workflow-catalog/src/settings.ts delete mode 100644 packages/workflow-catalog/src/template-block-routes.test.ts delete mode 100644 packages/workflow-catalog/src/template-block-routes.ts delete mode 100644 packages/workflow-catalog/src/templates.ts delete mode 100644 packages/workflow-catalog/test/catalog.test.ts delete mode 100644 packages/workflow-catalog/test/connect-github-setup.race.test.ts delete mode 100644 packages/workflow-catalog/test/instantiate.test.ts delete mode 100644 packages/workflow-catalog/test/reconcile-duplicate-repo-grants.test.ts delete mode 100644 packages/workflow-catalog/test/settings.test.ts delete mode 100644 packages/workflow-catalog/test/templates.test.ts delete mode 100644 packages/workflow-catalog/tsconfig.json diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 03e704507..b51a8df52 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -113,7 +113,7 @@ separate action. ## Workbench Definition (shipped) `WorkbenchDefinition` (`WorkbenchDefinitionSchema` in -`@corbits/workflow-catalog`) is the one type for a named picker row: +`templates/index.ts`) is the one type for a named picker row: default agents, routines, tools, plugins `{required, optional}`, and ordered `onboardingSteps`. A template is a shipped definition, not a second kind. `instantiateWorkbenchTemplate` resolves the definition diff --git a/bun.lock b/bun.lock index 43b5e204b..74e74fca3 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,5 @@ { - "lockfileVersion": 1, + "lockfileVersion": 2, "configVersion": 1, "workspaces": { "": { @@ -14,12 +14,13 @@ "@corbits/ollama-adapter": "workspace:*", "@corbits/run-scope": "workspace:*", "@corbits/webhook-triggers": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", "@eslint/js": "^10.0.0", "@intx/db": "workspace:*", "@intx/mime": "0.3.0", "@intx/types": "0.3.0", "@types/bun": "1.3.14", + "@workbench/connections": "workspace:*", + "@workbench/templates": "workspace:*", "eslint": "^10.8.0", "jiti": "^2.7.0", "prettier": "^3.6.2", @@ -40,6 +41,7 @@ "@corbits/artifacts-hub": "workspace:*", "@corbits/bench": "workspace:*", "@corbits/chat": "workspace:*", + "@corbits/code-review-workflow": "workspace:*", "@corbits/commands": "workspace:*", "@corbits/credential-providers": "workspace:*", "@corbits/docker-provisioner": "workspace:*", @@ -68,7 +70,6 @@ "@corbits/turn-artifacts": "workspace:*", "@corbits/url-path": "workspace:*", "@corbits/webhook-triggers": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", "@corbits/workflows": "workspace:*", "@intx/authz": "0.3.0", "@intx/crypto": "0.3.0", @@ -87,6 +88,7 @@ "@workbench/echo": "workspace:*", "@workbench/hub-client": "workspace:*", "@workbench/onboarding": "workspace:*", + "@workbench/templates": "workspace:*", "arktype": "catalog:", "better-auth": "^1.4.18", "drizzle-orm": "0.45.2", @@ -162,7 +164,6 @@ "@corbits/slug": "workspace:*", "@corbits/text-diff": "workspace:*", "@corbits/url-path": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", "@corbits/workflows": "workspace:*", "@intx/types": "workspace:*", "@radix-ui/react-dialog": "^1.1.15", @@ -170,6 +171,7 @@ "@tanstack/react-query": "catalog:", "@workbench/connections": "workspace:*", "@workbench/hub-client": "workspace:*", + "@workbench/templates": "workspace:*", "arktype": "catalog:", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -214,7 +216,6 @@ "@corbits/error-sink": "workspace:*", "@corbits/folded-run-one-shot": "workspace:*", "@corbits/skills": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", "@corbits/workflows": "workspace:*", "@intx/agent": "workspace:*", "@intx/db": "workspace:*", @@ -224,6 +225,7 @@ "@intx/types": "workspace:*", "@intx/workflow": "workspace:*", "@workbench/hub-client": "workspace:*", + "@workbench/templates": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", "hono": "^4.11.9", @@ -434,7 +436,6 @@ "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "@corbits/turn-artifacts": "workspace:*", "@corbits/url-path": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", "@corbits/workflows": "workspace:*", "@intx/agent": "workspace:*", "@intx/authz": "0.3.0", @@ -473,9 +474,9 @@ "@corbits/preferences": "workspace:*", "@corbits/presence": "workspace:*", "@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635", - "@corbits/workflow-catalog": "workspace:*", "@tanstack/react-query": "catalog:", "@workbench/connections": "workspace:*", + "@workbench/templates": "workspace:*", "arktype": "catalog:", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -568,8 +569,10 @@ "name": "@workbench/connections", "version": "0.0.1", "dependencies": { + "@corbits/code-review": "workspace:*", "@corbits/credential-providers": "workspace:*", "@corbits/error-sink": "workspace:*", + "@corbits/github-tools": "workspace:*", "@corbits/inference-settings": "workspace:*", "@corbits/mcp-tools": "workspace:*", "@intx/crypto": "0.3.0", @@ -578,9 +581,11 @@ "@intx/types": "workspace:*", "@modelcontextprotocol/sdk": "catalog:", "@workbench/hub-client": "workspace:*", + "@workbench/templates": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", "hono": "^4.11.9", + "postgres": "catalog:", "simple-icons": "^16.28.0", }, "devDependencies": { @@ -691,7 +696,6 @@ "@corbits/migration-runner": "workspace:*", "@corbits/routines": "workspace:*", "@corbits/webhook-triggers": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", "@intx/agent": "workspace:*", "@intx/db": "workspace:*", "@intx/hub-api": "workspace:*", @@ -703,6 +707,7 @@ "@workbench/connections": "workspace:*", "@workbench/hub-client": "workspace:*", "@workbench/onboarding": "workspace:*", + "@workbench/templates": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", "hono": "^4.11.9", @@ -819,10 +824,10 @@ "@corbits/last-30-days-research-workflow": "workspace:*", "@corbits/tool-registry-publish": "workspace:*", "@corbits/workbench-digest-workflow": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", "@corbits/workflows": "workspace:*", "@intx/inference": "workspace:*", "@intx/types": "workspace:*", + "@workbench/templates": "workspace:*", "arktype": "catalog:", }, "devDependencies": { @@ -1114,7 +1119,6 @@ "@corbits/icons": "workspace:*", "@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635", "@corbits/settings-ui": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", "@workbench/connections": "workspace:*", "arktype": "catalog:", "react": "^19.2.0", @@ -1197,7 +1201,6 @@ "@corbits/folded-run-one-shot": "workspace:*", "@corbits/migration-runner": "workspace:*", "@corbits/slug": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", "@corbits/workflows": "workspace:*", "@intx/authz": "0.3.0", "@intx/db": "workspace:*", @@ -1206,6 +1209,7 @@ "@intx/log": "0.3.0", "@intx/types": "workspace:*", "@workbench/hub-client": "workspace:*", + "@workbench/templates": "workspace:*", "arktype": "catalog:", "cronstrue": "^3.24.0", "drizzle-orm": "catalog:", @@ -1219,7 +1223,7 @@ }, "packages/routines-tools": { "name": "@corbits/routines-tools", - "version": "0.0.9", + "version": "0.0.10", "dependencies": { "@corbits/routines": "workspace:*", "@corbits/workflows": "workspace:*", @@ -1307,9 +1311,9 @@ "@corbits/icons": "workspace:*", "@corbits/inference-settings": "workspace:*", "@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635", - "@corbits/workflow-catalog": "workspace:*", "@intx/types": "workspace:*", "@workbench/connections": "workspace:*", + "@workbench/templates": "workspace:*", "arktype": "catalog:", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -1522,19 +1526,17 @@ "typescript": "catalog:", }, }, - "packages/workflow-catalog": { - "name": "@corbits/workflow-catalog", + "packages/workflows": { + "name": "@corbits/workflows", "version": "0.0.1", "dependencies": { - "@corbits/code-review": "workspace:*", - "@corbits/code-review-workflow": "workspace:*", "@corbits/error-sink": "workspace:*", - "@corbits/github-tools": "workspace:*", - "@corbits/jimmy-agent": "workspace:*", - "@corbits/scout-agent": "workspace:*", - "@corbits/webhook-triggers": "workspace:*", + "@corbits/migration-runner": "workspace:*", + "@intx/authz": "0.3.0", "@intx/db": "workspace:*", "@intx/hub-api": "workspace:*", + "@intx/hub-sessions": "workspace:*", + "@intx/types": "workspace:*", "@workbench/hub-client": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", @@ -1542,31 +1544,22 @@ "postgres": "catalog:", }, "devDependencies": { - "@intx/crypto": "0.3.0", "@types/bun": "catalog:", - "@workbench/connections": "workspace:*", "typescript": "catalog:", }, }, - "packages/workflows": { - "name": "@corbits/workflows", + "templates": { + "name": "@workbench/templates", "version": "0.0.1", "dependencies": { - "@corbits/error-sink": "workspace:*", - "@corbits/migration-runner": "workspace:*", - "@intx/authz": "0.3.0", - "@intx/db": "workspace:*", - "@intx/hub-api": "workspace:*", - "@intx/hub-sessions": "workspace:*", - "@intx/types": "workspace:*", - "@workbench/hub-client": "workspace:*", + "@corbits/code-review": "workspace:*", + "@corbits/jimmy-agent": "workspace:*", + "@corbits/scout-agent": "workspace:*", "arktype": "catalog:", - "drizzle-orm": "catalog:", - "hono": "^4.11.9", - "postgres": "catalog:", }, "devDependencies": { "@types/bun": "catalog:", + "@workbench/connections": "workspace:*", "typescript": "catalog:", }, }, @@ -2135,13 +2128,13 @@ "@better-fetch/fetch": ["@better-fetch/fetch@1.3.1", "", {}, "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g=="], - "@bufbuild/protobuf": ["@bufbuild/protobuf@2.14.0", "", {}, "sha512-C3UGsiCwSprE2NKIIFA3hCDlpXTMCAXRZuEVp88L1GY36Y41+rYL5fryE+nOFhp4p4JPQvdV8PQ4DWgHgeTE+w=="], + "@bufbuild/protobuf": ["@bufbuild/protobuf@2.14.1", "", {}, "sha512-agRJn3+EJDUe8AvxTx/LnHA/GErvLE62pSaSk7+MwFOtOv8eWBu/qCq2qoZjBjVZ3C2aiFJCveuSs17KMkYGOw=="], - "@chat-adapter/shared": ["@chat-adapter/shared@4.38.1", "", { "dependencies": { "chat": "4.38.1" } }, "sha512-zJiMl43fiS3a2RjLFdCyTUTqcDY2xss1Acbzc9yfsw8YAkS0LZRdrNmp9/RjLhZB0+FzLarNAayvdKqF2GwnQg=="], + "@chat-adapter/shared": ["@chat-adapter/shared@4.39.0", "", { "dependencies": { "chat": "4.39.0" } }, "sha512-zc5mB5rYqbp4ts+Z3tG/YwkCypPYphriYpG95ZMRNGiEYx3cYZJZFmAFFM4YLfdz3VIQ99Fd0HFDYVNyAMV7cg=="], - "@chat-adapter/slack": ["@chat-adapter/slack@4.38.1", "", { "dependencies": { "@chat-adapter/shared": "4.38.1", "@slack/socket-mode": "^2.0.5", "@slack/web-api": "^7.18.0", "chat": "4.38.1" } }, "sha512-xMZ43YUQKzqOAiePR5mEFjhJPI6/jsEHCMq9Y+Rrm+N4+DotBddQDpbGMzb5oiogZD6iyMVCMvWSPLST7ZX0sg=="], + "@chat-adapter/slack": ["@chat-adapter/slack@4.39.0", "", { "dependencies": { "@chat-adapter/shared": "4.39.0", "@slack/socket-mode": "^2.0.5", "@slack/web-api": "^7.18.0", "chat": "4.39.0" } }, "sha512-nDBYFEmD9MUobUH+HJbOYwKYcaeoCFJDXVvd41flh5sslpixSUG2SFcz7F3py62jJ9c0YYNEdwprIExO20IyZQ=="], - "@chat-adapter/state-memory": ["@chat-adapter/state-memory@4.38.1", "", { "dependencies": { "chat": "4.38.1" } }, "sha512-XYRmnnuckJ7HubQIRqKTwOn2XbpK+GQclN7mRJEpiGk/4LXD46g0XKrYPrQMGe8PGLMdSQ4139Huu34rvyfgSw=="], + "@chat-adapter/state-memory": ["@chat-adapter/state-memory@4.39.0", "", { "dependencies": { "chat": "4.39.0" } }, "sha512-sTslMdRA9MG2MYUF9anD+eatZ1o3Nu64TkQCJ9GrGWfRCefM8DnIG/Op+Ej9XS6XjqOORb3eDJvy6IpvWrfT4A=="], "@connectrpc/connect": ["@connectrpc/connect@2.1.2", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.7.0" } }, "sha512-MXkBijtcX09R10Eb6sFeIetc6w6746eio6xtfuyVOH7oQAacT1X0GzMIQFux6Qy8cq3W/T5qX5Bei8YbFtmRGA=="], @@ -2335,8 +2328,6 @@ "@corbits/workflow-authoring-tools": ["@corbits/workflow-authoring-tools@workspace:packages/workflow-authoring-tools"], - "@corbits/workflow-catalog": ["@corbits/workflow-catalog@workspace:packages/workflow-catalog"], - "@corbits/workflows": ["@corbits/workflows@workspace:packages/workflows"], "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], @@ -2423,7 +2414,7 @@ "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], - "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.11.6", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.11.6" } }, "sha512-ZQ47qUTeNbGhHkCGExJ1oZhruoxKRaxO44RgFETl3T4c1rRxIBlAOnM3SAH4XHu7Ue2owJXP+jOx1vOuKuxcSg=="], + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.13.2", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.13.2" } }, "sha512-pGO7WhgHVLi7mi7zCLyJ+FkTad2aPTcjUXhRbgA4T5n1vLRJ/9uw3EsLieScNSX6HOfPF947bOP7N42l+KrWjg=="], "@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="], @@ -2495,21 +2486,21 @@ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@logtape/hono": ["@logtape/hono@2.3.1", "", { "peerDependencies": { "@logtape/logtape": "^2.3.1", "hono": "^4.0.0" } }, "sha512-ryqg3HBdd79M0/wkiDIcFyB6RXBM1laauYDGWVqAVMrUu53mqRp5lDXrlQYmbIZBtyAyZK/TS2YJR22/79NzpQ=="], + "@logtape/hono": ["@logtape/hono@2.3.2", "", { "peerDependencies": { "@logtape/logtape": "^2.3.2", "hono": "^4.0.0" } }, "sha512-fwjfktpQSucHQCO6OYCHUrcgwHPhU41VKTJt2gtqSoW3MjYMUtdZJTGJx2JTAj+8zyQ1cXqugkEKVG07wwSU4g=="], - "@logtape/logtape": ["@logtape/logtape@2.3.1", "", {}, "sha512-nYLsBWMKk0Llp62U6cDbQxNcEKc0E0s5hWbAxkmUM8ax6VchGkPSjPrVp3ixFQUuvUB9ku7MSgPzFhmVTSh2Jw=="], + "@logtape/logtape": ["@logtape/logtape@2.3.2", "", {}, "sha512-SfxHfSdDlTp6tLpYP5uezNitSCTQUB3FZ1MXDbgDBN9t9kvF+XL0rOGTIa/pJcDMy1o9FELaZ25E/Ypmsf5fNQ=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="], "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], - "@noble/ciphers": ["@noble/ciphers@2.3.0", "", {}, "sha512-Clu/xdfgVTf9o7ngLOURaxePwR0j8sjclKEtVij10/jGulwFsPWCvvRgG/XjUVf8Nei+jLG6uwyXzUTGY1DQrw=="], + "@noble/ciphers": ["@noble/ciphers@2.4.0", "", {}, "sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw=="], - "@noble/hashes": ["@noble/hashes@2.3.0", "", {}, "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ=="], + "@noble/hashes": ["@noble/hashes@2.4.0", "", {}, "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA=="], "@npmcli/agent": ["@npmcli/agent@4.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg=="], @@ -2583,55 +2574,55 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.5", "", { "os": "android", "cpu": "arm" }, "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.1", "", { "os": "android", "cpu": "arm" }, "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.5", "", { "os": "android", "cpu": "arm64" }, "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.1", "", { "os": "android", "cpu": "arm64" }, "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.63.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.63.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.63.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.63.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.63.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.63.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.5", "", { "os": "none", "cpu": "arm64" }, "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.63.1", "", { "os": "none", "cpu": "arm64" }, "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.63.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.63.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w=="], "@slack/logger": ["@slack/logger@4.0.1", "", { "dependencies": { "@types/node": ">=18" } }, "sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ=="], @@ -2677,9 +2668,9 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], - "@tanstack/query-core": ["@tanstack/query-core@5.101.4", "", {}, "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw=="], + "@tanstack/query-core": ["@tanstack/query-core@5.102.8", "", {}, "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg=="], - "@tanstack/react-query": ["@tanstack/react-query@5.101.4", "", { "dependencies": { "@tanstack/query-core": "5.101.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA=="], + "@tanstack/react-query": ["@tanstack/react-query@5.102.8", "", { "dependencies": { "@tanstack/query-core": "5.102.8" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-TYBea4OuXWD7MhaSHq069TWbFe7rcwWN6kzT7JF0OKi1K6c1gTv2IzD6A6ExJsCMozdkqBWeuIUZmu4KQg0O5A=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -2703,11 +2694,11 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="], + "@types/node": ["@types/node@26.4.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA=="], "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], - "@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="], + "@types/react-dom": ["@types/react-dom@19.2.5", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg=="], "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], @@ -2721,25 +2712,25 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.67.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.67.0", "@typescript-eslint/type-utils": "8.67.0", "@typescript-eslint/utils": "8.67.0", "@typescript-eslint/visitor-keys": "8.67.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.67.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.69.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.69.0", "@typescript-eslint/type-utils": "8.69.0", "@typescript-eslint/utils": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.69.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.67.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.67.0", "@typescript-eslint/types": "8.67.0", "@typescript-eslint/typescript-estree": "8.67.0", "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.69.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.69.0", "@typescript-eslint/types": "8.69.0", "@typescript-eslint/typescript-estree": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.67.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.67.0", "@typescript-eslint/types": "^8.67.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.69.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.69.0", "@typescript-eslint/types": "^8.69.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "@typescript-eslint/visitor-keys": "8.67.0" } }, "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.69.0", "", { "dependencies": { "@typescript-eslint/types": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0" } }, "sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.67.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.69.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "@typescript-eslint/typescript-estree": "8.67.0", "@typescript-eslint/utils": "8.67.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.69.0", "", { "dependencies": { "@typescript-eslint/types": "8.69.0", "@typescript-eslint/typescript-estree": "8.69.0", "@typescript-eslint/utils": "8.69.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.67.0", "", {}, "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.69.0", "", {}, "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.67.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.67.0", "@typescript-eslint/tsconfig-utils": "8.67.0", "@typescript-eslint/types": "8.67.0", "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.69.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.69.0", "@typescript-eslint/tsconfig-utils": "8.69.0", "@typescript-eslint/types": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.67.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.67.0", "@typescript-eslint/types": "8.67.0", "@typescript-eslint/typescript-estree": "8.67.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.69.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.69.0", "@typescript-eslint/types": "8.69.0", "@typescript-eslint/typescript-estree": "8.69.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.69.0", "", { "dependencies": { "@typescript-eslint/types": "8.69.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], @@ -2759,6 +2750,8 @@ "@workbench/sidecar": ["@workbench/sidecar@workspace:apps/sidecar"], + "@workbench/templates": ["@workbench/templates@workspace:templates"], + "@workbench/web": ["@workbench/web@workspace:apps/web"], "@workflow/serde": ["@workflow/serde@4.1.0-beta.2", "", {}, "sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww=="], @@ -2793,7 +2786,7 @@ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "axios": ["axios@1.19.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw=="], + "axios": ["axios@1.20.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], @@ -2801,7 +2794,7 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.11.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H/bNPUFHewJHyCTdjn1n3Pit5+2GmWT6mmeHImPX+8MA9NA6b67jO4gYmi4jTbCJb2otq34KMZnovndDPqJwhQ=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw=="], "better-auth": ["better-auth@1.6.29", "", { "dependencies": { "@better-auth/core": "1.6.29", "@better-auth/drizzle-adapter": "1.6.29", "@better-auth/kysely-adapter": "1.6.29", "@better-auth/memory-adapter": "1.6.29", "@better-auth/mongo-adapter": "1.6.29", "@better-auth/prisma-adapter": "1.6.29", "@better-auth/telemetry": "1.6.29", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.4.0", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-7i2kOry+Jb1mRUR14kCDVS23GCKUGWxayZRXULbNz/6nFLPT6XSuGJVrwo/d+Qfq/iubA0/4Acw2/eg/tPCCnA=="], @@ -2831,7 +2824,7 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -2839,7 +2832,7 @@ "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], - "chat": ["chat@4.38.1", "", { "dependencies": { "@workflow/serde": "4.1.0-beta.2", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "remend": "^1.2.1", "unified": "^11.0.5" }, "peerDependencies": { "ai": "^6.0.182 || ^7.0.0", "workflow": "^5.0.0-beta.35", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai", "workflow", "zod"] }, "sha512-FD6vNLT8clXHKhN3I1m+3Bn2nz0rsB6/bmNPsJeh6Cr7kwWLg0MYl2uor9YaKBp38F77sBRVKdYKVA6ihUby3w=="], + "chat": ["chat@4.39.0", "", { "dependencies": { "@workflow/serde": "4.1.0-beta.2", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "remend": "^1.2.1", "unified": "^11.0.5" }, "peerDependencies": { "ai": "^6.0.182 || ^7.0.0", "workflow": "^5.0.0-beta.35", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai", "workflow", "zod"] }, "sha512-o7D+oAbQAYFVoc9+OH8wya11609ybRThNTF3s/5C1LAABGGi6IrxoQQEN/aIe+4h5pmKYzUjXeqTcuOZcpIAZA=="], "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], @@ -2915,11 +2908,11 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "e2b": ["e2b@2.44.1", "", { "dependencies": { "@bufbuild/protobuf": "^2.12.1", "@connectrpc/connect": "^2.1.2", "@connectrpc/connect-web": "^2.1.2", "chalk": "^5.3.0", "compare-versions": "^6.1.0", "dockerfile-ast": "^0.7.1", "glob": "^13.0.6", "openapi-fetch": "^0.14.1", "platform": "^1.3.6", "tar": "^7.5.19", "undici": "^7.29.0" }, "optionalDependencies": { "undici8": "npm:undici@8.10.0" } }, "sha512-6yx3FjYnR2zlidvF5QLaFQAtv7WndKAIrP8NEi9lhJm5+98Iinb7zHYel/Ug/o9BEZTb7ODYUvD/0P9b2Q6ufg=="], + "e2b": ["e2b@2.46.1", "", { "dependencies": { "@bufbuild/protobuf": "^2.14.0", "@connectrpc/connect": "^2.1.2", "@connectrpc/connect-web": "^2.1.2", "chalk": "^5.3.0", "compare-versions": "^6.1.0", "dockerfile-ast": "^0.7.1", "glob": "^13.0.6", "openapi-fetch": "^0.14.1", "platform": "^1.3.6", "tar": "^7.5.22", "undici": "^7.29.0" }, "optionalDependencies": { "undici8": "npm:undici@8.10.0" } }, "sha512-OqYovS2oFrt4mk737CgfW/RoMadBYK84l5qjKpvbEoOB9KKxaZIXm7YUwOKSRTlijrrwDRX7oZlyPoVXiCpyTw=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.411", "", {}, "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.420", "", {}, "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -2945,7 +2938,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@10.8.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ=="], + "eslint": ["eslint@10.9.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A=="], "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], @@ -2975,7 +2968,7 @@ "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="], + "express-rate-limit": ["express-rate-limit@8.7.0", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -2987,7 +2980,7 @@ "fast-text-encoding": ["fast-text-encoding@1.0.6", "", {}, "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w=="], - "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="], + "fast-uri": ["fast-uri@3.1.7", "", {}, "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -3039,7 +3032,7 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "happy-dom": ["happy-dom@20.11.6", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-Hldbg8AdAa5a5oDcZpjqnGitp7JB0hqWmfv/8qr+kft4vzSD8BHsbdRfzYvL/0QcbKcURC/yyoygbeDQarPvYg=="], + "happy-dom": ["happy-dom@20.13.2", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-VC9HoEaT3Jf3K/1g/GjMYGAZCCg+ULgGVcXJlAeSzdTj8mIoPJRp14KVclP5gpUcF/kauuaVB2ri2/WxXbl4vg=="], "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], @@ -3073,7 +3066,7 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="], + "ip-address": ["ip-address@10.7.0", "", {}, "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -3097,7 +3090,7 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "isomorphic-git": ["isomorphic-git@1.41.7", "", { "dependencies": { "async-lock": "^1.4.1", "clean-git-ref": "^2.0.1", "crc-32": "^1.2.0", "diff3": "0.0.3", "ignore": "^5.1.4", "minimisted": "^2.0.0", "pako": "^1.0.10", "pify": "^4.0.1", "readable-stream": "^4.0.0", "sha.js": "^2.4.12", "simple-get": "^4.0.1" }, "bin": { "isogit": "cli.cjs" } }, "sha512-ZCyMNg0eLUmuhGXdVQFi56fM93kwIqqlUSVnWbgzf1oDYidrQbWfrf+hEvn2E8xyo191zRPQyDCIlwg6YJ6isQ=="], + "isomorphic-git": ["isomorphic-git@1.41.9", "", { "dependencies": { "async-lock": "^1.4.1", "clean-git-ref": "^2.0.1", "crc-32": "^1.2.0", "diff3": "0.0.3", "ignore": "^5.1.4", "minimisted": "^2.0.0", "pako": "^1.0.10", "pify": "^4.0.1", "readable-stream": "^4.0.0", "sha.js": "^2.4.12", "simple-get": "^4.0.1" }, "bin": { "isogit": "cli.cjs" } }, "sha512-WOh4ujm5mznHphzQvwj9bVj+pQpV0oZ8H4lAnh4dSaie+lN/lJ2rb6rNOOcP+2m4z8IOQp186TkEULD28NMBJg=="], "isomorphic-textencoder": ["isomorphic-textencoder@1.0.1", "", { "dependencies": { "fast-text-encoding": "^1.0.0" } }, "sha512-676hESgHullDdHDsj469hr+7t3i/neBKU9J7q1T4RHaWwLAsaQnywC0D1dIUId0YZ+JtVrShzuBk1soo0+GVcQ=="], @@ -3105,7 +3098,7 @@ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "jose": ["jose@6.2.9", "", {}, "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA=="], + "jose": ["jose@6.2.10", "", {}, "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -3165,7 +3158,7 @@ "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], - "lucide-react": ["lucide-react@1.33.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-MTRwMy0ZlL8Ur/vOAiJ9XGHE+kFPC7brq6MxAm0GiGXEBj0qy0jA/pG4N675oSzciO/UCdX8T+5yUQdmDeTLxg=="], + "lucide-react": ["lucide-react@1.39.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y8nXoEwvqqIsF927NBWXODa4bfMrcUeEb/9sgpwFqg0gUjgn3j5Hznk+v7STmPgZ2iQ11JKlbQGdFRuTOwvYkA=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -3291,13 +3284,13 @@ "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], - "nanostores": ["nanostores@1.5.2", "", {}, "sha512-B0UbxzK1s0CN8Xht6r+7iT5+xV8PTaRERR1nATeplRv1Rw5YLWfVAid0hkqY3EceqpG4RjTk8GAwIxQY39Rnwg=="], + "nanostores": ["nanostores@1.5.3", "", {}, "sha512-rQLB6eV4f2AW/n3L0JmwCROpaisYy9EDEADvEFSd1C/qG8hB6O5TPlh9A791JRbJr4CnMQBzptDcvD9OR1+6WA=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], - "node-releases": ["node-releases@2.0.53", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="], + "node-releases": ["node-releases@2.0.54", "", {}, "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ=="], "npm-install-checks": ["npm-install-checks@7.1.2", "", { "dependencies": { "semver": "^7.1.1" } }, "sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ=="], @@ -3331,7 +3324,7 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "p-map": ["p-map@7.0.6", "", {}, "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg=="], + "p-map": ["p-map@7.0.7", "", {}, "sha512-VaWRu2i4FJNRtiRWCuuQRgfQ1B7a6+gMSrO+3j0EQi/k0ULfS9kosRxGoiqwzIjZTDI02tGfk5mXXltLg6QtfQ=="], "p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], @@ -3353,7 +3346,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], @@ -3381,9 +3374,9 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "puppeteer-core": ["puppeteer-core@25.8.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.1", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1666840", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.1" } }, "sha512-LDOrawV8vfCVk+yLj2ozvajNP4Sv3OV9y3Tpiyy2g2Z+aQlbcozP6KJfI4iSBq7YQER+86ihEtPa5ioiZyWxMQ=="], + "puppeteer-core": ["puppeteer-core@25.9.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.1", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1666840", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.3" } }, "sha512-U61rCwSMha62CA/Opy6tCx2Fx+ck7ouiKnbpEApzSoLYMoEu9F71nuFpHL55vmIt33/GYm6eKZVhH2ev0nAIeg=="], - "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], + "qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -3411,7 +3404,7 @@ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], - "remend": ["remend@1.3.0", "", {}, "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw=="], + "remend": ["remend@1.3.1", "", {}, "sha512-N3DiY5qbRPoa5vkxn1oDLMyXOVTeo6Hp+XOj6SIqJAYUgLS0Q587gILPMom/qm86AQ/ZrcOdwEIzCz8V3J0nxQ=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -3419,7 +3412,7 @@ "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], - "rollup": ["rollup@4.62.5", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.5", "@rollup/rollup-android-arm64": "4.62.5", "@rollup/rollup-darwin-arm64": "4.62.5", "@rollup/rollup-darwin-x64": "4.62.5", "@rollup/rollup-freebsd-arm64": "4.62.5", "@rollup/rollup-freebsd-x64": "4.62.5", "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", "@rollup/rollup-linux-arm-musleabihf": "4.62.5", "@rollup/rollup-linux-arm64-gnu": "4.62.5", "@rollup/rollup-linux-arm64-musl": "4.62.5", "@rollup/rollup-linux-loong64-gnu": "4.62.5", "@rollup/rollup-linux-loong64-musl": "4.62.5", "@rollup/rollup-linux-ppc64-gnu": "4.62.5", "@rollup/rollup-linux-ppc64-musl": "4.62.5", "@rollup/rollup-linux-riscv64-gnu": "4.62.5", "@rollup/rollup-linux-riscv64-musl": "4.62.5", "@rollup/rollup-linux-s390x-gnu": "4.62.5", "@rollup/rollup-linux-x64-gnu": "4.62.5", "@rollup/rollup-linux-x64-musl": "4.62.5", "@rollup/rollup-openbsd-x64": "4.62.5", "@rollup/rollup-openharmony-arm64": "4.62.5", "@rollup/rollup-win32-arm64-msvc": "4.62.5", "@rollup/rollup-win32-ia32-msvc": "4.62.5", "@rollup/rollup-win32-x64-gnu": "4.62.5", "@rollup/rollup-win32-x64-msvc": "4.62.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw=="], + "rollup": ["rollup@4.63.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.1", "@rollup/rollup-android-arm64": "4.63.1", "@rollup/rollup-darwin-arm64": "4.63.1", "@rollup/rollup-darwin-x64": "4.63.1", "@rollup/rollup-freebsd-arm64": "4.63.1", "@rollup/rollup-freebsd-x64": "4.63.1", "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", "@rollup/rollup-linux-arm-musleabihf": "4.63.1", "@rollup/rollup-linux-arm64-gnu": "4.63.1", "@rollup/rollup-linux-arm64-musl": "4.63.1", "@rollup/rollup-linux-loong64-gnu": "4.63.1", "@rollup/rollup-linux-loong64-musl": "4.63.1", "@rollup/rollup-linux-ppc64-gnu": "4.63.1", "@rollup/rollup-linux-ppc64-musl": "4.63.1", "@rollup/rollup-linux-riscv64-gnu": "4.63.1", "@rollup/rollup-linux-riscv64-musl": "4.63.1", "@rollup/rollup-linux-s390x-gnu": "4.63.1", "@rollup/rollup-linux-x64-gnu": "4.63.1", "@rollup/rollup-linux-x64-musl": "4.63.1", "@rollup/rollup-openbsd-x64": "4.63.1", "@rollup/rollup-openharmony-arm64": "4.63.1", "@rollup/rollup-win32-arm64-msvc": "4.63.1", "@rollup/rollup-win32-ia32-msvc": "4.63.1", "@rollup/rollup-win32-x64-gnu": "4.63.1", "@rollup/rollup-win32-x64-msvc": "4.63.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg=="], "rou3": ["rou3@0.9.2", "", {}, "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ=="], @@ -3461,7 +3454,7 @@ "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], - "simple-icons": ["simple-icons@16.28.0", "", {}, "sha512-sQPR5AtK/ijRjou7zw7mlLp08oB6FH7i0lOy5XJ2zp9mJs/yejgiOn7KvQoe2q4YJIx6VmgUSW5AOefebPt5kg=="], + "simple-icons": ["simple-icons@16.29.0", "", {}, "sha512-4H94f5ZgcCcgJroc902TFlFdgPu2IU2eD7+WebN2Z14xKYrKHeJ4UQcZwzgSuH4Rgzw+jp7sbHl40NefuIEYsg=="], "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], @@ -3507,7 +3500,7 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tsx": ["tsx@4.23.12", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q=="], + "tsx": ["tsx@4.23.13", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -3519,7 +3512,7 @@ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "typescript-eslint": ["typescript-eslint@8.67.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.67.0", "@typescript-eslint/parser": "8.67.0", "@typescript-eslint/typescript-estree": "8.67.0", "@typescript-eslint/utils": "8.67.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg=="], + "typescript-eslint": ["typescript-eslint@8.69.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.69.0", "@typescript-eslint/parser": "8.69.0", "@typescript-eslint/typescript-estree": "8.69.0", "@typescript-eslint/utils": "8.69.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-B3MltX0VqjUBNEe3b3sSuiRbfa6XrfHFtBiPamjT5AsW/dfq+y+bc0wyuS9DxAS1LyzCxRp2+rxzpLUvqM2BvA=="], "undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], @@ -3539,7 +3532,7 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - "update-browserslist-db": ["update-browserslist-db@1.3.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ=="], + "update-browserslist-db": ["update-browserslist-db@1.3.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -3557,9 +3550,9 @@ "vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="], - "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.14", "", {}, "sha512-EQyqJMi552E4ZTf46izQ4Fj6XquqxCySR3J5ZSD1SisMf6RfpeOWHxGBE8Gr6V0/3GHIGdAzDn8F8+1nTGCnoQ=="], - "vscode-languageserver-types": ["vscode-languageserver-types@3.18.0", "", {}, "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g=="], + "vscode-languageserver-types": ["vscode-languageserver-types@3.18.3", "", {}, "sha512-XIlzJ7Qp/jzSI1ds7/FwPAWrPeTZA7pAtlW4hdJ1J6xXWJL6dR9QYnDhJOdLzdKhUQ5Mm6mvUMw+3DcOQQasPw=="], "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.2", "", {}, "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA=="], @@ -3591,7 +3584,7 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], @@ -3603,8 +3596,6 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@corbits/memory-hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#9e6f213", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-9e6f213", "sha512-utnM4ZT2zmslcPXYWAAqxlDNLcpGsXFiTOtj8h7+OXnhCP0Eaw8yl25+yCTyHpvt3jcdeG4h5uFsSj7ou0BZCA=="], - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -3625,9 +3616,7 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], - - "@workbench/hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#9e6f213", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-9e6f213", "sha512-utnM4ZT2zmslcPXYWAAqxlDNLcpGsXFiTOtj8h7+OXnhCP0Eaw8yl25+yCTyHpvt3jcdeG4h5uFsSj7ou0BZCA=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.8", "", {}, "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q=="], "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index d78cab40d..5d0255431 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -17,7 +17,7 @@ lower-level name left to list. | **DM** | kind: chat | The one 1:1 tenant with that agent — two opens never clone a second DM | | **Channel** | kind: workbench | A shared room between people and agents (multi-principal tenant). `+` mints an empty one with nobody hosted; named templates mint the same empty channel, then instantiate their Workbench Definition and run its onboarding walkthrough in the room | | **Definition** | workflow definition | A deployable unit of agent behavior, authored as code — not a template you mint into many conversations | -| **Workbench Definition** | — | The arktype description a named template instantiates: its default agents, routines, tools, required/optional plugins, and its ordered onboarding walkthrough — see `packages/workflow-catalog/src/templates.ts`'s `WorkbenchDefinitionSchema` | +| **Workbench Definition** | — | The arktype description a named template instantiates: its default agents, routines, tools, required/optional plugins, and its ordered onboarding walkthrough — see `templates/index.ts`'s `WorkbenchDefinitionSchema` | | **Run** | workflow run | A definition executing in a bench; interactive runs carry conversations | | **Routine** | — | The named parent entity over runs of one definition — a trigger (or none), launch input, a delivery workbench, enabled state, and an explicit target: the definition's asset id, resolved at each launch to its latest approved deployment; see [`@corbits/routines`](../packages/routines/README.md) and [workflow-model.md](workflow-model.md) | | **Approval** | approval | A human decision gating an external side effect | diff --git a/docs/workflow-detail.md b/docs/workflow-detail.md index dd5774858..3c3e9721a 100644 --- a/docs/workflow-detail.md +++ b/docs/workflow-detail.md @@ -2,7 +2,7 @@ A workflow definition's own page: `GET /api/tenants/:tenantId/workflows/definitions/:definitionAssetId/detail` -(`packages/workflow-catalog/src/detail-route.ts`), read at `/workflows/ +(`packages/workflows/src/detail`), read at `/workflows/ :definitionAssetId` (`apps/web/src/pages/workflow-detail-page.tsx`). First useful version — read-only, no editing surface here. diff --git a/package.json b/package.json index 5d689d777..56af4e10b 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "workspaces": [ "apps/*", "packages/*", + "templates", "tools/*", "vendor/intx/*", "workflows/*" @@ -54,8 +55,9 @@ "@corbits/artifacts-hub": "workspace:*", "@corbits/evals": "workspace:*", "@corbits/ollama-adapter": "workspace:*", - "@corbits/workflow-catalog": "workspace:*", "@intx/db": "workspace:*", + "@workbench/connections": "workspace:*", + "@workbench/templates": "workspace:*", "@eslint/js": "^10.0.0", "@intx/mime": "0.3.0", "@intx/types": "0.3.0", diff --git a/packages/workflow-catalog/LICENSE b/packages/workflow-catalog/LICENSE deleted file mode 100644 index c6487f4fd..000000000 --- a/packages/workflow-catalog/LICENSE +++ /dev/null @@ -1,176 +0,0 @@ -GNU LESSER GENERAL PUBLIC LICENSE - -Version 2.1, February 1999 - -Copyright (C) 1991, 1999 Free Software Foundation, Inc. -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] - -Preamble - -The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. - -This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. - -When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. - -To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. - -For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. - -We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. - -To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. - -Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. - -Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. - -When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. - -We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. - -For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. - -In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. - -Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. - -The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. - -GNU LESSER GENERAL PUBLIC LICENSE -TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - -0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". - -A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. - -The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) - -"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. - -Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. - -1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. - -You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. - -2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. - -(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. - -3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. - -Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. - -This option is useful when you wish to copy part of the code of the Library into a program that is not a library. - -4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. - -If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. - -5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. - -However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. - -When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. - -If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) - -Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. - -6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. - -You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: - - a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. - - e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. - -For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. - -It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. - -7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. - - b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. - -8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. - -9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. - -10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. - -11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. - -This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. - -12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. - -13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. - -14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. - -NO WARRANTY - -15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Libraries - -If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). - -To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - one line to give the library's name and an idea of what it does. - Copyright (C) year name of author - - This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: - -Yoyodyne, Inc., hereby disclaims all copyright interest in -the library `Frob' (a library for tweaking knobs) written -by James Random Hacker. - -signature of Ty Coon, 1 April 1990 -Ty Coon, President of Vice -That's all there is to it! diff --git a/packages/workflow-catalog/README.md b/packages/workflow-catalog/README.md deleted file mode 100644 index 5f71cbbcd..000000000 --- a/packages/workflow-catalog/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# @corbits/workflow-catalog - -Deploy-layer metadata for every seeded workflow package: display name, -whether it's schedulable as a routine, where its result actually lands -(a channel vs. the creator's Inbox only), required connections, and -optional trigger-input fields. Interchange's `defineWorkflow` has no -automatable/display-name concept of its own, so each `workflows/*/package.json` -carries a `corbits.workflow` block, and this package is the TypeScript -mirror both the seed script and the routines create picker import — the -browser never reads `package.json` at runtime. - -Also the home of **workbench templates** (`src/templates.ts`): a named -workbench worth having, assembled out of several catalog workflows, the -routines and webhook triggers that keep it running, the agents a person -talks to in it, and the open questions only they can answer. `apps/web`'s -`/new` picker and `instant-agent-create.ts` both read this as the single -description of what picking a template actually creates. - -## Composition with @intx/* - -No direct `@intx/*` imports. `src/templates.ts` and `src/instantiate.ts` -depend on `@corbits/code-review`, but only through its `./reviewers` and -`./agent-requests` subpaths — never the package root, which also -re-exports the review run and GitHub client and pulls in -`@corbits/github-tools` and `@intx/agent`'s full provider surface. Those -two files have no imports of their own, so this stays off that graph. -It is also consumed by `@corbits/routines` (to decide whether a routine -needs a delivery channel and to validate trigger input) and by -`@workbench/connections` (dev dependency only, for the connector-id type -trigger fields reference). - -## Key modules - -- `src/index.ts` — the catalog: `WORKFLOW_CATALOG` (the array of - `WorkflowCatalogEntry` records, one per seeded workflow), `WorkflowTriggerField` - (the arktype schema for a named trigger input), and the lookup helpers - `isAutomatableWorkflowName`, `deliveryChannelRequiredForWorkflowName`, - `workflowCatalogEntry`, `workflowDisplayName`, and - `validateTriggerFieldsInput`. -- `src/templates.ts` — `WorkbenchDefinition` (default agents, routines, - tools, plugins, and the ordered `onboardingSteps` walkthrough) and the - shipped definitions themselves (`GTM_TEMPLATE`, - `CODE_REVIEW_TEMPLATE`, `DUE_DILIGENCE_TEMPLATE`), each validated at - module load; `workbenchTemplate(id)` looks one up. -- `src/instantiate.ts` — `instantiateWorkbenchTemplate`: resolves a - definition against a bench over injected ports (create the agent - definitions that don't exist yet, record required plugins as pending, - then begin the definition's onboarding walkthrough). Today this only - supports a definition whose agents are backed by - `@corbits/code-review`'s reviewer roster or a standalone chat agent - this package can build a create request for. -- `src/settings.ts` — the `template/*` workbench-settings vocabulary a - template-instantiated room persists about itself. - -## Keeping the mirror in sync - -Each `workflows/*/package.json`'s `corbits.workflow` block is the -npm-visible source of truth for package authors; `WORKFLOW_CATALOG` here is -what runtime code actually reads. Adding or changing a workflow's catalog -facts means updating both — there is no codegen step keeping them in sync. - -## Running tests - -``` -cd packages/workflow-catalog && bun test -``` - -No drizzle suite; no `DATABASE_URL` needed. diff --git a/packages/workflow-catalog/package.json b/packages/workflow-catalog/package.json deleted file mode 100644 index 938edddfd..000000000 --- a/packages/workflow-catalog/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@corbits/workflow-catalog", - "private": true, - "description": "Deploy-layer workflow metadata (automatable, display names) mirrored from each workflows/*/package.json corbits.workflow field \u2014 the routines picker and seed share this pure catalog so the browser never reads package.json at runtime", - "version": "0.0.1", - "license": "LGPL-2.1-or-later", - "type": "module", - "exports": { - ".": "./src/index.ts", - "./connect-github-routes": "./src/connect-github-routes.ts", - "./template-block-routes": "./src/template-block-routes.ts" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "@corbits/code-review": "workspace:*", - "@corbits/code-review-workflow": "workspace:*", - "@corbits/error-sink": "workspace:*", - "@corbits/github-tools": "workspace:*", - "@corbits/jimmy-agent": "workspace:*", - "@corbits/scout-agent": "workspace:*", - "@corbits/webhook-triggers": "workspace:*", - "@intx/db": "workspace:*", - "@intx/hub-api": "workspace:*", - "@workbench/hub-client": "workspace:*", - "arktype": "catalog:", - "drizzle-orm": "catalog:", - "hono": "^4.11.9", - "postgres": "catalog:" - }, - "devDependencies": { - "@intx/crypto": "0.3.0", - "@types/bun": "catalog:", - "@workbench/connections": "workspace:*", - "typescript": "catalog:" - } -} diff --git a/packages/workflow-catalog/src/block-workflows.ts b/packages/workflow-catalog/src/block-workflows.ts deleted file mode 100644 index 2d9287bec..000000000 --- a/packages/workflow-catalog/src/block-workflows.ts +++ /dev/null @@ -1,63 +0,0 @@ -// The source form a template's referenced block workflow deploys as — -// the missing half of instantiation CL-6405 closes: `./instantiate.ts` -// resolves a manifest's participants through the agent-directory create -// path, and this module resolves its `blocks` into the same -// source-form deploy (a serialized definition rendered into a -// `@corbits/workflows`'s `./source` tree and projected onto a -// `workflow_definition` row — see `./template-block-routes.ts` and the -// hub's `deployWorkflowSource` binding). -// -// Server-only, on purpose: building `code-review`'s definition pulls in -// `@corbits/code-review-workflow` and with it `@intx/agent`/`@intx/workflow` -// — the heavy graph `./templates.ts` keeps every manifest consumer off. -// Only `./template-block-routes.ts` (mounted in `apps/hub`) imports -// this; it is deliberately not re-exported from the package root. - -import { - buildCodeReviewWorkflow, - serializeCodeReviewWorkflow, - type CodeReviewWorkflowInput, -} from "@corbits/code-review-workflow"; - -import { workflowCatalogEntry } from "./index"; - -// Matches the conversational default every folded builder in this -// codebase uses (`AGENT_DEFINITION_TURN_TIMEOUT_MS`, -// `ASSISTANT_TURN_TIMEOUT_MS`): a review turn reads a diff and posts -// one review, the same order of work as a research or assistant turn. -const BLOCK_WORKFLOW_TURN_TIMEOUT_MS = 2 * 60 * 1000; - -export interface BlockWorkflowBuildInput { - readonly tenantDomain: string; - readonly inferencePreferences: CodeReviewWorkflowInput["inferencePreferences"]; -} - -export interface BlockWorkflowSource { - readonly assetName: string; - readonly displayName: string; - readonly workflowJson: string; -} - -/** - * The serialized source-form definition for one template block, or - * `undefined` for an asset name no template block builder covers yet — - * the GTM blocks resolve through their own (future) path, exactly like - * `instantiateWorkbenchTemplate`'s participants; a route answering - * `undefined` as a 404 is the honest statement of that gap. - */ -export function buildBlockWorkflowSource( - assetName: string, - input: BlockWorkflowBuildInput, -): BlockWorkflowSource | undefined { - if (assetName !== "code-review") return undefined; - const definition = buildCodeReviewWorkflow({ - triggerAddress: `${assetName}@${input.tenantDomain}`, - inferencePreferences: input.inferencePreferences, - turnTimeoutMs: BLOCK_WORKFLOW_TURN_TIMEOUT_MS, - }); - return { - assetName, - displayName: workflowCatalogEntry(assetName)?.displayName ?? assetName, - workflowJson: serializeCodeReviewWorkflow(definition), - }; -} diff --git a/packages/workflow-catalog/src/connect-github-credential-link.test.ts b/packages/workflow-catalog/src/connect-github-credential-link.test.ts deleted file mode 100644 index c0b5b311b..000000000 --- a/packages/workflow-catalog/src/connect-github-credential-link.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -// CL-6463: the room's GitHub connect card submits a PAT through -// `@workbench/connections`' generic `persistConnectorCredential` (the -// exact sequence `POST /:connectorId/complete` runs) and reads its own -// connected state back through `createConnectGithubRoutes`' injected -// `resolveGithubConfig`. A host binds both against one real credential -// store, keyed by the `github` connector's own `CONNECTOR_REGISTRY` -// `displayName` (`apps/hub/src/index.ts`) -- never a second, hardcoded -// name literal that can drift out of step with it. This suite proves that -// contract with the real `persistConnectorCredential` and -// `createConnectGithubRoutes` functions, faking only the database layer -// underneath them: no real network, no real database. -import { describe, expect, test } from "bun:test"; -import { Hono } from "hono"; -import type { MiddlewareHandler } from "hono"; -import type { RequireGrant, TenantEnv } from "@intx/hub-api"; -import type { ApiCall, EnsureCredentialArgs } from "@workbench/hub-client"; -import { - CONNECTOR_REGISTRY, - persistConnectorCredential, -} from "@workbench/connections"; - -import { createConnectGithubRoutes } from "./connect-github-routes"; - -const TENANT = { - id: "tnt_1", - name: "Acme", - slug: "acme", - domain: "acme.example", - parentId: null, - config: null, - createdAt: new Date(), - updatedAt: new Date(), -}; - -const PRINCIPAL = { - id: "prn_alice", - tenantId: TENANT.id, - kind: "user" as const, - refId: "prn_alice", - status: "active" as const, - createdAt: new Date(), - updatedAt: new Date(), -}; - -const allowAll: RequireGrant = () => async (_c, next) => { - await next(); -}; - -function mountAs(routes: Hono): Hono { - const asTenant: MiddlewareHandler = async (c, next) => { - c.set("tenant", TENANT); - c.set("principal", PRINCIPAL); - await next(); - }; - const app = new Hono(); - app.use("*", asTenant); - app.route("/", routes); - return app; -} - -const fakeApi: ApiCall = async () => ({ - status: 200, - data: undefined, - cookies: [], -}); - -/** A minimal stand-in for the `credential` table: one row per name, the - * exact shape `resolveCredentialByName` reads and `ensureCredential` - * writes -- keyed by whatever name the caller passes, never assumed. */ -function createFakeCredentialStore() { - const rowsByName = new Map(); - let nextId = 1; - const ensureCredentialFn = async ( - _api: ApiCall, - _cookies: string[], - args: EnsureCredentialArgs, - ) => { - const id = `crd_${String(nextId)}`; - nextId += 1; - rowsByName.set(args.name, { id, secret: args.secret }); - return id; - }; - return { - ensureCredentialFn, - resolveByName: (name: string) => rowsByName.get(name), - }; -} - -/** Builds the `resolveGithubConfig` port the exact way - * `apps/hub/src/index.ts` does: read the credential row named by the - * `github` connector's own `displayName`, never a second literal. */ -function buildResolveGithubConfig( - store: ReturnType, - credentialName: string, -) { - return async (_tenantId: string) => { - const row = store.resolveByName(credentialName); - return row === undefined ? undefined : { apiKey: row.secret }; - }; -} - -/** `CONNECTOR_REGISTRY["github"]` is a fixed registry entry that always - * exists; a missing one is a broken test fixture, not a case to assert - * away with a non-null assertion. */ -function requireGithubDescriptor() { - const descriptor = CONNECTOR_REGISTRY["github"]; - if (descriptor === undefined) { - throw new Error('CONNECTOR_REGISTRY has no "github" entry'); - } - return descriptor; -} - -describe("the room GitHub connect card reads what its own submit writes", () => { - test("a PAT submitted through the connections' complete sequence is reported connected by the card's own state route", async () => { - const store = createFakeCredentialStore(); - const githubDescriptor = requireGithubDescriptor(); - - // The card's submit path: `POST /connections/github/complete` runs - // this exact sequence once the PAT probe passes. - await persistConnectorCredential({ - api: fakeApi, - cookies: [], - tenantId: TENANT.id, - descriptor: githubDescriptor, - secret: "ghp_test_token", - log: () => {}, - ensureProviderFn: async () => "prv_github", - ensureCredentialFn: store.ensureCredentialFn, - }); - - // The card's own read path, wired the way `apps/hub/src/index.ts` - // wires it: keyed by `githubDescriptor.displayName`, the same field - // `persistConnectorCredential` names the row with above. - const routes = createConnectGithubRoutes({ - requireGrant: allowAll, - log: () => {}, - resolveGithubConfig: buildResolveGithubConfig( - store, - githubDescriptor.displayName, - ), - resolveCodeReviewDefinitionId: async () => "wfd_code_review", - acquireRepoReviewLease: async () => true, - releaseRepoReviewLease: async () => {}, - hasRepoGrant: async () => false, - mintRepoGrant: async () => {}, - createWebhookTrigger: async () => ({ id: "trg_1" }), - hasWebhookTrigger: async () => false, - getTemplateSettings: async () => ({ - pendingConnections: [], - selectedRepos: [], - }), - persistSelectedRepos: async () => {}, - onReviewingStarted: async () => {}, - listReposFn: async () => [], - fetchAuthenticatedLoginFn: async () => "octocat", - }); - const app = mountAs(routes); - - const response = await app.request("/w_1/github/state"); - expect(response.status).toBe(200); - const body = (await response.json()) as { kind: string }; - expect(body.kind).toBe("connected"); - }); - - test("reading by any name other than the connector's own displayName silently leaves the card stuck disconnected -- this is the drift the two stores must never fall into", async () => { - const store = createFakeCredentialStore(); - const githubDescriptor = requireGithubDescriptor(); - - await persistConnectorCredential({ - api: fakeApi, - cookies: [], - tenantId: TENANT.id, - descriptor: githubDescriptor, - secret: "ghp_test_token", - log: () => {}, - ensureProviderFn: async () => "prv_github", - ensureCredentialFn: store.ensureCredentialFn, - }); - - // A second, independently-typed name -- e.g. the connector's `id` - // instead of its `displayName` -- is exactly the kind of hardcoded - // literal CL-6463 removed from `apps/hub/src/index.ts`. Reading by it - // instead of the descriptor's own field reproduces the stuck card. - const routes = createConnectGithubRoutes({ - requireGrant: allowAll, - log: () => {}, - resolveGithubConfig: buildResolveGithubConfig(store, githubDescriptor.id), - resolveCodeReviewDefinitionId: async () => "wfd_code_review", - acquireRepoReviewLease: async () => true, - releaseRepoReviewLease: async () => {}, - hasRepoGrant: async () => false, - mintRepoGrant: async () => {}, - createWebhookTrigger: async () => ({ id: "trg_1" }), - hasWebhookTrigger: async () => false, - getTemplateSettings: async () => ({ - pendingConnections: [], - selectedRepos: [], - }), - persistSelectedRepos: async () => {}, - onReviewingStarted: async () => {}, - listReposFn: async () => [], - fetchAuthenticatedLoginFn: async () => "octocat", - }); - const app = mountAs(routes); - - const response = await app.request("/w_1/github/state"); - const body = (await response.json()) as { kind: string }; - expect(body.kind).toBe("disconnected"); - }); -}); diff --git a/packages/workflow-catalog/src/connect-github-routes.test.ts b/packages/workflow-catalog/src/connect-github-routes.test.ts deleted file mode 100644 index fbe4debb0..000000000 --- a/packages/workflow-catalog/src/connect-github-routes.test.ts +++ /dev/null @@ -1,323 +0,0 @@ -// Exercises `createConnectGithubRoutes`' HTTP surface end to end through -// the real `startReviewingRepos` orchestration (`./connect-github-setup`), -// mounted the same way `packages/connections/src/routes.test.ts` mounts -// its own routes: a bare `Hono` with a tenant-injecting middleware, no -// real network, no real database — every port is a plain fake. -import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"; -import { Hono } from "hono"; -import type { MiddlewareHandler } from "hono"; -import type { RequireGrant, TenantEnv } from "@intx/hub-api"; -import * as errorSink from "@corbits/error-sink"; -import type { - GitHubClientConfig, - GitHubRepoSummary, -} from "@corbits/github-tools"; - -import { - createConnectGithubRoutes, - type ConnectGithubRoutesDeps, -} from "./connect-github-routes"; - -const TENANT = { - id: "tnt_1", - name: "Acme", - slug: "acme", - domain: "acme.example", - parentId: null, - config: null, - createdAt: new Date(), - updatedAt: new Date(), -}; - -const PRINCIPAL = { - id: "prn_alice", - tenantId: TENANT.id, - kind: "user" as const, - refId: "prn_alice", - status: "active" as const, - createdAt: new Date(), - updatedAt: new Date(), -}; - -const allowAll: RequireGrant = () => async (_c, next) => { - await next(); -}; - -const REPOS: readonly GitHubRepoSummary[] = [ - { id: "1", name: "acme/widgets" }, - { id: "2", name: "acme/gadgets" }, -]; - -function mountAs(routes: Hono): Hono { - const asTenant: MiddlewareHandler = async (c, next) => { - c.set("tenant", TENANT); - c.set("principal", PRINCIPAL); - await next(); - }; - const app = new Hono(); - app.use("*", asTenant); - app.route("/", routes); - return app; -} - -function buildApp(overrides: Partial = {}) { - const grants: { tenantId: string; repo: GitHubRepoSummary }[] = []; - const triggers: { tenantId: string; repo: GitHubRepoSummary }[] = []; - const introductionCalls: { - tenantId: string; - workbenchId: string; - principalId: string; - introductions: readonly { handle: string; text: string }[]; - }[] = []; - let settings: { - pendingConnections: readonly string[]; - selectedRepos: readonly string[]; - } = { pendingConnections: ["github"], selectedRepos: [] }; - const githubConfig: GitHubClientConfig | undefined = { apiKey: "ghp_test" }; - - const deps: ConnectGithubRoutesDeps = { - requireGrant: allowAll, - log: () => {}, - resolveGithubConfig: async () => githubConfig, - resolveCodeReviewDefinitionId: async () => "wfd_code_review", - acquireRepoReviewLease: async () => true, - releaseRepoReviewLease: async () => {}, - hasRepoGrant: async (tenantId, repo) => - grants.some((g) => g.tenantId === tenantId && g.repo.id === repo.id), - mintRepoGrant: async (tenantId, repo) => { - grants.push({ tenantId, repo }); - }, - createWebhookTrigger: async ( - tenantId, - _principalId, - _definitionId, - repo, - ) => { - triggers.push({ tenantId, repo }); - return { id: `trg_${repo.id}` }; - }, - hasWebhookTrigger: async (_tenantId, _definitionId, repo) => - triggers.some((t) => t.repo.id === repo.id), - getTemplateSettings: async () => settings, - persistSelectedRepos: async ( - _tenantId, - _workbenchId, - _principalId, - patch, - ) => { - settings = { - pendingConnections: patch["template/pendingConnections"], - selectedRepos: patch["template/selectedRepos"], - }; - }, - onReviewingStarted: async ( - tenantId, - workbenchId, - principalId, - introductions, - ) => { - introductionCalls.push({ - tenantId, - workbenchId, - principalId, - introductions, - }); - }, - listReposFn: async () => REPOS, - fetchAuthenticatedLoginFn: async () => "octocat", - ...overrides, - }; - - const routes = createConnectGithubRoutes(deps); - return { - app: mountAs(routes), - grants, - triggers, - introductionCalls, - settingsNow: () => settings, - }; -} - -afterEach(() => { - mock.restore(); -}); - -describe("GET /:workbenchId/github/state", () => { - test("reports disconnected with no github credential", async () => { - const { app } = buildApp({ resolveGithubConfig: async () => undefined }); - const response = await app.request("/wb_1/github/state"); - expect(response.status).toBe(200); - expect(await response.json()).toEqual({ kind: "disconnected" }); - }); - - test("reports connected repos and the room's selected ids once a credential exists", async () => { - const { app } = buildApp(); - const response = await app.request("/wb_1/github/state"); - expect(response.status).toBe(200); - expect(await response.json()).toEqual({ - kind: "connected", - orgName: "octocat", - repos: REPOS, - selectedRepoIds: [], - }); - }); - - test("surfaces a consumer-language error, never the raw GitHub failure, on a listRepos throw", async () => { - const { app } = buildApp({ - listReposFn: async () => { - throw new Error( - "GitHub request to /user/repos failed: 401 Bad credentials", - ); - }, - }); - const response = await app.request("/wb_1/github/state"); - expect(response.status).toBe(200); - expect(await response.json()).toEqual({ - kind: "error", - message: "Couldn't read your GitHub repositories. Try reconnecting.", - }); - }); -}); - -describe("POST /:workbenchId/github/start-reviewing", () => { - test("mints a grant and a live webhook trigger per repo, then persists the selection", async () => { - const harness = buildApp(); - const response = await harness.app.request("/wb_1/github/start-reviewing", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ repoIds: ["1", "2"] }), - }); - expect(response.status).toBe(200); - expect(await response.json()).toEqual({ startedTriggerCount: 2 }); - expect(harness.grants.map((g) => g.repo.name)).toEqual([ - "acme/widgets", - "acme/gadgets", - ]); - expect(harness.triggers.map((t) => t.repo.name)).toEqual([ - "acme/widgets", - "acme/gadgets", - ]); - expect(harness.settingsNow()).toEqual({ - pendingConnections: [], - selectedRepos: ["1", "2"], - }); - }); - - test("posts each reviewer's introduction once, naming the selected repos, after success", async () => { - const harness = buildApp(); - const response = await harness.app.request("/wb_1/github/start-reviewing", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ repoIds: ["1", "2"] }), - }); - expect(response.status).toBe(200); - expect(harness.introductionCalls.length).toBe(1); - const [call] = harness.introductionCalls; - if (call === undefined) throw new Error("expected one introduction call"); - expect(call.tenantId).toBe("tnt_1"); - expect(call.workbenchId).toBe("wb_1"); - expect(call.principalId).toBe("prn_alice"); - expect(call.introductions.length).toBe(3); - for (const introduction of call.introductions) { - expect( - ["acme/widgets", "acme/gadgets"].some((name) => - introduction.text.includes(name), - ), - ).toBe(true); - } - }); - - test("a rejecting introduction port still yields 200 and reports the failure", async () => { - const report = spyOn(errorSink, "reportError").mockReturnValue("ref_test"); - const harness = buildApp({ - onReviewingStarted: async () => { - throw new Error("boom"); - }, - }); - const response = await harness.app.request("/wb_1/github/start-reviewing", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ repoIds: ["1"] }), - }); - expect(response.status).toBe(200); - expect(report).toHaveBeenCalled(); - expect(report.mock.calls[0]?.[0]).toBeInstanceOf(Error); - expect(report.mock.calls[0]?.[1]).toMatchObject({ - operation: "connect-github.reviewerIntroductions", - tenantId: "tnt_1", - roomId: "wb_1", - }); - }); - - test("a second start-reviewing does not re-post introductions already landed for the room", async () => { - const harness = buildApp(); - const startReviewing = (repoIds: readonly string[]) => - harness.app.request("/wb_1/github/start-reviewing", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ repoIds }), - }); - const first = await startReviewing(["1"]); - expect(first.status).toBe(200); - expect(harness.introductionCalls).toHaveLength(1); - const second = await startReviewing(["1", "2"]); - expect(second.status).toBe(200); - expect(harness.introductionCalls).toHaveLength(1); - }); - - test("400s on a malformed body without leaking raw parser text", async () => { - const harness = buildApp(); - const response = await harness.app.request("/wb_1/github/start-reviewing", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ repoIds: "not-an-array" }), - }); - expect(response.status).toBe(400); - const body = (await response.json()) as { error: { code: string } }; - expect(body.error.code).toBe("bad_request"); - expect(harness.introductionCalls).toEqual([]); - }); - - test("409s when the tenant has no github credential yet", async () => { - const harness = buildApp({ resolveGithubConfig: async () => undefined }); - const response = await harness.app.request("/wb_1/github/start-reviewing", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ repoIds: ["1"] }), - }); - expect(response.status).toBe(409); - expect(harness.grants).toEqual([]); - expect(harness.introductionCalls).toEqual([]); - }); - - test("502s when GitHub cannot be read, without posting introductions", async () => { - const harness = buildApp({ - listReposFn: async () => { - throw new Error( - "GitHub request to /user/repos failed: 401 Bad credentials", - ); - }, - }); - const response = await harness.app.request("/wb_1/github/start-reviewing", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ repoIds: ["1"] }), - }); - expect(response.status).toBe(502); - expect(harness.introductionCalls).toEqual([]); - }); - - test("404s when the code-review workflow isn't deployed in this tenant", async () => { - const harness = buildApp({ - resolveCodeReviewDefinitionId: async () => undefined, - }); - const response = await harness.app.request("/wb_1/github/start-reviewing", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ repoIds: ["1"] }), - }); - expect(response.status).toBe(404); - expect(harness.grants).toEqual([]); - expect(harness.introductionCalls).toEqual([]); - }); -}); diff --git a/packages/workflow-catalog/src/connect-github-routes.ts b/packages/workflow-catalog/src/connect-github-routes.ts deleted file mode 100644 index 1880bf95d..000000000 --- a/packages/workflow-catalog/src/connect-github-routes.ts +++ /dev/null @@ -1,403 +0,0 @@ -// The room's own GitHub connect card, wired to real ports (CL-6344): the -// production half of `./connect-github-setup.ts`'s `startReviewingRepos` -// and the read side `@corbits/chat-ui`'s `ConnectGithubActions` polls. -// Mounted per-workbench, inside the platform's native tenant middleware -// (`TenantEnv`'s `tenant`/`principal` resolved before any handler here -// runs), the same way `@corbits/webhook-triggers`' management routes and -// `@workbench/connections`' routes are. -// -// Connecting the PAT itself is deliberately NOT a route here — -// `@corbits/chat-ui`'s `connect-github-actions.ts` header already says -// why: `@workbench/connections`' generic `/:connectorId/complete` (with -// `connectorId: "github"`, already registered in `CONNECTOR_REGISTRY`) is -// the credential test-and-store surface, and this module never -// reimplements it. What this module owns is the two things that need the -// decrypted secret and the live repo list: reading the connect card's -// state, and starting reviews once repos are picked. -// -// Every side effect a host needs — decrypting the tenant's GitHub -// credential, minting a grant, creating a live `webhook_trigger` row, -// resolving the deployed code-review workflow definition, and patching -// the room's settings — arrives as an injected port, exactly like -// `@workbench/connections`' `createConnectionRoutes`. `apps/hub` is the -// only caller that binds these against drizzle; this module never touches -// it, so it stays testable with plain fakes (`./connect-github-routes.test.ts`). -import { Hono } from "hono"; -import { type } from "arktype"; -import type { RequireGrant, TenantEnv } from "@intx/hub-api"; -import { cookiesFromHeader, makeErrorEnvelope } from "@workbench/hub-client"; -import { reportError } from "@corbits/error-sink"; - -import { - fetchAuthenticatedLogin, - listRepos, - type GitHubClientConfig, - type GitHubRepoSummary, -} from "@corbits/github-tools"; -import { - reviewerIntroductions, - type ReviewerIntroduction, -} from "@corbits/code-review/introductions"; - -import { startReviewingRepos } from "./connect-github-setup"; -import { templateReposSettingsPatch } from "./settings"; - -// `startReviewingRepos`' own selected-repo mismatch throw and a GitHub -// client failure both carry provider/internal detail (a rejected repo id, -// an HTTP status line) that a person pasting a token never needs to read — -// the CL-6360 idiom this route follows throughout via -// `@workbench/hub-client`'s `makeErrorEnvelope`: log the real cause behind -// a `refId`, answer with one honest, actionable `userMessage`. -const REPOS_UNREADABLE_MESSAGE = - "Couldn't read your GitHub repositories. Try reconnecting."; -const START_REVIEWING_FAILED_MESSAGE = - "Couldn't start reviewing those repositories. Try again in a moment."; - -const StartReviewingBody = type({ - repoIds: "string[]", -}); - -export type ConnectGithubTemplateSettings = { - readonly pendingConnections: readonly string[]; - readonly selectedRepos: readonly string[]; -}; - -export type ConnectGithubRoutesDeps = { - requireGrant: RequireGrant; - /** Where a failure's real cause goes — the CL-6360 idiom: the client - * only ever sees `{code, userMessage, refId}`; the raw detail (a - * GitHub HTTP status line, a `startReviewingRepos` mismatch) is - * logged here, keyed by the same `refId`, so an operator can look it - * up without the client having seen it. */ - log: (line: string) => void; - /** The tenant's decrypted GitHub PAT, ready to hand `@corbits/github-tools` - * — `undefined` when no `github` credential resolves for this tenant - * (nothing connected yet). A host binds this to `resolveCredentialByName` - * + `CredentialCipher.decrypt` against the `github` connector's own - * `CONNECTOR_REGISTRY` credential name, the same secret - * `buildCredentialDelivery` decrypts for a tool binding — never a second, - * bespoke credential store. */ - resolveGithubConfig( - tenantId: string, - ): Promise; - /** The tenant's deployed `code-review` workflow definition id — - * `undefined` when the template's own workflow was never deployed for - * this tenant (a create-flow bug, not something this route can fix). */ - resolveCodeReviewDefinitionId(tenantId: string): Promise; - /** Acquires the short-lived lease serializing one repo's setup - * (CL-7242) — see `./connect-github-setup.ts`'s - * `ConnectGithubSetupPorts.acquireRepoReviewLease` for why this is - * the actual concurrency backstop, not `hasRepoGrant`/`hasWebhookTrigger` - * below. A host binds this to `@corbits/webhook-triggers`' - * `RepoReviewLeaseStore.acquire`. */ - acquireRepoReviewLease( - tenantId: string, - repo: GitHubRepoSummary, - ): Promise; - /** Releases a lease `acquireRepoReviewLease` won — see - * `ConnectGithubSetupPorts.releaseRepoReviewLease`. A host binds - * this to `RepoReviewLeaseStore.release`. */ - releaseRepoReviewLease( - tenantId: string, - repo: GitHubRepoSummary, - ): Promise; - /** True once this repo already has the `repo:` grant — see - * `./connect-github-setup.ts`'s `ConnectGithubSetupPorts.hasRepoGrant` - * for why this makes a retry between minting the grant and creating - * the trigger safe. */ - hasRepoGrant( - tenantId: string, - repo: GitHubRepoSummary, - cookies: string[], - ): Promise; - /** Mints the `repo:`-scoped grant a launched review run - * needs to read this repo — see `./connect-github-setup.ts`'s - * `ConnectGithubSetupPorts.mintRepoGrant` for the exact resource shape. */ - mintRepoGrant( - tenantId: string, - repo: GitHubRepoSummary, - cookies: string[], - ): Promise; - /** Creates the live `webhook_trigger` row this repo's pull-request-opened - * events fire, scoped to the resolved code-review definition. A host - * binds this to `@corbits/webhook-triggers`' `WebhookTriggerStore.create`. */ - createWebhookTrigger( - tenantId: string, - principalId: string, - codeReviewDefinitionId: string, - repo: GitHubRepoSummary, - ): Promise<{ readonly id: string }>; - /** True once this repo already has a live webhook trigger for the - * resolved code-review definition — see - * `./connect-github-setup.ts`'s `ConnectGithubSetupPorts.hasWebhookTrigger` - * for why this makes a retry after a mid-loop failure safe. */ - hasWebhookTrigger( - tenantId: string, - codeReviewDefinitionId: string, - repo: GitHubRepoSummary, - ): Promise; - /** The room's current `template/*` settings — read before every - * `start-reviewing` write so the state read and the persisted patch - * never race a stale pending-connections list. */ - getTemplateSettings( - tenantId: string, - workbenchId: string, - ): Promise; - /** Applies the settings PATCH `./settings.ts`'s - * `templateReposSettingsPatch` builds and publishes the room's - * `chat.settings` stream event — the connect card refetches its own - * state as the direct consequence of the `start-reviewing` call that - * triggers this patch, never a fold off the stream event itself. */ - persistSelectedRepos( - tenantId: string, - workbenchId: string, - principalId: string, - patch: ReturnType, - ): Promise; - /** Posts each reviewer's canned introduction to the room once - * `startReviewingRepos` succeeds — the identity beat of the first - * minute a person sees after picking repos. Called once, after the - * repos are recorded and before the 200 response; a later - * `start-reviewing` (change repos) does not call this again. A - * rejection is reported through `reportError` and never fails the - * response — the webhook triggers already exist, so a person must - * not see an error for a missed introduction. */ - onReviewingStarted( - tenantId: string, - workbenchId: string, - principalId: string, - introductions: readonly ReviewerIntroduction[], - ): Promise; - /** Test-only override, defaulting to `@corbits/github-tools`' real - * `listRepos` — lets `connect-github-routes.test.ts` stub the GitHub - * client without reaching for module mocking or the real network. */ - listReposFn?: typeof listRepos; - /** Test-only override, matching `listReposFn`'s pattern. */ - fetchAuthenticatedLoginFn?: typeof fetchAuthenticatedLogin; -}; - -export function createConnectGithubRoutes( - deps: ConnectGithubRoutesDeps, -): Hono { - const app = new Hono(); - const runListRepos = deps.listReposFn ?? listRepos; - const runFetchAuthenticatedLogin = - deps.fetchAuthenticatedLoginFn ?? fetchAuthenticatedLogin; - - async function readGithubState( - tenantId: string, - config: GitHubClientConfig, - ): Promise< - | { - ok: true; - orgName: string; - repos: readonly GitHubRepoSummary[]; - } - | { ok: false } - > { - try { - const [orgName, repos] = await Promise.all([ - runFetchAuthenticatedLogin(config), - runListRepos(config), - ]); - return { ok: true, orgName, repos }; - } catch (cause) { - // The GitHub client's own errors (transport, HTTP status, shape - // mismatch) carry no actionable detail for the person who pasted a - // token — see this module's own `REPOS_UNREADABLE_MESSAGE` comment - // — so only the log line below carries the real cause. - const message = cause instanceof Error ? cause.message : String(cause); - deps.log( - `connect-github: reading GitHub state failed for tenant ${tenantId}: ${message}`, - ); - return { ok: false }; - } - } - - app.get( - "/:workbenchId/github/state", - deps.requireGrant("credential:*", "read"), - async (c) => { - const tenant = c.get("tenant"); - const workbenchId = c.req.param("workbenchId"); - const config = await deps.resolveGithubConfig(tenant.id); - if (config === undefined) { - return c.json({ kind: "disconnected" }, 200); - } - - const state = await readGithubState(tenant.id, config); - if (!state.ok) { - return c.json( - { kind: "error", message: REPOS_UNREADABLE_MESSAGE }, - 200, - ); - } - - const settings = await deps.getTemplateSettings(tenant.id, workbenchId); - return c.json( - { - kind: "connected", - orgName: state.orgName, - repos: state.repos, - selectedRepoIds: settings.selectedRepos, - }, - 200, - ); - }, - ); - - app.post( - "/:workbenchId/github/start-reviewing", - deps.requireGrant("credential:*", "read"), - async (c) => { - const body = StartReviewingBody( - await c.req.json().catch(() => undefined), - ); - if (body instanceof type.errors) { - return c.json( - makeErrorEnvelope({ - code: "bad_request", - userMessage: "Pick at least one repository.", - }), - 400, - ); - } - - const tenant = c.get("tenant"); - const principal = c.get("principal"); - const workbenchId = c.req.param("workbenchId"); - - const config = await deps.resolveGithubConfig(tenant.id); - if (config === undefined) { - return c.json( - makeErrorEnvelope({ - code: "not_connected", - userMessage: "Connect GitHub first.", - }), - 409, - ); - } - - const state = await readGithubState(tenant.id, config); - if (!state.ok) { - return c.json( - makeErrorEnvelope({ - code: "upstream_unavailable", - userMessage: REPOS_UNREADABLE_MESSAGE, - }), - 502, - ); - } - - const codeReviewDefinitionId = await deps.resolveCodeReviewDefinitionId( - tenant.id, - ); - if (codeReviewDefinitionId === undefined) { - return c.json( - makeErrorEnvelope({ - code: "not_found", - userMessage: - "This workbench's code-review workflow isn't set up yet.", - }), - 404, - ); - } - - try { - const settingsBefore = await deps.getTemplateSettings( - tenant.id, - workbenchId, - ); - const introductionsAlreadyPosted = - settingsBefore.selectedRepos.length > 0; - const result = await startReviewingRepos(body.repoIds, state.repos, { - acquireRepoReviewLease: (repo) => - deps.acquireRepoReviewLease(tenant.id, repo), - releaseRepoReviewLease: (repo) => - deps.releaseRepoReviewLease(tenant.id, repo), - hasRepoGrant: (repo) => - deps.hasRepoGrant( - tenant.id, - repo, - cookiesFromHeader(c.req.header("cookie")), - ), - mintRepoGrant: (repo) => - deps.mintRepoGrant( - tenant.id, - repo, - cookiesFromHeader(c.req.header("cookie")), - ), - createWebhookTrigger: (repo) => - deps.createWebhookTrigger( - tenant.id, - principal.id, - codeReviewDefinitionId, - repo, - ), - hasWebhookTrigger: (repo) => - deps.hasWebhookTrigger(tenant.id, codeReviewDefinitionId, repo), - persistSelectedRepos: async (repoIds) => { - const settings = await deps.getTemplateSettings( - tenant.id, - workbenchId, - ); - const pendingConnections = settings.pendingConnections.filter( - (id) => id !== "github", - ); - await deps.persistSelectedRepos( - tenant.id, - workbenchId, - principal.id, - templateReposSettingsPatch(pendingConnections, repoIds), - ); - }, - }); - - const reposById = new Map( - state.repos.map((repo) => [repo.id, repo] as const), - ); - const repoNames = body.repoIds - .map((repoId) => reposById.get(repoId)?.name) - .filter((name): name is string => name !== undefined); - if (!introductionsAlreadyPosted) { - try { - await deps.onReviewingStarted( - tenant.id, - workbenchId, - principal.id, - reviewerIntroductions(repoNames), - ); - } catch (cause) { - reportError(cause, { - operation: "connect-github.reviewerIntroductions", - tenantId: tenant.id, - roomId: workbenchId, - }); - } - } - - return c.json( - { startedTriggerCount: result.createdTriggerIds.length }, - 200, - ); - } catch (cause) { - // A repoId `state.repos` doesn't carry means the card's own - // selection state is stale against a live re-list — never - // something a raw stack trace helps the person fix. - const message = cause instanceof Error ? cause.message : String(cause); - deps.log( - `connect-github: start-reviewing failed for tenant ${tenant.id}, workbench ${workbenchId}: ${message}`, - ); - return c.json( - makeErrorEnvelope({ - code: "start_reviewing_failed", - userMessage: START_REVIEWING_FAILED_MESSAGE, - }), - 400, - ); - } - }, - ); - - return app; -} diff --git a/packages/workflow-catalog/src/connect-github-setup.test.ts b/packages/workflow-catalog/src/connect-github-setup.test.ts deleted file mode 100644 index bc8ebae6d..000000000 --- a/packages/workflow-catalog/src/connect-github-setup.test.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { - startReviewingRepos, - webhookTriggerName, - type ConnectGithubSetupPorts, -} from "./connect-github-setup"; -import type { GitHubRepoSummary } from "@corbits/github-tools"; - -const REPOS: readonly GitHubRepoSummary[] = [ - { id: "1", name: "acme/widgets" }, - { id: "2", name: "acme/gadgets" }, - { id: "3", name: "acme/sprockets" }, -]; - -function fakePorts() { - const grantedRepos: string[] = []; - const createdTriggerRepos: string[] = []; - let persistedRepoIds: readonly string[] | undefined; - const ports: ConnectGithubSetupPorts = { - async acquireRepoReviewLease() { - return true; - }, - async releaseRepoReviewLease() {}, - async hasRepoGrant() { - return false; - }, - async mintRepoGrant(repo) { - grantedRepos.push(repo.name); - }, - async createWebhookTrigger(repo) { - createdTriggerRepos.push(repo.name); - return { id: `trg_${repo.id}` }; - }, - async hasWebhookTrigger() { - return false; - }, - async persistSelectedRepos(repoIds) { - persistedRepoIds = repoIds; - }, - }; - return { - ports, - grantedRepos, - createdTriggerRepos, - persistedRepoIds: () => persistedRepoIds, - }; -} - -/** - * A grant-store- and `WebhookTriggerStore`-backed fake: `hasRepoGrant` - * and `hasWebhookTrigger` each reflect what `mintRepoGrant` and - * `createWebhookTrigger` have actually persisted so far — independently - * of each other, the same as the real ports bind against the real - * `grant` table and `WebhookTriggerStore.list`. Used to prove a retry - * after a mid-loop failure is idempotent regardless of exactly where in - * a repo's two steps the failure landed. - */ -function fakeBackedPorts() { - const grantedRepoNames = new Set(); - const existingTriggerRepoNames = new Set(); - const leasedRepoNames = new Set(); - const grantedRepos: string[] = []; - const createdTriggerRepos: string[] = []; - const ports: ConnectGithubSetupPorts = { - // Synchronous check-and-set, same as the real lease's DB-side - // compare-and-swap: two calls sharing this same `ports` object - // (e.g. via `Promise.all`) race here exactly like two real - // concurrent HTTP requests race the real `RepoReviewLeaseStore`. - async acquireRepoReviewLease(repo) { - if (leasedRepoNames.has(repo.name)) return false; - leasedRepoNames.add(repo.name); - return true; - }, - async releaseRepoReviewLease(repo) { - leasedRepoNames.delete(repo.name); - }, - async hasRepoGrant(repo) { - return grantedRepoNames.has(repo.name); - }, - async mintRepoGrant(repo) { - grantedRepos.push(repo.name); - grantedRepoNames.add(repo.name); - }, - async createWebhookTrigger(repo) { - createdTriggerRepos.push(repo.name); - existingTriggerRepoNames.add(repo.name); - return { id: `trg_${repo.id}` }; - }, - async hasWebhookTrigger(repo) { - return existingTriggerRepoNames.has(repo.name); - }, - async persistSelectedRepos() {}, - }; - return { ports, grantedRepos, createdTriggerRepos }; -} - -describe("startReviewingRepos", () => { - test("mints a grant and a webhook trigger per selected repo, then persists the selection", async () => { - const fake = fakePorts(); - const result = await startReviewingRepos(["1", "3"], REPOS, fake.ports); - - expect(fake.grantedRepos).toEqual(["acme/widgets", "acme/sprockets"]); - expect(fake.createdTriggerRepos).toEqual([ - "acme/widgets", - "acme/sprockets", - ]); - expect(result.createdTriggerIds).toEqual(["trg_1", "trg_3"]); - expect(fake.persistedRepoIds()).toEqual(["1", "3"]); - }); - - test("never mints anything for a repo that isn't selected", async () => { - const fake = fakePorts(); - await startReviewingRepos(["2"], REPOS, fake.ports); - expect(fake.grantedRepos).toEqual(["acme/gadgets"]); - }); - - test("throws rather than silently dropping a repoId the listed repos don't carry", async () => { - const fake = fakePorts(); - await expect( - startReviewingRepos(["not-a-real-repo"], REPOS, fake.ports), - ).rejects.toThrow(/not in the listed repos/); - expect(fake.grantedRepos).toEqual([]); - }); - - test("retrying after a mid-loop failure never mints a duplicate grant or trigger", async () => { - const fake = fakeBackedPorts(); - const failingPorts: ConnectGithubSetupPorts = { - ...fake.ports, - async mintRepoGrant(repo) { - if (repo.name === "acme/gadgets") { - throw new Error("mint failed"); - } - await fake.ports.mintRepoGrant(repo); - }, - }; - - await expect( - startReviewingRepos(["1", "2", "3"], REPOS, failingPorts), - ).rejects.toThrow(/mint failed/); - - // The first repo made it through before the failure; the other two - // never got a grant or a trigger. - expect(fake.grantedRepos).toEqual(["acme/widgets"]); - expect(fake.createdTriggerRepos).toEqual(["acme/widgets"]); - - // Retrying the same selection (as the route's "Try again" does) - // skips the repo that's already set up and only mints for the rest. - const result = await startReviewingRepos( - ["1", "2", "3"], - REPOS, - fake.ports, - ); - - expect(fake.grantedRepos).toEqual([ - "acme/widgets", - "acme/gadgets", - "acme/sprockets", - ]); - expect(fake.createdTriggerRepos).toEqual([ - "acme/widgets", - "acme/gadgets", - "acme/sprockets", - ]); - expect(result.createdTriggerIds).toEqual(["trg_2", "trg_3"]); - }); - - test("retrying after a failure between minting the grant and creating the trigger never re-mints the grant", async () => { - const fake = fakeBackedPorts(); - const failingPorts: ConnectGithubSetupPorts = { - ...fake.ports, - async createWebhookTrigger(repo) { - if (repo.name === "acme/gadgets") { - throw new Error("trigger create failed"); - } - return fake.ports.createWebhookTrigger(repo); - }, - }; - - await expect( - startReviewingRepos(["1", "2", "3"], REPOS, failingPorts), - ).rejects.toThrow(/trigger create failed/); - - // The failing repo's grant was minted before the trigger create blew - // up; the repo after it was never reached at all. - expect(fake.grantedRepos).toEqual(["acme/widgets", "acme/gadgets"]); - expect(fake.createdTriggerRepos).toEqual(["acme/widgets"]); - - const result = await startReviewingRepos( - ["1", "2", "3"], - REPOS, - fake.ports, - ); - - // acme/gadgets' grant is not re-minted on retry — only its missing - // trigger is created. - expect(fake.grantedRepos).toEqual([ - "acme/widgets", - "acme/gadgets", - "acme/sprockets", - ]); - expect(fake.createdTriggerRepos).toEqual([ - "acme/widgets", - "acme/gadgets", - "acme/sprockets", - ]); - expect(result.createdTriggerIds).toEqual(["trg_2", "trg_3"]); - }); - - test("two concurrent calls for the same repo mint exactly one grant and one trigger (CL-7242)", async () => { - // Reconstructs the audit's own reproduction: `Promise.all` of two - // concurrent `startReviewingRepos` calls sharing the same - // backing state, for the same repo. Before the lease existed, - // both calls' `hasRepoGrant`/`hasWebhookTrigger` reads landed - // before either write, so both minted -- 2 grants, 2 triggers, - // for one repo. `acquireRepoReviewLease`'s synchronous - // check-and-set means only one of the two calls below ever gets - // past the lease for "acme/widgets": the other's per-repo body - // never runs at all. - const fake = fakeBackedPorts(); - - const [first, second] = await Promise.all([ - startReviewingRepos(["1"], REPOS, fake.ports), - startReviewingRepos(["1"], REPOS, fake.ports), - ]); - - expect(fake.grantedRepos).toEqual(["acme/widgets"]); - expect(fake.createdTriggerRepos).toEqual(["acme/widgets"]); - - const totalCreated = - first.createdTriggerIds.length + second.createdTriggerIds.length; - expect(totalCreated).toBe(1); - - const totalSkipped = - first.skippedRepoIds.length + second.skippedRepoIds.length; - expect(totalSkipped).toBe(1); - }); -}); - -describe("webhookTriggerName", () => { - test("names a repo's trigger consistently, the one convention both the create and the lookup bind against", () => { - const repo: GitHubRepoSummary = { - id: "1", - name: "acme/widgets", - }; - expect(webhookTriggerName(repo)).toBe("acme/widgets pull-request-opened"); - }); -}); diff --git a/packages/workflow-catalog/src/connect-github-setup.ts b/packages/workflow-catalog/src/connect-github-setup.ts deleted file mode 100644 index c65fca755..000000000 --- a/packages/workflow-catalog/src/connect-github-setup.ts +++ /dev/null @@ -1,190 +0,0 @@ -// What runs once a person has actually picked repos on a room's GitHub -// connect card and starts reviewing. `instantiateWorkbenchTemplate` -// resolves a template at create time, before any repo is known; this -// module resolves the repo-scoped half once the person answers the -// connect card and creates the live webhook trigger per repo, mirroring -// `instantiate.ts`'s own shape exactly: pure orchestration over injected -// async ports, no HTTP, no store, testable with plain fakes. -import type { GitHubRepoSummary } from "@corbits/github-tools"; - -export interface ConnectGithubSetupPorts { - /** - * Acquires the short-lived lease serializing this repo's setup - * (CL-7242): true means this call now owns it and must run the rest - * of this repo's body below; false means another call currently - * owns it (or very recently did) and this repo must be skipped - * entirely, including `hasRepoGrant`/`hasWebhookTrigger`. This is - * the actual concurrency backstop — `hasRepoGrant` and - * `hasWebhookTrigger` below are a fast path for a *sequential* - * retry (CL-7134), not a lock, and are only safe to reach because - * the lease already ensures two concurrent calls for the same repo - * can never both get here. A host binds this to - * `@corbits/webhook-triggers`' `RepoReviewLeaseStore.acquire`. - */ - acquireRepoReviewLease(repo: GitHubRepoSummary): Promise; - /** - * Releases the lease `acquireRepoReviewLease` won, once this repo's - * body finishes (success or failure) — so a legitimate retry never - * waits out the lease's own staleness window. A host binds this to - * `RepoReviewLeaseStore.release`. - */ - releaseRepoReviewLease(repo: GitHubRepoSummary): Promise; - /** - * True once this repo already has the `repo:` grant — - * checked before minting one, so a retry after a failure between - * minting the grant and creating the trigger skips straight past - * minting for a repo that already has one (CL-7134's fast path). The - * `grant` table (`vendor/intx/db`) carries no unique constraint over - * tenant/resource/action, so this read is the only thing standing - * between a retry and a duplicate row absent the lease below. A host - * binds this to GET `/api/tenants/:id/grants?resource=repo:`. - * - * Safe to be a plain read, not a conflict-checked one: the lease - * above already guarantees only one caller ever reaches this point - * for a given repo at a time. - */ - hasRepoGrant(repo: GitHubRepoSummary): Promise; - /** - * Mints one grant scoped to `repo:` (the `owner/name` full - * name — the same `":"` resource-string shape - * `idResource("room", "id")` builds in `@intx/hub-api`'s grant - * middleware, applied here to a repo instead of a room). A host binds - * this to POST `/api/tenants/:id/grants`; this module never touches - * drizzle directly. - * - * A plain insert is safe here (CL-7242): the lease above is what - * makes this call-site single-flight per repo, so this never needs - * its own conflict handling against the platform's `grant` table. - */ - mintRepoGrant(repo: GitHubRepoSummary): Promise; - /** - * Creates the live `webhook_trigger` row this repo's pull-request-opened - * events fire — the onboarding card's start-reviewing step is what - * creates this trigger, for each repo the person picked. A host - * binds this to `@corbits/webhook-triggers`' `WebhookTriggerStore.ensure` - * rather than `create`: the lease already makes this call-site - * single-flight per repo, so `ensure`'s own idempotence - * (backed by `webhook_trigger_tenant_definition_name_unique`, our - * own schema, unaffected by CL-7242's vendored-table constraint) is - * pure defense-in-depth — a lease bug degrades to a silent no-op - * here instead of a hard failure or a real duplicate trigger. - */ - createWebhookTrigger( - repo: GitHubRepoSummary, - ): Promise<{ readonly id: string }>; - /** - * True once this repo already has a live webhook trigger — checked - * before creating one, so a retry after a mid-loop failure (a repo - * 1..N-1 already set up, N onward not) skips straight past creating - * one for a repo a prior attempt already finished (CL-7134's fast - * path). A host binds this to a read against - * `@corbits/webhook-triggers`' `WebhookTriggerStore.list`. - * - * This is never cleared on GitHub disconnect: nothing here disables or - * deletes a trigger, so re-adding a repo after a reconnect finds its - * old trigger still live and skips it rather than minting a new one — - * intentional, not a gap this module owns closing. - */ - hasWebhookTrigger(repo: GitHubRepoSummary): Promise; - /** - * Records which repos this room is reviewing — the `template/*` - * settings namespace's `selectedRepos` key (`./settings.ts`'s - * `templateReposSettingsPatch`). A host binds this to the room's - * existing settings PATCH route; the connect-github card refetches - * its own state as the direct consequence of the call that triggers - * this patch, never a fold off the resulting stream event. - */ - persistSelectedRepos(repoIds: readonly string[]): Promise; -} - -export interface StartReviewingReposResult { - readonly createdTriggerIds: readonly string[]; - /** - * Repos this call didn't touch because another call currently owns - * (or very recently owned) their setup lease — not an error, but - * worth surfacing rather than silently dropping: if the lease - * holder crashed, this is the caller's only signal that a repo in - * the selection got no setup work done at all this round. - */ - readonly skippedRepoIds: readonly string[]; -} - -/** - * Mints one grant and one webhook trigger per selected repo that doesn't - * already have one, then records the selection. `repoIds` must all - * resolve against `repos` — a caller passing an id `repos` doesn't carry - * is a bug in how the connect card's own selection state was built, not - * something to silently drop. - * - * Idempotent by construction: the grant and the trigger are each gated - * on their own existence check, independently, rather than one check - * guarding both — a retry after a failure between minting the grant and - * creating the trigger must still create the trigger without re-minting - * the grant, and a retry after a failure before the grant was minted - * must still mint it. A repo both checks already report true for is - * skipped entirely. (CL-7134.) - * - * `hasRepoGrant`/`hasWebhookTrigger` are a fast path for a *sequential* - * retry, not a lock: two truly concurrent calls for the same repo (a - * double-click, or a client retrying an in-flight request rather than - * a failed one) can both read "not set up yet" before either write - * lands. `acquireRepoReviewLease` is the actual concurrency backstop - * (CL-7242): only the caller that wins the lease enters the rest of a - * repo's body at all, so the checks below never race against a - * concurrent duplicate for the same repo — the lease is released - * (`releaseRepoReviewLease`) as soon as that body finishes, success or - * failure, so a legitimate retry is never blocked by its own prior - * attempt. - */ -export async function startReviewingRepos( - repoIds: readonly string[], - repos: readonly GitHubRepoSummary[], - ports: ConnectGithubSetupPorts, -): Promise { - const reposById = new Map(repos.map((repo) => [repo.id, repo])); - const selected = repoIds.map((repoId) => { - const repo = reposById.get(repoId); - if (repo === undefined) { - throw new Error( - `startReviewingRepos was asked to review repo "${repoId}", which is not in the listed repos`, - ); - } - return repo; - }); - - const createdTriggerIds: string[] = []; - const skippedRepoIds: string[] = []; - for (const repo of selected) { - if (!(await ports.acquireRepoReviewLease(repo))) { - skippedRepoIds.push(repo.id); - continue; - } - try { - if (!(await ports.hasRepoGrant(repo))) { - await ports.mintRepoGrant(repo); - } - if (await ports.hasWebhookTrigger(repo)) { - continue; - } - const trigger = await ports.createWebhookTrigger(repo); - createdTriggerIds.push(trigger.id); - } finally { - await ports.releaseRepoReviewLease(repo); - } - } - - await ports.persistSelectedRepos(repoIds); - - return { createdTriggerIds, skippedRepoIds }; -} - -/** - * The one place the `webhook_trigger.name` convention for a repo's - * pull-request-opened trigger is spelled out — both - * `createWebhookTrigger`'s insert and `hasWebhookTrigger`'s lookup bind - * against this, in `apps/hub`, so the two can never drift into matching - * different strings. - */ -export function webhookTriggerName(repo: GitHubRepoSummary): string { - return `${repo.name} pull-request-opened`; -} diff --git a/packages/workflow-catalog/src/index.ts b/packages/workflow-catalog/src/index.ts deleted file mode 100644 index 9b1d43e97..000000000 --- a/packages/workflow-catalog/src/index.ts +++ /dev/null @@ -1,560 +0,0 @@ -// Deploy-layer metadata for seeded workflow packages. Interchange's -// defineWorkflow has no automatable / display-name fields; each -// workflows/*/package.json carries a `corbits.workflow` block, and this -// module is the TypeScript mirror both seed and the web picker import. -// Keep the two in lockstep — the package.json block is the npm-visible -// source of truth for package authors; this list is what runtime code -// reads. - -import { type } from "arktype"; - -export { - CODE_REVIEW_TEMPLATE, - DUE_DILIGENCE_TEMPLATE, - GTM_TEMPLATE, - WORKBENCH_TEMPLATES, - WorkbenchDefinitionAgent, - WorkbenchDefinitionSchema, - WorkbenchOnboardingStep, - WorkbenchTemplateBlock, - WorkbenchTemplateOpenInput, - WorkbenchTemplateRoutine, - WorkbenchTemplateWebhookTrigger, - parseWorkbenchDefinition, - serializeWorkbenchDefinition, - templateBlockAssetNames, - workbenchTemplate, - workbenchTemplateLibraryEntries, -} from "./templates"; -export type { WorkbenchDefinition } from "./templates"; -export { - instantiateWorkbenchTemplate, - type ParticipantAgentRequest, - type WorkbenchTemplateInstantiationPorts, - type WorkbenchTemplateInstantiationResult, -} from "./instantiate"; -export { - jimmyAgentRequest, - scoutAgentRequest, -} from "./participant-agent-requests"; -export { - TemplateReposSettingsPatch, - TemplateSettingsPatch, - templateReposSettingsPatch, - templateSettingsPatch, -} from "./settings"; -export { - startReviewingRepos, - webhookTriggerName, - type ConnectGithubSetupPorts, - type StartReviewingReposResult, -} from "./connect-github-setup"; -// CL-7373: the workflow definition lifecycle/detail types moved to -// @corbits/workflows (`.` for the detail route, `./client` for the -// browser-safe wire schema and pure lifecycle derivation) alongside the -// deploy-source record they read. - -/** - * One named field a mail trigger reads by name — the create-time UI's only - * source of truth for what a workflow's trigger actually expects (see each - * workflow's own system prompt / intake tool for the underlying contract - * this mirrors, e.g. `workflows/last-30-days-research/src/index.ts`'s - * "the trigger carries a `topic` and an optional `focus`", or - * `workflows/pain-point-collateral/src/intake-tool.ts`'s `IntakeArgs`). - * `key` is the exact field name a trigger payload carries — never - * relabeled or humanized before it reaches the workflow. - */ -export const WorkflowTriggerField = type({ - key: "/^[a-zA-Z][a-zA-Z0-9]*$/", - // Explicit, never defaulted: `"text"` renders a plain input and - // accepts any non-empty string; `"agent"` renders a picker of - // taskable agent definitions (the routines create stepper reuses the - // task composer's own listing) and its value must resolve to a real - // taskable definition at create time — see - // `packages/routines/src/routes.ts`'s trigger-input validation. - kind: "'text' | 'agent'", - label: "string > 0", - "placeholder?": "string", - required: "boolean", - "default?": "string", - "help?": "string", -}); -export type WorkflowTriggerField = typeof WorkflowTriggerField.infer; - -export type WorkflowCatalogEntry = { - readonly assetName: string; - readonly displayName: string; - /** Schedulable as a Routine. False for conversational agents / chat hosts. */ - readonly automatable: boolean; - /** - * A real chat partner a person can open a DM with and converse - * freely — as opposed to a mail-triggered utility whose only sane - * input is its declared trigger contract (a topic, a transcript, a - * scheduler-computed digest line, …). `automatable` alone can't tell - * these apart: several utilities (`echo`, `last-30-days-research`, - * `pain-point-collateral`, …) are non-automatable on-demand runs, not - * conversational agents. Only the seeded `assistant`/Myra definition - * is `true` today; every other catalog entry — the whole reason this - * catalog exists — is a workflow utility, never `true`. - */ - readonly conversational: boolean; - /** - * Where a run's result actually lands — the honest end-to-end - * contract a routine's "Deliver results to" step depends on. Every - * entry states this plainly, whether or not it is automatable: - * `"workbench"` posts into the picked delivery workbench's thread — - * every catalog entry today; `"inbox"` never posts to a workbench at - * all — its result reaches only the creator's Inbox, so a create/run - * flow for it must never collect or require a deliveryWorkbenchId that - * would otherwise be silently discarded. - */ - readonly deliveryMode: "workbench" | "inbox"; - /** One honest sentence: what this workflow actually does. No metrics, no hype. */ - readonly whatItDoes: string; - /** - * Connector ids this workflow's tool packages call — either a native - * connector (`@workbench/connections/registry`'s `CONNECTOR_REGISTRY`) - * or an MCP preset slug a person connects under Plugins - * (`@workbench/connections/mcp-presets`' `MCP_PRESETS`), which is the - * only way some integrations (Attio) are reachable here at all. Empty - * for workflows with no external connector dependency. - */ - readonly requiredConnections: readonly string[]; - /** A short, honest one-line readout of what a run actually produces — - * capitalized, no trailing period, same shape across every entry. */ - readonly exampleOutput: string; - /** A short, honestly-hedged hint — never fabricated precision. */ - readonly typicalDuration: string; - /** - * Named trigger inputs a person can fill in at create time (the routine - * create stepper) or on a manual run — omitted entirely for workflows - * whose trigger carries no human-supplied content (see each entry's own - * comment for why). Order is display order. - */ - readonly triggerFields?: readonly WorkflowTriggerField[]; -}; - -/** - * Every known workbench workflow package, keyed by the asset name seed - * deploys under. Agent definitions created at runtime are never listed - * here, so they cannot pass the automatable filter by accident. - */ -export const WORKFLOW_CATALOG: readonly WorkflowCatalogEntry[] = [ - { - assetName: "echo", - displayName: "Echo", - automatable: false, - conversational: false, - deliveryMode: "workbench", - whatItDoes: - "Replies with the exact text it received — a wiring check for the mail-triggered contract, not a real assistant.", - requiredConnections: [], - exampleOutput: "Echoed back: Testing 1 2 3", - typicalDuration: "a few seconds", - }, - { - assetName: "code-review", - displayName: "Code review", - automatable: false, - conversational: false, - deliveryMode: "workbench", - whatItDoes: - "Reads a pull request's diff, reviews it for correctness, architecture, and release risk, and posts one review back on the pull request.", - requiredConnections: ["github"], - exampleOutput: "One review posted: 1 blocking, 2 worth fixing, 1 for later", - typicalDuration: "a minute or two", - triggerFields: [ - { - key: "pullRequestUrl", - kind: "text", - label: "Pull request URL", - placeholder: "https://github.com/owner/repo/pull/123", - required: true, - help: "The pull request to review. A GitHub webhook fills this in on its own for every new pull request.", - }, - ], - }, - { - assetName: "assistant", - displayName: "Myra", - automatable: false, - conversational: true, - deliveryMode: "workbench", - whatItDoes: - "A general-purpose assistant for the workspace — answers questions, drafts text, and reasons through problems in conversation.", - requiredConnections: [], - exampleOutput: "Drafted a short, polite decline you can send as-is", - typicalDuration: "varies with the conversation", - }, - { - assetName: "heartbeat", - displayName: "Heartbeat", - automatable: true, - conversational: false, - deliveryMode: "workbench", - whatItDoes: - "Completes immediately on every trigger with no real reply — a lightweight target for testing scheduling and mail triggers.", - requiredConnections: [], - exampleOutput: "Completed at the trigger time, no reply content", - typicalDuration: "a few seconds", - }, - { - assetName: "workbench-digest", - displayName: "Workbench digest", - automatable: true, - conversational: false, - deliveryMode: "workbench", - whatItDoes: - "Relays a scheduler-computed digest line straight into a workbench, unchanged — the digest content itself comes entirely from its trigger.", - requiredConnections: [], - exampleOutput: "Digest of 12 messages since yesterday · last post 9:41am", - typicalDuration: "a few seconds", - }, - { - assetName: "granola-call", - displayName: "Granola call notes", - automatable: true, - conversational: false, - deliveryMode: "workbench", - whatItDoes: - "Polls Granola for recent calls and starts one process-granola-call run per call that doesn't yet have published notes.", - requiredConnections: ["granola"], - exampleOutput: "Checked 10 calls, started 2 new process-granola-call runs", - typicalDuration: "a few seconds", - }, - { - assetName: "process-granola-call", - displayName: "Process Granola call", - automatable: false, - conversational: false, - deliveryMode: "workbench", - whatItDoes: - "Fetches one call's transcript and publishes five-section working notes — Participants, Summary, Pain points, Decisions, Action items — grounded in the transcript.", - requiredConnections: ["granola"], - exampleOutput: - "Participants, summary, and action items from the call transcript", - typicalDuration: "1-2 minutes", - }, - { - assetName: "morning-brief", - displayName: "Morning brief", - automatable: true, - conversational: false, - deliveryMode: "workbench", - whatItDoes: - "Pulls the sender's recent Granola calls and Linear issues and writes a three-section daily brief: what happened, what needs attention, and suggested next actions.", - requiredConnections: ["granola", "linear"], - exampleOutput: - "Brief covering 2 calls, 3 issue updates, one blocked review", - typicalDuration: "under a minute", - }, - { - assetName: "pain-point-collateral", - displayName: "Pain-point collateral", - automatable: false, - conversational: false, - deliveryMode: "workbench", - whatItDoes: - "Extracts a customer's real pain points from a call transcript and drafts one piece of targeted collateral, held for approval before it's finalized.", - requiredConnections: ["granola"], - exampleOutput: "Drafted a one-pager on the onboarding-speed pain point", - typicalDuration: "a few minutes, plus the time to approve", - // Neither field is required on its own: the workflow's intake tool - // (workflows/pain-point-collateral/src/intake-tool.ts) accepts a - // pasted transcript OR a Granola note id — transcript wins if both are - // given, and neither given is a valid "teach me what to send" path, - // not an error. Both are worth collecting at create/run time even - // though neither is required: a manual run with no input at all would - // just re-teach the same instructions the workflow already gives. - triggerFields: [ - { - key: "transcript", - kind: "text", - label: "Transcript", - placeholder: "Paste the call transcript", - required: false, - help: "Or leave blank and give a Granola note id below.", - }, - { - key: "noteId", - kind: "text", - label: "Granola note ID", - placeholder: "note_abc123", - required: false, - help: "Used only if no transcript is pasted above.", - }, - ], - }, - { - assetName: "collateral-generation", - displayName: "Collateral generation", - automatable: false, - conversational: false, - deliveryMode: "workbench", - whatItDoes: - "Drafts marketing collateral across picked content types from Granola notes, Linear issues, or pasted text, with a swipe review on every draft and one approval on the final set.", - requiredConnections: ["granola", "linear"], - exampleOutput: - "Drafts ready to review: LinkedIn post, blog, Twitter thread", - typicalDuration: "several minutes, plus review and approval time", - }, - { - assetName: "reddit-opportunity-scanner", - displayName: "Reddit opportunity scanner", - automatable: false, - conversational: false, - deliveryMode: "workbench", - whatItDoes: - "Scores Reddit posts as outreach opportunities for a target website, after a review of the search plan and one approval on the final list.", - requiredConnections: ["scrapecreators"], - exampleOutput: "Ranked 6 opportunities, 2 scored 5/5 on buying signals", - typicalDuration: "a few minutes, plus review and approval time", - }, - { - assetName: "last-30-days-research", - displayName: "Last 30 days research report", - automatable: false, - conversational: false, - deliveryMode: "workbench", - whatItDoes: - "Researches a topic over the last 30 days across web search and GitHub, and writes a cited report with sourced findings.", - requiredConnections: ["exa"], - exampleOutput: "Cited report: 3 new competing launches this month", - typicalDuration: "1-2 minutes", - // Mirrors workflows/last-30-days-research/src/index.ts's system prompt - // exactly: "the trigger carries a `topic` and an optional `focus`" — - // topic is required (the prompt refuses to invent one), focus narrows - // which angle to chase and is skippable. - triggerFields: [ - { - key: "topic", - kind: "text", - label: "Topic", - placeholder: "AI coding agents", - required: true, - help: "What to research over the last 30 days.", - }, - { - key: "focus", - kind: "text", - label: "Focus", - placeholder: "Competing launches", - required: false, - help: "Optional — narrows which angle of the topic to chase.", - }, - ], - }, - { - assetName: "diligence-brief", - displayName: "Diligence brief", - automatable: false, - conversational: false, - deliveryMode: "workbench", - whatItDoes: - "Researches a company across web search and firm memory, and writes a cited diligence brief across five fixed sections, held for approval before it's saved.", - requiredConnections: ["exa"], - exampleOutput: - "Diligence brief: 5 sections, 2 flagged as insufficient evidence", - typicalDuration: "1-2 minutes, plus review and approval time", - // Mirrors workflows/diligence-brief/src/index.ts's system prompt - // exactly: "the trigger carries a `company` and an optional `focus`" — - // company is required (the prompt refuses to draft with no subject), - // focus narrows which angle to dig into and is skippable. - triggerFields: [ - { - key: "company", - kind: "text", - label: "Company", - placeholder: "Acme Corp", - required: true, - help: "The company this brief is about.", - }, - { - key: "focus", - kind: "text", - label: "Focus", - placeholder: "Founder track record", - required: false, - help: "Optional — narrows which angle to dig into.", - }, - ], - }, - { - assetName: "exa-topic-watch", - displayName: "Web topic watch", - automatable: true, - conversational: false, - deliveryMode: "workbench", - whatItDoes: - "Searches the live web for one topic each run and publishes a short digest of what moved, held for approval before it's saved.", - requiredConnections: ["exa"], - exampleOutput: "Weekly digest: 4 takeaways, 2 worth a closer look", - typicalDuration: "a minute or two, plus the time to approve", - // Mirrors workflows/exa-topic-watch/src/index.ts's system prompt: the - // trigger carries a `topic` and nothing else, and the prompt refuses - // to invent or widen one, so it is the single required field. - triggerFields: [ - { - key: "topic", - kind: "text", - label: "Topic", - placeholder: "AI coding agents for go-to-market", - required: true, - help: "What to watch on the web each run.", - }, - ], - }, - { - assetName: "attio-task-agent", - displayName: "Attio task agent", - automatable: false, - conversational: false, - deliveryMode: "workbench", - whatItDoes: - "Works one Attio task: reads the record and the surrounding context, drafts what the task needs, and writes back to Attio only once you approve it.", - requiredConnections: ["attio"], - exampleOutput: - "Two drafts ready to review, plus a CRM note awaiting your OK", - typicalDuration: "a few minutes, plus review and approval time", - // Mirrors workflows/attio-task-agent/src/index.ts's system prompt: - // the trigger names the task by id, or names whose task list to look - // in. Neither is required on its own — with neither, the run lists - // the open tasks it can see and asks which one, which is a valid - // start, not an error. - triggerFields: [ - { - key: "taskId", - kind: "text", - label: "Attio task ID", - placeholder: "The task to work", - required: false, - help: "Or leave blank and name whose tasks to look at below.", - }, - { - key: "assignee", - kind: "text", - label: "Assignee", - placeholder: "Whose task list to look in", - required: false, - help: "Used only if no task ID is given above.", - }, - ], - }, -]; - -for (const entry of WORKFLOW_CATALOG) { - if (entry.triggerFields === undefined) continue; - const parsed = WorkflowTriggerField.array()(entry.triggerFields); - if (parsed instanceof type.errors) { - throw new Error( - `workflow-catalog entry "${entry.assetName}" has an invalid ` + - `triggerFields shape: ${parsed.summary}`, - ); - } -} - -const byAssetName = new Map( - WORKFLOW_CATALOG.map((entry) => [entry.assetName, entry]), -); - -export function isAutomatableWorkflowName(name: string): boolean { - return byAssetName.get(name)?.automatable === true; -} - -/** - * Whether a workflow definition name is fit to offer as a DM/chat target - * (the sidebar's agent rows, a taskable-agent picker, …) rather than a - * triggered automation utility. A name absent from this catalog is an - * agent-directory-created definition — "Agent definitions created at - * runtime are never listed here" (see `WORKFLOW_CATALOG`'s own comment) — - * and is always conversational; a name present here is conversational - * only when its entry says so (`assistant`/Myra today). - */ -export function isConversationalWorkflowName(name: string): boolean { - const entry = byAssetName.get(name); - return entry === undefined || entry.conversational; -} - -/** - * Whether a routine on this workflow needs a `deliveryWorkbenchId` at - * all — `false` only for a known `"inbox"`-delivering entry (see - * `WorkflowCatalogEntry.deliveryMode`). An unknown name defaults `true` - * (workbench required): the safe, prior-behavior default when a workflow - * isn't catalog-known at all. - */ -export function deliveryWorkbenchRequiredForWorkflowName( - name: string, -): boolean { - return byAssetName.get(name)?.deliveryMode !== "inbox"; -} - -/** The full catalog entry for an asset name, or `undefined` if it isn't - * a known workflow — the demo-card fields (`whatItDoes`, `exampleOutput`, - * etc.) live here alongside the display name. */ -export function workflowCatalogEntry( - name: string, -): WorkflowCatalogEntry | undefined { - return byAssetName.get(name); -} - -/** - * Friendly label for a workflow definition. Prefer the catalog display - * name, then a non-empty description, then a humanized asset name — never - * a raw definition id. - */ -export function workflowDisplayName( - name: string, - description?: string | null, -): string { - const entry = byAssetName.get(name); - if (entry !== undefined) return entry.displayName; - if (description !== undefined && description !== null) { - const trimmed = description.trim(); - if (trimmed.length > 0) return trimmed; - } - return humanizeAssetName(name); -} - -function humanizeAssetName(name: string): string { - return name - .split(/[-_]/) - .filter((part) => part.length > 0) - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(" "); -} - -export type TriggerFieldsValidation = - { readonly ok: true } | { readonly ok: false; readonly message: string }; - -/** - * The create-time boundary check for a routine's stored `input` - * (CL-6358): inputs bind at USE, never at creation, so a required - * field with no value at all in `input` is never a create-time - * rejection — the seeded last-30-days-research preset's whole reason - * for existing is a required "Topic" left open until someone actually - * runs it. Only a value the caller explicitly provided gets checked, - * and only for basic shape (a non-empty string) — a key present but - * blank is a caller bug, not an open input, and still rejected. - * `kind: "agent"` resolution (does a provided value name a real - * taskable definition) is a separate, still-eager check a host layers - * on top (`apps/hub/src/index.ts`'s `routineInputValid`) since it - * needs a tenant DB lookup this function can't do. Fire-time - * validation (a host's own launcher definition checks) remains the - * authoritative required-field gate. - */ -export function validateTriggerFieldsAtCreate( - fields: readonly WorkflowTriggerField[], - input: Record, -): TriggerFieldsValidation { - for (const field of fields) { - if (!(field.key in input)) continue; - const value = input[field.key]; - if (typeof value !== "string" || value.trim() === "") { - return { - ok: false, - message: `"${field.label}" must not be blank`, - }; - } - } - return { ok: true }; -} diff --git a/packages/workflow-catalog/src/instantiate.ts b/packages/workflow-catalog/src/instantiate.ts deleted file mode 100644 index 7370a3bcd..000000000 --- a/packages/workflow-catalog/src/instantiate.ts +++ /dev/null @@ -1,203 +0,0 @@ -// Turns a picked workbench definition into the state a freshly minted -// workbench needs: the agent definitions that don't already exist, the -// block workflows behind them, the still-pending plugin list the room -// persists, and the definition's ordered onboarding walkthrough handed -// to whatever surface runs it. Pure orchestration over injected ports — -// no HTTP, no store — so a host (today, `apps/web`'s -// `instant-agent-create.ts`) can bind the ports to its own REST clients -// and this stays testable with plain fakes. -// -// This resolves a definition whose non-Myra agents are backed by either -// `@corbits/code-review`'s reviewer roster (`CODE_REVIEW_TEMPLATE`) or a -// standalone chat agent this catalog installs the same way — Scout, for -// `DUE_DILIGENCE_TEMPLATE` (see `./participant-agent-requests.ts`). -// Jimmy resolves through the identical `ParticipantAgentRequest` shape -// (`jimmyAgentRequest()`), but no shipped definition names his handle -// today — he is not a "kind of workbench"; `@corbits/chat-ui`'s "Add -// Jimmy" quick-create row calls `jimmyAgentRequest()` directly instead. -// Kept registered here too so a future definition naming his handle -// resolves without new plumbing. -// -// A definition like `GTM_TEMPLATE`, whose agents are backed by their own -// deployed workflow definitions rather than an agent-directory create -// request, needs its own resolution path — calling this function against -// such a definition throws rather than silently doing nothing. -import { - codeReviewAgentRequests, - type CodeReviewAgentRequest, -} from "@corbits/code-review/agent-requests"; -import { - jimmyAgentRequest, - scoutAgentRequest, -} from "./participant-agent-requests"; - -import type { - WorkbenchDefinition, - WorkbenchOnboardingStep, - WorkbenchTemplateBlock, -} from "./templates"; - -/** The agent-directory create-request shape every agent resolves - * to: `CodeReviewAgentRequest`'s own fields, plus the tool-package pins - * a tool-calling agent (Scout, Jimmy) needs and a pure-text - * reviewer does not. */ -export type ParticipantAgentRequest = CodeReviewAgentRequest & { - readonly toolPackagePins?: readonly string[]; -}; - -export interface WorkbenchTemplateInstantiationPorts { - /** Every agent definition already deployed in the bench, as - * `{handle, id}` pairs. Doubles as the idempotency check (re-running - * instantiation — a retried create, a second workbench from the same - * template — never double-creates a reviewer) and as the id source - * for `inviteParticipantAgent` below: an agent definition the tenant - * already has is not yet a member of a freshly minted room, so - * its id still has to reach the invite call even when this function - * skips creating it. */ - listAgentHandles(): Promise< - readonly { readonly handle: string; readonly id: string }[] - >; - /** The agent-directory create path (`POST /agent-definitions`), or a - * fake of it in tests. */ - createParticipantAgent( - request: ParticipantAgentRequest, - ): Promise<{ readonly id: string }>; - /** Deploys one of the definition's referenced block workflows through - * the same source-form deploy the agent definitions use - * (`POST /template-blocks/:assetName/deploy` — see - * `./template-block-routes.ts`), or a fake of it in tests. `created` - * is `false` when the tenant already carries a deployed definition - * under this asset name, so a retried instantiation never - * double-deploys. */ - deployBlockWorkflow( - block: WorkbenchTemplateBlock, - ): Promise<{ readonly created: boolean }>; - /** Adds one agent definition to the newly created - * workbench's room (`POST /workbenches/:id/invite` — - * `@corbits/chat-ui`'s `inviteAgent`), or a fake of it in tests. This - * is what makes a definition's roster actually present in the room - * rather than merely registered in the agent directory. Called for - * Myra too: she is an existing principal invited after mint, not - * the room's host `definitionId`. */ - inviteParticipantAgent(id: string): Promise; - /** Persists the room's still-needed connections — the workbench - * settings `template/pendingConnections` key today; see - * `apps/web/src/instant-agent-create.ts`. */ - recordPendingConnections( - pendingConnections: readonly string[], - ): Promise; - /** Runs the definition's onboarding walkthrough in the freshly minted - * room — `apps/web` posts the first step's card through - * `postWorkbenchOnboardingStep`. Called last, and only when the - * definition has steps at all. */ - beginOnboarding(steps: readonly WorkbenchOnboardingStep[]): Promise; -} - -export interface WorkbenchTemplateInstantiationResult { - readonly createdHandles: readonly string[]; - readonly skippedHandles: readonly string[]; - /** Every non-Myra agent handle actually added to the room — - * `createdHandles` and `skippedHandles` combined, in definition order. - * A caller proving the roster a definition promises is really present - * checks this, not just that the definitions exist. */ - readonly invitedHandles: readonly string[]; - /** Block workflows `deployBlockWorkflow` actually deployed on this - * run, by asset name; a block the tenant already carried lands in - * `skippedBlockAssetNames` instead. */ - readonly deployedBlockAssetNames: readonly string[]; - readonly skippedBlockAssetNames: readonly string[]; - readonly pendingConnections: readonly string[]; - /** The ordered walkthrough handed to `beginOnboarding` — empty for a - * definition with no onboarding at all. */ - readonly onboardingSteps: readonly WorkbenchOnboardingStep[]; -} - -/** - * Resolves `definition` against the bench: creates the agent - * definitions that don't already exist (Myra is never re-created — she - * is the bench's seeded default setup agent, reused as-is and invited - * into the new channel), records the definition's required plugins as - * still pending, and then begins its onboarding walkthrough. Never - * registers a live webhook trigger itself: that is what the - * walkthrough's own start-reviewing step does, once the person has - * picked repos. - */ -export async function instantiateWorkbenchTemplate( - definition: WorkbenchDefinition, - ports: WorkbenchTemplateInstantiationPorts, -): Promise { - const existingIdsByHandle = new Map( - (await ports.listAgentHandles()).map((agent) => [agent.handle, agent.id]), - ); - const assistantId = existingIdsByHandle.get("assistant"); - if (assistantId !== undefined && !existingIdsByHandle.has("myra")) { - existingIdsByHandle.set("myra", assistantId); - } - const requestsByHandle = new Map( - [ - ...codeReviewAgentRequests(), - scoutAgentRequest(), - jimmyAgentRequest(), - ].map((request) => [request.handle, request]), - ); - - // The definition's referenced block workflows deploy first: an agent - // is a lens over a block, and the connect card's - // start-reviewing step resolves the deployed block definition by - // name, so a bench must never end up with reviewers but no - // `code-review` workflow behind them. - const deployedBlockAssetNames: string[] = []; - const skippedBlockAssetNames: string[] = []; - for (const block of definition.blocks) { - const outcome = await ports.deployBlockWorkflow(block); - (outcome.created ? deployedBlockAssetNames : skippedBlockAssetNames).push( - block.assetName, - ); - } - - const createdHandles: string[] = []; - const skippedHandles: string[] = []; - const invitedHandles: string[] = []; - for (const agent of definition.agents) { - const existingId = existingIdsByHandle.get(agent.handle); - let agentId: string; - if (existingId !== undefined) { - skippedHandles.push(agent.handle); - agentId = existingId; - } else if (agent.handle === "myra") { - // Seeded principal (`name: "assistant"`), never minted from a - // definition's roster. The create path already gates on - // `findMyraDefinition`; skip rather than throw. - continue; - } else { - const request = requestsByHandle.get(agent.handle); - if (request === undefined) { - throw new Error( - `workbench definition "${definition.id}" agent "${agent.handle}" ` + - "has no known create-agent request to instantiate it from", - ); - } - const created = await ports.createParticipantAgent(request); - createdHandles.push(agent.handle); - agentId = created.id; - } - await ports.inviteParticipantAgent(agentId); - invitedHandles.push(agent.handle); - } - - await ports.recordPendingConnections(definition.plugins.required); - - if (definition.onboardingSteps.length > 0) { - await ports.beginOnboarding(definition.onboardingSteps); - } - - return { - createdHandles, - skippedHandles, - invitedHandles, - deployedBlockAssetNames, - skippedBlockAssetNames, - pendingConnections: definition.plugins.required, - onboardingSteps: definition.onboardingSteps, - }; -} diff --git a/packages/workflow-catalog/src/participant-agent-requests.ts b/packages/workflow-catalog/src/participant-agent-requests.ts deleted file mode 100644 index 367f54bb6..000000000 --- a/packages/workflow-catalog/src/participant-agent-requests.ts +++ /dev/null @@ -1,49 +0,0 @@ -// Request-shaped views of this catalog's two standalone chat-agent -// participants (Scout, Jimmy), in the shape `./instantiate.ts`'s -// `createParticipantAgent` port takes — the same shape -// `@corbits/code-review/agent-requests`' `codeReviewAgentRequests` -// establishes for the reviewer roster, extended with `toolPackagePins` -// since neither Scout nor Jimmy is a pure-text agent. Built here, from -// each package's own exported definition, rather than inside -// `packages/scout-agent`/`packages/jimmy-agent` themselves: this -// package already owns "what a template participant needs to become an -// agent-directory create request" (it owns `codeReviewAgentRequests`' -// caller), so the mapping belongs beside it, not duplicated into every -// agent package that wants to be installable this way. -import { SCOUT_AGENT_DEFINITION } from "@corbits/scout-agent/definition"; -import { - JIMMY_AGENT_ID, - JIMMY_DESCRIPTION, - JIMMY_DISPLAY_NAME, - JIMMY_SYSTEM_PROMPT, - JIMMY_TOOL_PACKAGE_PINS, -} from "@corbits/jimmy-agent/metadata"; - -import type { ParticipantAgentRequest } from "./instantiate"; - -export function scoutAgentRequest(): ParticipantAgentRequest { - return { - name: SCOUT_AGENT_DEFINITION.displayName, - handle: SCOUT_AGENT_DEFINITION.handle, - description: SCOUT_AGENT_DEFINITION.description, - systemPrompt: SCOUT_AGENT_DEFINITION.systemPrompt, - toolPackagePins: SCOUT_AGENT_DEFINITION.toolPackagePins.map( - (pin) => pin.name, - ), - }; -} - -/** Jimmy's own package exports an `@intx/agent`-native `AgentDefinition` - * builder (`buildJimmyAgent`), not the `{handle, displayName, ...}` - * plain-data shape the agent-directory create path takes — it has no - * handle or display name of its own at all. `JIMMY_AGENT_ID` ("jimmy") - * is both his id and the handle a person types to reach him. */ -export function jimmyAgentRequest(): ParticipantAgentRequest { - return { - name: JIMMY_DISPLAY_NAME, - handle: JIMMY_AGENT_ID, - description: JIMMY_DESCRIPTION, - systemPrompt: JIMMY_SYSTEM_PROMPT, - toolPackagePins: JIMMY_TOOL_PACKAGE_PINS.map((pin) => pin.name), - }; -} diff --git a/packages/workflow-catalog/src/reconcile-duplicate-repo-grants.ts b/packages/workflow-catalog/src/reconcile-duplicate-repo-grants.ts deleted file mode 100644 index 2b6f66f7d..000000000 --- a/packages/workflow-catalog/src/reconcile-duplicate-repo-grants.ts +++ /dev/null @@ -1,55 +0,0 @@ -// One-time cleanup for pre-existing duplicate `repo:` -// grants a database can already carry from CL-7242's check-then-act -// race, before this fix's lease (`@corbits/webhook-triggers`' -// `repo_review_lease`) started preventing new ones. -// -// This is ordinary application code, not a migration: a plain DELETE -// against Interchange's own `grant` table, issued through `@intx/db`'s -// published `grant` export — the exact same table object -// `apps/hub/src/index.ts` already reads and writes through for every -// other grant operation in this codebase. No schema is touched (no -// `ALTER TABLE`, no index, no constraint), so this creates no re-pin -// debt: a DDL delta would need hand-reapplying at every future -// `vendor/intx/*` re-pin, but a DML statement runs once and leaves -// nothing behind for a re-pin to reconcile. Deliberately safe to -// re-run — a database with no duplicates left just deletes nothing. -import postgres from "postgres"; - -export interface ReconcileDuplicateRepoGrantsReport { - /** Ids of the duplicate rows removed, kept for the caller to log. */ - readonly removedIds: readonly string[]; -} - -/** - * Keeps the lowest-`id` row per `(tenant_id, resource, action)` among - * `repo:`-prefixed grants with an origin `mintRepoGrant` could plausibly - * have written and deletes the rest. `'creator'` is what the current - * `mintRepoGrantViaHttp` path (`apps/hub/src/native-repo-grants.ts`) - * writes; `'system'` is what the direct-insert path this PR replaced - * used to write, so a database carrying duplicates from either - * generation of this feature gets them cleaned up. Never touches any - * other grant family. - */ -export async function reconcileDuplicateRepoGrants( - databaseUrl: string, -): Promise { - const sql = postgres(databaseUrl, { max: 1, onnotice: () => undefined }); - try { - const removed = await sql<{ id: string }[]>` - DELETE FROM "grant" AS g - USING "grant" AS older - WHERE g.origin IN ('system', 'creator') - AND g.resource LIKE 'repo:%' - AND older.origin IN ('system', 'creator') - AND older.resource LIKE 'repo:%' - AND g.tenant_id = older.tenant_id - AND g.resource = older.resource - AND g.action = older.action - AND older.id < g.id - RETURNING g.id - `; - return { removedIds: removed.map((row) => row.id) }; - } finally { - await sql.end(); - } -} diff --git a/packages/workflow-catalog/src/settings.ts b/packages/workflow-catalog/src/settings.ts deleted file mode 100644 index e36c5c0d0..000000000 --- a/packages/workflow-catalog/src/settings.ts +++ /dev/null @@ -1,71 +0,0 @@ -// The `template/*` workbench-settings vocabulary: what a room minted -// from a template persists about that fact, so the shell can render -// its setup state (which connections still need connecting) without a -// second round trip. `@corbits/chat`'s settings route validates only -// its own `chat/*` keys and passes any other namespace through opaquely -// (see `packages/chat/src/workbench-settings.ts`) — this schema is the -// boundary check a caller runs before a `template/*` patch ever leaves -// the client, matching the "parse at every trust boundary" rule for a -// namespace `chat` deliberately leaves to its owner. -import { type } from "arktype"; - -export const TemplateSettingsPatch = type({ - "template/id": "string > 0", - "template/pendingConnections": "string[]", -}); -export type TemplateSettingsPatch = typeof TemplateSettingsPatch.infer; - -/** Builds and validates the settings patch a freshly instantiated - * template writes onto its workbench. Throws on a malformed id or - * connections list rather than sending bad data to the wire. */ -export function templateSettingsPatch( - templateId: string, - pendingConnections: readonly string[], -): TemplateSettingsPatch { - const patch = { - "template/id": templateId, - "template/pendingConnections": [...pendingConnections], - }; - const parsed = TemplateSettingsPatch(patch); - if (parsed instanceof type.errors) { - throw new Error(`invalid template settings patch: ${parsed.summary}`); - } - return parsed; -} - -/** - * The `github` connect card's own settled state (CL-6345): once a - * person has picked which repos to review, `./connect-github-setup.ts` - * writes both keys in the same patch — `pendingConnections` with - * `"github"` removed (this template needs nothing else, so that leaves - * it empty) and `selectedRepos` naming exactly what got a live webhook - * trigger. This patch rides the same `chat/*` settings PATCH route - * every other `template/*` write does; the card itself refetches its - * state as the direct consequence of the `start-reviewing` call that - * triggers this patch, same as it does for its own PAT submit - * (`ConnectGithubBlockContainer`) — never a fold off this stream event. - */ -export const TemplateReposSettingsPatch = type({ - "template/pendingConnections": "string[]", - "template/selectedRepos": "string[]", -}); -export type TemplateReposSettingsPatch = - typeof TemplateReposSettingsPatch.infer; - -/** Builds and validates the settings patch that records which repos a - * room's GitHub connect card started reviewing. Throws on a malformed - * repo id list rather than sending bad data to the wire. */ -export function templateReposSettingsPatch( - pendingConnections: readonly string[], - selectedRepoIds: readonly string[], -): TemplateReposSettingsPatch { - const patch = { - "template/pendingConnections": [...pendingConnections], - "template/selectedRepos": [...selectedRepoIds], - }; - const parsed = TemplateReposSettingsPatch(patch); - if (parsed instanceof type.errors) { - throw new Error(`invalid template repos settings patch: ${parsed.summary}`); - } - return parsed; -} diff --git a/packages/workflow-catalog/src/template-block-routes.test.ts b/packages/workflow-catalog/src/template-block-routes.test.ts deleted file mode 100644 index f6488c0d7..000000000 --- a/packages/workflow-catalog/src/template-block-routes.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -// Exercises `createTemplateBlockRoutes`' HTTP surface through the real -// `buildBlockWorkflowSource` builder, mounted the same way -// `./connect-github-routes.test.ts` mounts its routes: a bare `Hono` -// with a tenant-injecting middleware, every port a plain fake. -import { describe, expect, test } from "bun:test"; -import { Hono } from "hono"; -import type { MiddlewareHandler } from "hono"; -import type { RequireGrant, TenantEnv } from "@intx/hub-api"; - -import { - createTemplateBlockRoutes, - type TemplateBlockRoutesDeps, -} from "./template-block-routes"; - -const TENANT = { - id: "tnt_1", - name: "Acme", - slug: "acme", - domain: "acme.example", - parentId: null, - config: null, - createdAt: new Date(), - updatedAt: new Date(), -}; - -const PRINCIPAL = { - id: "prn_alice", - tenantId: TENANT.id, - kind: "user" as const, - refId: "prn_alice", - status: "active" as const, - createdAt: new Date(), - updatedAt: new Date(), -}; - -const allowAll: RequireGrant = () => async (_c, next) => { - await next(); -}; - -function mountAs(routes: Hono): Hono { - const asTenant: MiddlewareHandler = async (c, next) => { - c.set("tenant", TENANT); - c.set("principal", PRINCIPAL); - await next(); - }; - const app = new Hono(); - app.use("*", asTenant); - app.route("/", routes); - return app; -} - -type DeployedSource = Parameters< - TemplateBlockRoutesDeps["deployWorkflowSource"] ->[0]; - -function buildApp(overrides: Partial = {}) { - const deployed: DeployedSource[] = []; - const deps: TemplateBlockRoutesDeps = { - requireGrant: allowAll, - log: () => {}, - inferencePreferences: async () => [ - { provider: "anthropic", model: "claude-sonnet-5" }, - ], - deployWorkflowSource: async (args) => { - deployed.push(args); - return { id: "wfd_code_review", created: true }; - }, - ...overrides, - }; - return { app: mountAs(createTemplateBlockRoutes(deps)), deployed }; -} - -describe("POST /:assetName/deploy", () => { - test("deploys the code-review block as a source-form definition carrying the github tool pin", async () => { - const { app, deployed } = buildApp(); - const res = await app.request("/code-review/deploy", { method: "POST" }); - expect(res.status).toBe(201); - expect(await res.json()).toEqual({ id: "wfd_code_review", created: true }); - - expect(deployed).toHaveLength(1); - const source = deployed[0]; - if (source === undefined) throw new Error("nothing deployed"); - expect(source.tenantId).toBe(TENANT.id); - expect(source.principalId).toBe(PRINCIPAL.id); - expect(source.assetName).toBe("code-review"); - expect(source.displayName).toBe("Code review"); - - const definition = JSON.parse(source.workflowJson) as { - triggers: { type: string; to: string }[]; - steps: Record< - string, - { agent: { toolPackagePins?: { name: string }[] } } - >; - }; - expect(definition.triggers).toEqual([ - { type: "mail", to: "code-review@acme.example" }, - ]); - const pins = Object.values(definition.steps).flatMap( - (step) => step.agent.toolPackagePins ?? [], - ); - expect(pins.map((pin) => pin.name)).toContain("@corbits/github-tools"); - }); - - test("an already-deployed block answers 200 with created: false", async () => { - const { app } = buildApp({ - deployWorkflowSource: async () => ({ - id: "wfd_existing", - created: false, - }), - }); - const res = await app.request("/code-review/deploy", { method: "POST" }); - expect(res.status).toBe(200); - expect(await res.json()).toEqual({ id: "wfd_existing", created: false }); - }); - - test("an asset name no block builder covers answers 404, deploying nothing", async () => { - const { app, deployed } = buildApp(); - const res = await app.request("/granola-call/deploy", { method: "POST" }); - expect(res.status).toBe(404); - expect(deployed).toHaveLength(0); - }); - - test("a failing deploy port answers a 500 envelope, never a raw error", async () => { - const { app } = buildApp({ - deployWorkflowSource: async () => { - throw new Error("disk full"); - }, - }); - const res = await app.request("/code-review/deploy", { method: "POST" }); - expect(res.status).toBe(500); - const body = (await res.json()) as { error?: { userMessage?: string } }; - expect(JSON.stringify(body)).not.toContain("disk full"); - }); -}); diff --git a/packages/workflow-catalog/src/template-block-routes.ts b/packages/workflow-catalog/src/template-block-routes.ts deleted file mode 100644 index a07a9f4aa..000000000 --- a/packages/workflow-catalog/src/template-block-routes.ts +++ /dev/null @@ -1,108 +0,0 @@ -// The instantiate path's block-workflow deploy surface (CL-6405): -// `instantiateWorkbenchTemplate`'s `deployBlockWorkflow` port binds to -// `POST /:assetName/deploy` here, the way its `createParticipantAgent` -// port binds to `POST /agent-definitions`. Mounted per-tenant inside -// the platform's native tenant middleware, mirroring -// `./connect-github-routes.ts`: every side effect a host needs — the -// tenant's default inference preferences, and the actual source-form -// asset write + `workflow_definition` projection — arrives as an -// injected port, so `apps/hub` is the only place drizzle and the -// `AssetService` are touched and this stays testable with plain fakes. -import { Hono } from "hono"; -import type { RequireGrant, TenantEnv } from "@intx/hub-api"; -import { makeErrorEnvelope } from "@workbench/hub-client"; - -import { - buildBlockWorkflowSource, - type BlockWorkflowBuildInput, -} from "./block-workflows"; - -const DEPLOY_FAILED_MESSAGE = - "Couldn't set up this template's workflow. Try again in a moment."; - -export type TemplateBlockRoutesDeps = { - requireGrant: RequireGrant; - /** Where a failure's real cause goes — the same CL-6360 idiom - * `./connect-github-routes.ts` documents: the client sees one honest - * `userMessage`, the raw detail lands here. */ - log: (line: string) => void; - /** The tenant's default inference preferences — the same resolution a - * fresh workbench host launches against (`@corbits/chat`'s - * `createWorkbenchHostInferencePreferencesResolver`), so a deployed - * block runs on the model the bench actually connected. */ - inferencePreferences( - tenantId: string, - ): Promise; - /** The source-form deploy itself: renders `workflowJson` into a - * `@corbits/workflows`'s `./source` tree on a `workflow`-kind asset and - * projects it onto a `workflow_definition` row — the exact - * materialization `createAgentDefinitionCore` runs for a participant - * agent, minus its agent-only prompt/skills machinery. `created` is - * `false` when the tenant already carries a deployed definition under - * `assetName` (the port skips instead of double-deploying). */ - deployWorkflowSource(args: { - readonly tenantId: string; - readonly principalId: string; - readonly assetName: string; - readonly displayName: string; - readonly workflowJson: string; - }): Promise<{ readonly id: string; readonly created: boolean }>; -}; - -export function createTemplateBlockRoutes( - deps: TemplateBlockRoutesDeps, -): Hono { - const app = new Hono(); - - app.post( - "/:assetName/deploy", - deps.requireGrant("workflow:*", "create"), - async (c) => { - const tenant = c.get("tenant"); - const principal = c.get("principal"); - const assetName = c.req.param("assetName"); - - const source = buildBlockWorkflowSource(assetName, { - tenantDomain: tenant.domain, - inferencePreferences: await deps.inferencePreferences(tenant.id), - }); - if (source === undefined) { - return c.json( - makeErrorEnvelope({ - code: "not_found", - userMessage: `"${assetName}" isn't a deployable template workflow.`, - }), - 404, - ); - } - - try { - const result = await deps.deployWorkflowSource({ - tenantId: tenant.id, - principalId: principal.id, - assetName: source.assetName, - displayName: source.displayName, - workflowJson: source.workflowJson, - }); - return c.json( - { id: result.id, created: result.created }, - result.created ? 201 : 200, - ); - } catch (cause) { - const message = cause instanceof Error ? cause.message : String(cause); - deps.log( - `template-blocks: deploying "${assetName}" failed for tenant ${tenant.id}: ${message}`, - ); - return c.json( - makeErrorEnvelope({ - code: "deploy_failed", - userMessage: DEPLOY_FAILED_MESSAGE, - }), - 500, - ); - } - }, - ); - - return app; -} diff --git a/packages/workflow-catalog/src/templates.ts b/packages/workflow-catalog/src/templates.ts deleted file mode 100644 index 2600f11d1..000000000 --- a/packages/workflow-catalog/src/templates.ts +++ /dev/null @@ -1,647 +0,0 @@ -// Workbench definitions: the single description of what "pick a kind of -// workbench" actually creates — its default agents, routines, tools, -// plugins, and the ordered onboarding walkthrough a person works -// through once the room exists. -// -// `./index.ts`'s `WORKFLOW_CATALOG` describes ONE workflow at a time — -// what it does, what it needs connected, what its trigger carries. A -// definition is the layer above: a named workbench worth having, -// assembled out of several of those workflows. The three shipped -// definitions here are the bench library's templates; a *template* is a -// shipped definition, not a second kind of thing. -// -// Everything here is pure data. Creating a workbench from a definition -// is a host concern (`apps/web`'s picker starts the flow); this module -// is the single description both the picker and the creator read, so -// neither hand-types an asset name, a cron, a connector id, or a step -// of onboarding copy. -// -// Blocks are referenced by asset name AND version. A definition names -// the exact `workflows//package.json` version it was designed -// against, so bumping a workflow is a deliberate edit here rather than -// a silent change in what it creates. - -import { type } from "arktype"; -// Imported from the package's own `./reviewers` subpath, never its root -// (`@corbits/code-review`) — the root barrel also re-exports the review -// run and GitHub client, which pull in `@corbits/github-tools` and -// `@intx/agent`'s full provider surface. `reviewers.ts` itself has no -// imports at all, so this subpath keeps every consumer of this -// definition (this package's whole point) off that much heavier graph. -import { CODE_REVIEW_REVIEWERS } from "@corbits/code-review/reviewers"; -import { - SCOUT_AGENT_HANDLE, - SCOUT_AGENT_DISPLAY_NAME, - SCOUT_AGENT_DESCRIPTION, - SCOUT_TOOL_PACKAGE_PINS, -} from "@corbits/scout-agent/definition"; -/** One workflow a template installs, pinned to the version it was - * designed against. `assetName` matches a `WORKFLOW_CATALOG` entry. */ -export const WorkbenchTemplateBlock = type({ - assetName: "string > 0", - version: "/^[0-9]+\\.[0-9]+\\.[0-9]+$/", -}); -export type WorkbenchTemplateBlock = typeof WorkbenchTemplateBlock.infer; - -/** - * One routine a template creates on install. `cron` is a 5-field - * expression in the grammar `@corbits/routines`' `cron.ts` speaks — the - * authority on whether one is valid and when it fires; the shape check - * here only catches a malformed literal at module load. - */ -export const WorkbenchTemplateRoutine = type({ - /** Stable key an open input can point at. Unique within a template. */ - key: "/^[a-z][a-z0-9-]*$/", - /** Which block runs on this schedule. */ - blockAssetName: "string > 0", - /** What the person sees this routine called. */ - label: "string > 0", - cron: "/^\\S+ \\S+ \\S+ \\S+ \\S+$/", - /** One honest line: why this runs on a clock at all. */ - why: "string > 0", -}); -export type WorkbenchTemplateRoutine = typeof WorkbenchTemplateRoutine.infer; - -/** - * One agent a person can address in the created workbench. `handle` is - * what they type to reach it. `blockAssetName` names the workflow behind - * it when the agent is a lens over one of the definition's own blocks - * (the code-review reviewers); it is absent for an agent that is a - * standalone chat agent installed straight through the agent-directory - * create path (Scout, Jimmy) with no block of its own to reference. - */ -export const WorkbenchDefinitionAgent = type({ - handle: "/^[a-z][a-z0-9-]*$/", - displayName: "string > 0", - "blockAssetName?": "string > 0", - /** One honest line: what this agent is for. */ - role: "string > 0", -}); -export type WorkbenchDefinitionAgent = typeof WorkbenchDefinitionAgent.infer; - -/** - * One workflow a template fires from an inbound webhook rather than a - * clock — the PR-review trigger a code-review template needs, as opposed - * to `WorkbenchTemplateRoutine`'s cron-scheduled kind. `triggerFieldKey` - * names the block's own `WorkflowCatalogEntry.triggerFields` entry the - * webhook payload fills — see `./index.ts`'s `WorkflowTriggerField`. - * Creating the live `webhook_trigger` row itself (`@corbits/webhook-triggers`) - * needs a repo to scope it to, which only exists once the person has - * answered the template's own open input for it — this is the spec a - * create flow resolves against that answer, not the row. - */ -export const WorkbenchTemplateWebhookTrigger = type({ - /** Stable key an open input can point at. Unique within a template. */ - key: "/^[a-z][a-z0-9-]*$/", - /** Which block this webhook launches a run of. */ - blockAssetName: "string > 0", - /** What the person sees this trigger called. */ - label: "string > 0", - /** One honest line: why this fires on a webhook instead of a clock. */ - why: "string > 0", - triggerFieldKey: "/^[a-zA-Z][a-zA-Z0-9]*$/", -}); -export type WorkbenchTemplateWebhookTrigger = - typeof WorkbenchTemplateWebhookTrigger.infer; - -/** - * One answer the template cannot supply for the person — the questions - * the create flow asks before anything runs. Exactly one of - * `appliesToRoutine` / `appliesToWebhookTrigger` names the trigger this - * input's answer feeds: a cron-scheduled routine, or a webhook trigger - * spec. - */ -export const WorkbenchTemplateOpenInput = type({ - key: "/^[a-zA-Z][a-zA-Z0-9]*$/", - label: "string > 0", - "placeholder?": "string", - help: "string > 0", - required: "boolean", - "appliesToRoutine?": "/^[a-z][a-z0-9-]*$/", - "appliesToWebhookTrigger?": "/^[a-z][a-z0-9-]*$/", -}); -export type WorkbenchTemplateOpenInput = - typeof WorkbenchTemplateOpenInput.infer; - -/** - * One step in a definition's onboarding walkthrough, in the order a - * person works through it. This is the *definition-level* step — the - * full ordered walkthrough a template describes. `@corbits/chat`'s own - * `WorkbenchOnboardingStep` is the narrower wire-level body a host - * posts into a room to raise one card; the two are different types and - * neither is derived from the other. - */ -export const WorkbenchOnboardingStep = type({ - kind: "'connect-plugin'", - connectorId: "string > 0", - title: "string > 0", - why: "string > 0", -}) - .or({ kind: "'pick-github-repos'", title: "string > 0", why: "string > 0" }) - .or({ - kind: "'start-webhook-trigger'", - webhookTriggerKey: "/^[a-z][a-z0-9-]*$/", - title: "string > 0", - why: "string > 0", - }); -export type WorkbenchOnboardingStep = typeof WorkbenchOnboardingStep.infer; - -/** - * The full definition, as parsed back off a trust boundary — the bench - * library row a hub seeded (see `@corbits/artifacts-hub`'s template - * library) travels over HTTP before a picker instantiates from it, so - * it re-enters through this schema, never through `as`. - */ -export const WorkbenchDefinitionSchema = type({ - id: "/^[a-z][a-z0-9-]*$/", - title: "string > 0", - promise: "string > 0", - blocks: WorkbenchTemplateBlock.array(), - plugins: { required: "string[]", optional: "string[]" }, - tools: "string[]", - routines: WorkbenchTemplateRoutine.array(), - webhookTriggers: WorkbenchTemplateWebhookTrigger.array(), - agents: WorkbenchDefinitionAgent.array(), - openInputs: WorkbenchTemplateOpenInput.array(), - onboardingSteps: WorkbenchOnboardingStep.array(), -}); - -export type WorkbenchDefinition = { - readonly id: string; - /** What the picker row calls it. */ - readonly title: string; - /** The picker row's one-line promise, in the reader's language. */ - readonly promise: string; - readonly blocks: readonly WorkbenchTemplateBlock[]; - /** - * Connector ids (see `@workbench/connections`' `CONNECTOR_REGISTRY` and - * `MCP_PRESETS`): `required` is what this definition cannot work - * without, in the order the walkthrough asks for them; `optional` - * makes it better and never gates the create. - */ - readonly plugins: { - readonly required: readonly string[]; - readonly optional: readonly string[]; - }; - /** Tool-package names this definition's agents need — package names - * only, never `{name, version}` pins: the pinned version lives with - * the agent package that owns the tool. */ - readonly tools: readonly string[]; - readonly routines: readonly WorkbenchTemplateRoutine[]; - /** Webhook-fired triggers this definition installs — empty for a - * clock-only definition like GTM. */ - readonly webhookTriggers: readonly WorkbenchTemplateWebhookTrigger[]; - readonly agents: readonly WorkbenchDefinitionAgent[]; - readonly openInputs: readonly WorkbenchTemplateOpenInput[]; - /** The ordered walkthrough a freshly created workbench runs — connect - * the plugin, pick what it works on, start the trigger. */ - readonly onboardingSteps: readonly WorkbenchOnboardingStep[]; -}; - -/** - * The GTM template (CL-6349): the go-to-market workbench, ported from the - * OG gtm-workbench's four v1 workflows. - * - * The call backbone (granola-call discovering new calls, process-granola-call - * writing each one up) is what makes the workbench useful on day one, so it - * is the routine that runs on a clock without anyone asking. The web watch - * runs on its own weekly clock. The CRM agent and the collateral drafter are - * on-demand: both start from a specific thing a person points at. - */ -export const GTM_TEMPLATE: WorkbenchDefinition = { - id: "gtm", - title: "Go to market", - promise: - "Your calls get written up, your CRM tasks get worked, and the web gets watched — you approve anything that leaves the room.", - blocks: [ - { assetName: "granola-call", version: "0.0.1" }, - { assetName: "process-granola-call", version: "0.0.1" }, - { assetName: "attio-task-agent", version: "0.0.1" }, - { assetName: "exa-topic-watch", version: "0.0.1" }, - { assetName: "pain-point-collateral", version: "0.0.1" }, - ], - // Attio and Exa are hard requirements: the CRM agent has nothing to - // work without Attio, and the web watch has nothing to read without - // Exa. Granola is what the call backbone runs on — the create flow - // offers it up front, but a workbench with the CRM agent and the web - // watch alone is still a real workbench, so it never blocks the create. - plugins: { required: ["attio", "exa"], optional: ["granola"] }, - tools: [], - webhookTriggers: [], - routines: [ - { - key: "call-discovery", - blockAssetName: "granola-call", - label: "Check for new calls", - cron: "*/30 9-18 * * 1-5", - why: "Notes are worth most right after the call, so this looks for new ones through the working day rather than once overnight.", - }, - { - key: "topic-watch", - blockAssetName: "exa-topic-watch", - label: "Watch the web", - cron: "0 8 * * 1", - why: "One digest at the start of the week, so a quiet week reads as quiet instead of as five empty runs.", - }, - ], - agents: [ - { - handle: "crm", - displayName: "CRM task agent", - blockAssetName: "attio-task-agent", - role: "Point it at a CRM task and it works the task — reading first, drafting second, and asking before it writes anything back.", - }, - { - handle: "collateral", - displayName: "Pain-point collateral", - blockAssetName: "pain-point-collateral", - role: "Give it a call transcript and it drafts a piece aimed at the pain point the customer actually named.", - }, - ], - openInputs: [ - { - key: "topic", - label: "What should we watch?", - placeholder: "AI coding agents for go-to-market", - help: "One topic, as specific as you can make it. The weekly digest covers this and nothing else.", - required: true, - appliesToRoutine: "topic-watch", - }, - ], - // GTM is not offered by the picker yet, and both its instantiation - // paths say so out loud rather than half-creating a workbench: - // `instantiateWorkbenchTemplate` throws because its agents are lenses - // over deployed workflow definitions with no agent-directory create - // request, and the web `beginOnboarding` binding throws because - // neither Attio nor Exa has an in-room onboarding card. - onboardingSteps: [ - { - kind: "connect-plugin", - connectorId: "attio", - title: "Connect Attio", - why: "The CRM agent reads and works your tasks in Attio — without it there is nothing to work.", - }, - { - kind: "connect-plugin", - connectorId: "exa", - title: "Connect Exa", - why: "The weekly web watch reads the open web through Exa.", - }, - { - kind: "connect-plugin", - connectorId: "granola", - title: "Connect Granola", - why: "Connect it and every new call gets written up on its own; skip it and the rest of the workbench still works.", - }, - ], -}; - -/** - * The code-review definition: three reviewer lenses over every pull - * request. Its blocks install the one `code-review` workflow; the - * reviewer roster itself is `@corbits/code-review`'s own - * `CODE_REVIEW_REVIEWERS` — mirrored into `agents` here rather than - * duplicated, so a reviewer's handle, name, and one-line role can never - * drift between the package that runs the review and the definition - * that describes it. - */ -export const CODE_REVIEW_TEMPLATE: WorkbenchDefinition = { - id: "code-review", - title: "Code review", - promise: - "Three reviewers read every pull request and post what they'd change.", - blocks: [{ assetName: "code-review", version: "0.0.1" }], - // GitHub is the one thing this template cannot work without: no - // repository, no diff to read and nowhere to post the review. - plugins: { required: ["github"], optional: [] }, - tools: ["@corbits/github-tools"], - routines: [], - webhookTriggers: [ - { - key: "pull-request-opened", - blockAssetName: "code-review", - label: "Review new pull requests", - why: "A review is worth most posted while the pull request is still open for comment, so this fires the moment GitHub says one exists rather than waiting on a clock.", - triggerFieldKey: "pullRequestUrl", - }, - ], - agents: [ - ...CODE_REVIEW_REVIEWERS.map((reviewer) => ({ - handle: reviewer.handle, - displayName: reviewer.displayName, - blockAssetName: "code-review", - role: reviewer.description, - })), - ], - openInputs: [ - { - key: "repos", - label: "Which repositories?", - placeholder: "corbitsdev/workbench", - help: "The GitHub repositories to watch. Every new pull request there gets reviewed.", - required: true, - appliesToWebhookTrigger: "pull-request-opened", - }, - ], - onboardingSteps: [ - { - kind: "connect-plugin", - connectorId: "github", - title: "Connect GitHub", - why: "Create a token scoped to the repositories you want reviewed — that is what the reviewers will be able to read.", - }, - { - kind: "pick-github-repos", - title: "Choose what gets reviewed", - why: "Of the repositories your token reaches, these are the ones a new pull request starts a review in.", - }, - { - kind: "start-webhook-trigger", - webhookTriggerKey: "pull-request-opened", - title: "Start reviewing", - why: "From now on, every new pull request in those repositories gets a review.", - }, - ], -}; - -/** - * The due-diligence template (CL-6499): Scout for the web/firm-memory - * research and Myra to talk through what it found. Scout is a - * standalone chat agent, not a lens over a block workflow — it has no - * cron, no webhook, nothing to schedule — so it carries no - * `blockAssetName` and this template's `blocks` list stays empty. - * Exa (Scout's web-research tool) resolves through the keyless MCP - * preset, so nothing here blocks the create on a connection. - */ -export const DUE_DILIGENCE_TEMPLATE: WorkbenchDefinition = { - id: "due-diligence", - title: "Due Diligence", - promise: - "Scout checks a company, deal, or vendor against the web and what your team already knows, and saves what it finds so you can pick it up later.", - blocks: [], - plugins: { required: [], optional: ["exa"] }, - tools: SCOUT_TOOL_PACKAGE_PINS.map((pin) => pin.name), - routines: [], - webhookTriggers: [], - agents: [ - { - handle: "myra", - displayName: "Myra", - role: "Talks through what Scout found and helps you decide what to do with it.", - }, - { - handle: SCOUT_AGENT_HANDLE, - displayName: SCOUT_AGENT_DISPLAY_NAME, - role: SCOUT_AGENT_DESCRIPTION, - }, - ], - openInputs: [], - onboardingSteps: [], -}; - -export const WORKBENCH_TEMPLATES: readonly WorkbenchDefinition[] = [ - GTM_TEMPLATE, - CODE_REVIEW_TEMPLATE, - DUE_DILIGENCE_TEMPLATE, -]; - -const templateById = new Map( - WORKBENCH_TEMPLATES.map((template) => [template.id, template]), -); - -export function workbenchTemplate(id: string): WorkbenchDefinition | undefined { - return templateById.get(id); -} - -/** - * Every asset name a definition names — its blocks, and the block behind - * each routine and agent. A caller checking a definition against - * `WORKFLOW_CATALOG` walks this rather than three separate arrays. - */ -export function templateBlockAssetNames( - definition: WorkbenchDefinition, -): readonly string[] { - return definition.blocks.map((block) => block.assetName); -} - -/** The shipped definitions as bench-library seed entries — the ONE - * serialization every seeder uses (`apps/hub`'s boot seed and the eval - * harness's scratch-hub seed), so the two can never drift. */ -export function workbenchTemplateLibraryEntries(): readonly { - readonly id: string; - readonly content: string; -}[] { - return WORKBENCH_TEMPLATES.map((template) => ({ - id: template.id, - content: serializeWorkbenchDefinition(template), - })); -} - -export function serializeWorkbenchDefinition( - definition: WorkbenchDefinition, -): string { - return JSON.stringify(definition, null, 2); -} - -/** - * Parses a seeded library row's content back into a definition, running - * the same cross-reference checks module load runs on the shipped - * constants. Throws on anything malformed — an unreadable library row - * is a seeding defect to surface, never a shape to limp past. - */ -export function parseWorkbenchDefinition(data: unknown): WorkbenchDefinition { - const raw = typeof data === "string" ? JSON.parse(data) : data; - const parsed = WorkbenchDefinitionSchema(raw); - if (parsed instanceof type.errors) { - throw new Error(`workbench definition failed to parse: ${parsed.summary}`); - } - assertValid(parsed); - return parsed; -} - -function assertValid(definition: WorkbenchDefinition): void { - const blockNames = new Set(templateBlockAssetNames(definition)); - const parsedBlocks = WorkbenchTemplateBlock.array()(definition.blocks); - if (parsedBlocks instanceof type.errors) { - throw new Error( - `workbench definition "${definition.id}" has an invalid blocks shape: ${parsedBlocks.summary}`, - ); - } - const parsedRoutines = WorkbenchTemplateRoutine.array()(definition.routines); - if (parsedRoutines instanceof type.errors) { - throw new Error( - `workbench definition "${definition.id}" has an invalid routines shape: ${parsedRoutines.summary}`, - ); - } - const parsedAgents = WorkbenchDefinitionAgent.array()(definition.agents); - if (parsedAgents instanceof type.errors) { - throw new Error( - `workbench definition "${definition.id}" has an invalid agents shape: ${parsedAgents.summary}`, - ); - } - const parsedInputs = WorkbenchTemplateOpenInput.array()( - definition.openInputs, - ); - if (parsedInputs instanceof type.errors) { - throw new Error( - `workbench definition "${definition.id}" has an invalid openInputs shape: ${parsedInputs.summary}`, - ); - } - const parsedWebhookTriggers = WorkbenchTemplateWebhookTrigger.array()( - definition.webhookTriggers, - ); - if (parsedWebhookTriggers instanceof type.errors) { - throw new Error( - `workbench definition "${definition.id}" has an invalid webhookTriggers shape: ${parsedWebhookTriggers.summary}`, - ); - } - const parsedSteps = WorkbenchOnboardingStep.array()( - definition.onboardingSteps, - ); - if (parsedSteps instanceof type.errors) { - throw new Error( - `workbench definition "${definition.id}" has an invalid onboardingSteps shape: ${parsedSteps.summary}`, - ); - } - if (new Set(definition.tools).size !== definition.tools.length) { - throw new Error( - `workbench definition "${definition.id}" names the same tool package more than once`, - ); - } - const routineKeys = new Set( - definition.routines.map((routine) => routine.key), - ); - for (const routine of definition.routines) { - if (!blockNames.has(routine.blockAssetName)) { - throw new Error( - `workbench definition "${definition.id}" routine "${routine.key}" runs "${routine.blockAssetName}", which the definition does not install`, - ); - } - } - const webhookTriggerKeys = new Set( - definition.webhookTriggers.map((trigger) => trigger.key), - ); - for (const trigger of definition.webhookTriggers) { - if (!blockNames.has(trigger.blockAssetName)) { - throw new Error( - `workbench definition "${definition.id}" webhook trigger "${trigger.key}" fires "${trigger.blockAssetName}", which the definition does not install`, - ); - } - } - for (const agent of definition.agents) { - if ( - agent.blockAssetName !== undefined && - !blockNames.has(agent.blockAssetName) - ) { - throw new Error( - `workbench definition "${definition.id}" agent "${agent.handle}" is backed by "${agent.blockAssetName}", which the definition does not install`, - ); - } - } - for (const input of definition.openInputs) { - const appliesToCount = - Number(input.appliesToRoutine !== undefined) + - Number(input.appliesToWebhookTrigger !== undefined); - if (appliesToCount !== 1) { - throw new Error( - `workbench definition "${definition.id}" input "${input.key}" must apply to exactly one of a routine or a webhook trigger`, - ); - } - if ( - input.appliesToRoutine !== undefined && - !routineKeys.has(input.appliesToRoutine) - ) { - throw new Error( - `workbench definition "${definition.id}" input "${input.key}" applies to routine "${input.appliesToRoutine}", which the definition does not create`, - ); - } - if ( - input.appliesToWebhookTrigger !== undefined && - !webhookTriggerKeys.has(input.appliesToWebhookTrigger) - ) { - throw new Error( - `workbench definition "${definition.id}" input "${input.key}" applies to webhook trigger "${input.appliesToWebhookTrigger}", which the definition does not create`, - ); - } - } - assertOnboardingWalkthrough(definition); -} - -/** - * The walkthrough is ordered, so its checks are ordered too: a step can - * only ask for something an earlier step made possible, and a required - * plugin with no step to connect it is a definition that promises setup - * it never asks for. - */ -function assertOnboardingWalkthrough(definition: WorkbenchDefinition): void { - const declaredPlugins = new Set([ - ...definition.plugins.required, - ...definition.plugins.optional, - ]); - const connectedSoFar = new Set(); - const startedTriggerKeys = new Set(); - let pickedRepos = false; - - for (const step of definition.onboardingSteps) { - if (step.kind === "connect-plugin") { - if (!declaredPlugins.has(step.connectorId)) { - throw new Error( - `workbench definition "${definition.id}" onboarding connects "${step.connectorId}", which is not one of its plugins`, - ); - } - connectedSoFar.add(step.connectorId); - continue; - } - if (step.kind === "pick-github-repos") { - if (!connectedSoFar.has("github")) { - throw new Error( - `workbench definition "${definition.id}" asks for repos before its onboarding connects "github"`, - ); - } - pickedRepos = true; - continue; - } - if (!webhookTriggerKeyExists(definition, step.webhookTriggerKey)) { - throw new Error( - `workbench definition "${definition.id}" onboarding starts webhook trigger "${step.webhookTriggerKey}", which the definition does not create`, - ); - } - if (startedTriggerKeys.has(step.webhookTriggerKey)) { - throw new Error( - `workbench definition "${definition.id}" onboarding starts webhook trigger "${step.webhookTriggerKey}" more than once`, - ); - } - if (!pickedRepos) { - throw new Error( - `workbench definition "${definition.id}" starts webhook trigger "${step.webhookTriggerKey}" before its onboarding picks repos`, - ); - } - startedTriggerKeys.add(step.webhookTriggerKey); - } - - for (const connectorId of definition.plugins.required) { - if (!connectedSoFar.has(connectorId)) { - throw new Error( - `workbench definition "${definition.id}" requires plugin "${connectorId}" but its onboarding never asks anyone to connect it`, - ); - } - } - for (const trigger of definition.webhookTriggers) { - if (!startedTriggerKeys.has(trigger.key)) { - throw new Error( - `workbench definition "${definition.id}" webhook trigger "${trigger.key}" is never started by an onboarding step`, - ); - } - } -} - -function webhookTriggerKeyExists( - definition: WorkbenchDefinition, - key: string, -): boolean { - return definition.webhookTriggers.some((trigger) => trigger.key === key); -} - -for (const definition of WORKBENCH_TEMPLATES) { - assertValid(definition); -} diff --git a/packages/workflow-catalog/test/catalog.test.ts b/packages/workflow-catalog/test/catalog.test.ts deleted file mode 100644 index 75c6b88b3..000000000 --- a/packages/workflow-catalog/test/catalog.test.ts +++ /dev/null @@ -1,373 +0,0 @@ -import { type } from "arktype"; -import { describe, expect, test } from "bun:test"; - -import { CONNECTOR_REGISTRY } from "@workbench/connections/registry"; -import { MCP_PRESETS } from "@workbench/connections/mcp-presets"; - -import { - deliveryWorkbenchRequiredForWorkflowName, - isAutomatableWorkflowName, - isConversationalWorkflowName, - validateTriggerFieldsAtCreate, - workflowDisplayName, - workflowCatalogEntry, - WorkflowTriggerField, - WORKFLOW_CATALOG, -} from "../src/index"; - -describe("workflow catalog", () => { - test("marks workbench-digest, heartbeat, and morning-brief automatable, not echo or assistant", () => { - expect(isAutomatableWorkflowName("workbench-digest")).toBe(true); - expect(isAutomatableWorkflowName("heartbeat")).toBe(true); - expect(isAutomatableWorkflowName("morning-brief")).toBe(true); - expect(isAutomatableWorkflowName("echo")).toBe(false); - expect(isAutomatableWorkflowName("assistant")).toBe(false); - }); - - test("marks the granola-call parent automatable, not its process-granola-call child", () => { - // The parent is schedule-attachable; the child is spawned per call and - // must never appear as an independent Routines-picker option. - expect(isAutomatableWorkflowName("granola-call")).toBe(true); - expect(isAutomatableWorkflowName("process-granola-call")).toBe(false); - }); - - test("prefers the catalog display name for the granola-call workflows", () => { - expect(workflowDisplayName("granola-call")).toBe("Granola call notes"); - expect(workflowDisplayName("process-granola-call")).toBe( - "Process Granola call", - ); - }); - - test("does not mark pain-point-collateral automatable — its approval gate is a poor fit for unattended scheduling", () => { - expect(isAutomatableWorkflowName("pain-point-collateral")).toBe(false); - expect(workflowDisplayName("pain-point-collateral")).toBe( - "Pain-point collateral", - ); - }); - - test("does not mark collateral-generation automatable — on-demand only, gated behind its final approval", () => { - expect(isAutomatableWorkflowName("collateral-generation")).toBe(false); - expect(workflowDisplayName("collateral-generation")).toBe( - "Collateral generation", - ); - }); - - test("marks reddit-opportunity-scanner on-demand — its approval gates make it a poor unattended fit", () => { - expect(isAutomatableWorkflowName("reddit-opportunity-scanner")).toBe(false); - expect(workflowDisplayName("reddit-opportunity-scanner")).toBe( - "Reddit opportunity scanner", - ); - }); - - test("does not mark last-30-days-research automatable — on-demand only, gated behind a human-supplied topic per run", () => { - expect(isAutomatableWorkflowName("last-30-days-research")).toBe(false); - expect(workflowDisplayName("last-30-days-research")).toBe( - "Last 30 days research report", - ); - }); - - test("rejects agent handles and workbench-host names as automatable", () => { - expect(isAutomatableWorkflowName("my-researcher")).toBe(false); - expect(isAutomatableWorkflowName("workbench-host-abc")).toBe(false); - expect(isAutomatableWorkflowName("wfd_deadbeef")).toBe(false); - }); - - test("prefers catalog display names over raw asset names", () => { - expect(workflowDisplayName("workbench-digest")).toBe("Workbench digest"); - expect(workflowDisplayName("heartbeat")).toBe("Heartbeat"); - expect(workflowDisplayName("morning-brief")).toBe("Morning brief"); - expect(workflowDisplayName("echo")).toBe("Echo"); - expect(workflowDisplayName("assistant")).toBe("Myra"); - }); - - test("productizes the seeded assistant under the Myra display name", () => { - // The assistant workflow ships in DEFAULT_WORKFLOWS for every personal - // bench. Its catalog display name is the productized label Myra, not - // the generic "Assistant" — the routines picker and seeded asset both - // read it from here. - expect(workflowDisplayName("assistant")).toBe("Myra"); - const entry = WORKFLOW_CATALOG.find((e) => e.assetName === "assistant"); - expect(entry?.displayName).toBe("Myra"); - }); - - test("falls back to description, then humanized name — never blank", () => { - expect(workflowDisplayName("unknown-flow", " Weekly brief ")).toBe( - "Weekly brief", - ); - expect(workflowDisplayName("last-30-days")).toBe("Last 30 Days"); - }); - - describe("isConversationalWorkflowName", () => { - test("marks only the seeded assistant/Myra definition conversational", () => { - expect(isConversationalWorkflowName("assistant")).toBe(true); - }); - - test("marks every workflow utility, automatable or not, non-conversational", () => { - expect(isConversationalWorkflowName("echo")).toBe(false); - expect(isConversationalWorkflowName("workbench-digest")).toBe(false); - expect(isConversationalWorkflowName("last-30-days-research")).toBe(false); - expect(isConversationalWorkflowName("heartbeat")).toBe(false); - expect(isConversationalWorkflowName("process-granola-call")).toBe(false); - }); - - test("treats a name absent from the catalog as conversational — a runtime agent-directory definition", () => { - expect(isConversationalWorkflowName("my-researcher")).toBe(true); - expect(isConversationalWorkflowName("wfd_deadbeef")).toBe(true); - }); - - // CL-6649: `echo` and `last-30-days-research` are both non-automatable - // AND non-conversational — the exact combination that let a picker - // gated on `!isAutomatableWorkflowName` alone (rather than - // `isConversationalWorkflowName`) mistake a routine's delivery - // workflow, and the Echo wiring check, for an invitable chat agent. - test("a non-automatable utility is still non-conversational (the CL-6649 trap)", () => { - for (const name of ["echo", "last-30-days-research"]) { - expect(isAutomatableWorkflowName(name)).toBe(false); - expect(isConversationalWorkflowName(name)).toBe(false); - } - }); - - test("every catalog entry declares a conversational flag", () => { - for (const entry of WORKFLOW_CATALOG) { - expect(typeof entry.conversational).toBe("boolean"); - } - }); - }); - - test("every catalog entry has a non-empty display name", () => { - for (const entry of WORKFLOW_CATALOG) { - expect(entry.displayName.trim().length).toBeGreaterThan(0); - expect(entry.assetName).toMatch(/^[a-z0-9-]+$/); - } - }); - - describe("deliveryMode", () => { - test("every catalog entry declares a delivery mode", () => { - for (const entry of WORKFLOW_CATALOG) { - expect(["workbench", "inbox"]).toContain(entry.deliveryMode); - } - }); - - test("no catalog entry currently delivers to inbox", () => { - const inboxEntries = WORKFLOW_CATALOG.filter( - (entry) => entry.deliveryMode === "inbox", - ); - expect(inboxEntries).toEqual([]); - }); - - test("deliveryWorkbenchRequiredForWorkflowName is true for every known catalog entry", () => { - for (const entry of WORKFLOW_CATALOG) { - expect(deliveryWorkbenchRequiredForWorkflowName(entry.assetName)).toBe( - true, - ); - } - }); - - test("an unknown workflow name defaults to workbench-required", () => { - expect(deliveryWorkbenchRequiredForWorkflowName("unknown-workflow")).toBe( - true, - ); - }); - }); - - test("every catalog entry carries honest demo-card copy", () => { - for (const entry of WORKFLOW_CATALOG) { - expect(entry.whatItDoes.trim().length).toBeGreaterThan(0); - expect(entry.exampleOutput.trim().length).toBeGreaterThan(0); - expect(entry.typicalDuration.trim().length).toBeGreaterThan(0); - // No fake precision or invented metrics dressed up as facts. - expect(entry.whatItDoes).not.toMatch(/%|\$\d/); - } - }); - - test("every exampleOutput is a single capitalized readout fragment with no trailing period", () => { - for (const entry of WORKFLOW_CATALOG) { - expect(entry.exampleOutput).not.toContain("\n"); - expect(entry.exampleOutput.endsWith(".")).toBe(false); - expect(entry.exampleOutput[0]).toBe( - entry.exampleOutput[0]?.toUpperCase(), - ); - } - }); - - test("every requiredConnections id names a connector a person can actually connect", () => { - // Either a native connector (its own credential plugin and tool - // package) or an MCP preset a person connects under Plugins — Attio - // is only ever the latter here, reached through `@corbits/mcp-tools`. - const connectable = new Set([ - ...Object.keys(CONNECTOR_REGISTRY), - ...MCP_PRESETS.map((preset) => preset.slug), - ]); - for (const entry of WORKFLOW_CATALOG) { - for (const connectorId of entry.requiredConnections) { - expect(connectable.has(connectorId)).toBe(true); - } - } - }); - - test("pins the GTM ports to the connectors their tool packages actually need", () => { - const byAssetName = new Map( - WORKFLOW_CATALOG.map((entry) => [entry.assetName, entry]), - ); - expect(byAssetName.get("granola-call")?.requiredConnections).toEqual([ - "granola", - ]); - expect( - byAssetName.get("process-granola-call")?.requiredConnections, - ).toEqual(["granola"]); - expect(byAssetName.get("morning-brief")?.requiredConnections).toEqual([ - "granola", - "linear", - ]); - expect( - byAssetName.get("pain-point-collateral")?.requiredConnections, - ).toEqual(["granola"]); - expect( - byAssetName.get("collateral-generation")?.requiredConnections, - ).toEqual(["granola", "linear"]); - expect( - byAssetName.get("reddit-opportunity-scanner")?.requiredConnections, - ).toEqual(["scrapecreators"]); - expect( - byAssetName.get("last-30-days-research")?.requiredConnections, - ).toEqual(["exa"]); - }); - - test("workflows with no external connector requirement declare an empty list", () => { - const byAssetName = new Map( - WORKFLOW_CATALOG.map((entry) => [entry.assetName, entry]), - ); - expect(byAssetName.get("echo")?.requiredConnections).toEqual([]); - expect(byAssetName.get("assistant")?.requiredConnections).toEqual([]); - expect(byAssetName.get("heartbeat")?.requiredConnections).toEqual([]); - expect(byAssetName.get("workbench-digest")?.requiredConnections).toEqual( - [], - ); - }); - - describe("triggerFields", () => { - test("every declared triggerFields entry matches the WorkflowTriggerField shape", () => { - for (const entry of WORKFLOW_CATALOG) { - if (entry.triggerFields === undefined) continue; - const parsed = WorkflowTriggerField.array()(entry.triggerFields); - expect(parsed instanceof type.errors).toBe(false); - } - }); - - test("every triggerFields key is unique within its entry and non-blank labeled", () => { - for (const entry of WORKFLOW_CATALOG) { - if (entry.triggerFields === undefined) continue; - const keys = entry.triggerFields.map((field) => field.key); - expect(new Set(keys).size).toBe(keys.length); - for (const field of entry.triggerFields) { - expect(field.label.trim().length).toBeGreaterThan(0); - } - } - }); - - test("last-30-days-research declares a required topic and an optional focus", () => { - const entry = workflowCatalogEntry("last-30-days-research"); - expect(entry?.triggerFields).toEqual([ - { - key: "topic", - kind: "text", - label: "Topic", - placeholder: "AI coding agents", - required: true, - help: "What to research over the last 30 days.", - }, - { - key: "focus", - kind: "text", - label: "Focus", - placeholder: "Competing launches", - required: false, - help: "Optional — narrows which angle of the topic to chase.", - }, - ]); - }); - - test("pain-point-collateral declares its either/or transcript and noteId as optional", () => { - // Neither field is required on its own — the workflow's intake tool - // accepts either one (or neither, and teaches what to send next); see - // workflows/pain-point-collateral/src/intake-tool.ts's IntakeArgs. - const entry = workflowCatalogEntry("pain-point-collateral"); - expect(entry?.triggerFields?.map((f) => f.key)).toEqual([ - "transcript", - "noteId", - ]); - for (const field of entry?.triggerFields ?? []) { - expect(field.required).toBe(false); - } - }); - - test("workflows with no named trigger inputs declare no triggerFields", () => { - // Heartbeat and workbench-digest take no human-supplied content at - // create time — heartbeat ignores its trigger entirely, and - // workbench-digest's content is computed server-side by the scheduler, - // not typed in by a person. - expect(workflowCatalogEntry("heartbeat")?.triggerFields).toBeUndefined(); - expect( - workflowCatalogEntry("workbench-digest")?.triggerFields, - ).toBeUndefined(); - }); - }); - - // Two required fields, one "agent"-kind and one "text"-kind — an - // explicit local fixture, not pulled from any catalog entry, so this - // block's assertions about required/blank/non-string handling stay - // meaningful regardless of which entries the catalog happens to carry. - const AGENT_AND_PROMPT_FIELDS: readonly WorkflowTriggerField[] = [ - { key: "agent", kind: "agent", label: "Agent", required: true }, - { key: "prompt", kind: "text", label: "Prompt", required: true }, - ]; - - // CL-6358: inputs bind at USE, never at creation — a routine (or a - // seed preset) must be creatable with a required trigger field left - // entirely unbound. `validateTriggerFieldsAtCreate` is the boundary - // check `@corbits/routines`' create route actually applies now: - // absence of a required field is never rejected, only a value the - // caller explicitly provided but left malformed is. - describe("validateTriggerFieldsAtCreate", () => { - const fields = AGENT_AND_PROMPT_FIELDS; - - test("a required field left entirely unbound passes at create time", () => { - expect( - validateTriggerFieldsAtCreate(fields, { prompt: "Do it" }), - ).toEqual({ ok: true }); - }); - - test("last-30-days-research seeds with Topic unbound", () => { - const researchFields = workflowCatalogEntry("last-30-days-research") - ?.triggerFields as readonly WorkflowTriggerField[]; - expect(validateTriggerFieldsAtCreate(researchFields, {})).toEqual({ - ok: true, - }); - }); - - test("a provided-but-blank required field still fails: a caller who sets it must set it honestly", () => { - const result = validateTriggerFieldsAtCreate(fields, { - agent: " ", - prompt: "Do it", - }); - expect(result.ok).toBe(false); - }); - - test("a provided non-string value for a required field still fails", () => { - const result = validateTriggerFieldsAtCreate(fields, { - agent: 12345, - prompt: "Do it", - }); - expect(result.ok).toBe(false); - }); - - test("a fully valid input still passes", () => { - expect( - validateTriggerFieldsAtCreate(fields, { - agent: "wfd_1", - prompt: "Do it", - }), - ).toEqual({ ok: true }); - }); - }); -}); diff --git a/packages/workflow-catalog/test/connect-github-setup.race.test.ts b/packages/workflow-catalog/test/connect-github-setup.race.test.ts deleted file mode 100644 index 2e288d577..000000000 --- a/packages/workflow-catalog/test/connect-github-setup.race.test.ts +++ /dev/null @@ -1,219 +0,0 @@ -// CL-7242: reconstructs the audit's own reproduction -- two concurrent -// `startReviewingRepos` calls for the same repo -- against real, -// database-backed ports rather than plain fakes. `hasRepoGrant`/ -// `mintRepoGrant` here fake a plain read-then-insert against the -// `grant` table directly, standing in for `apps/hub/src/index.ts`'s -// real binding (HTTP calls through `native-repo-grants.ts`, see -// CL-7242 follow-up "Mint workbench tenants and repo grants via -// Interchange HTTP") without a live hub-api server -- what this test -// actually proves is that `startReviewingRepos`' lease serializes any -// such hasRepoGrant/mintRepoGrant pair correctly, which is exactly -// what makes the real HTTP-bound versions safe too. The lease store's -// own test (`@corbits/webhook-triggers`'s -// `repo-review-lease.drizzle.test.ts`) proves the underlying -// compare-and-swap works; this proves `startReviewingRepos` itself -// settles on exactly one grant and one trigger, racing the two calls -// a double-click or a client retry would produce. DB-gated: skipped -// when DATABASE_URL is unset. Runs against its own scratch database -// (mirroring `@corbits/webhook-triggers`' own `store.drizzle.test.ts`), -// never the developer's or the walking-skeleton suite's. -import { afterAll, beforeAll, expect, test } from "bun:test"; -import { drizzle } from "drizzle-orm/postgres-js"; -import { and, eq } from "drizzle-orm"; -import postgres from "postgres"; - -import { runMigrations } from "@intx/db"; -import * as intxSchema from "@intx/db/schema"; -import { grant as grantTable, role as roleTable } from "@intx/db/schema"; -import { createNoopCredentialCipher } from "@intx/crypto"; -import { - createDrizzleRepoReviewLeaseStore, - createDrizzleWebhookTriggerStore, - applyWebhookTriggersMigrations, -} from "@corbits/webhook-triggers"; -import type { GitHubRepoSummary } from "@corbits/github-tools"; - -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; -import { dbGate } from "../../../scripts/e2e/db-gate"; -import { - startReviewingRepos, - webhookTriggerName, - type ConnectGithubSetupPorts, -} from "../src/connect-github-setup"; - -function scratchUrlFor(e2eUrl: string): string { - const url = new URL(e2eUrl); - const database = url.pathname.replace(/^\//, ""); - url.pathname = `/${database}_connect_github_race_test`; - return url.toString(); -} - -const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = dbGate(databaseUrl, import.meta.path); - -const REPO: GitHubRepoSummary = { - id: "1", - name: "acme/widgets", -}; -const TENANT_ID = "tnt_race"; -const DEFINITION_ID = "def_code_review"; - -describeIfDb("startReviewingRepos under real concurrency (CL-7242)", () => { - const scratchUrl = scratchUrlFor( - databaseUrl ?? "postgres://localhost:5432/unused", - ); - const scratchTarget = new URL(scratchUrl); - const scratchDatabase = scratchTarget.pathname.replace(/^\//, ""); - - beforeAll(async () => { - const maintenanceUrl = new URL(scratchUrl); - maintenanceUrl.pathname = "/postgres"; - const maintenance = postgres(maintenanceUrl.toString(), { - max: 1, - onnotice: () => undefined, - }); - try { - await maintenance.unsafe(`DROP DATABASE IF EXISTS "${scratchDatabase}"`); - await maintenance.unsafe(`CREATE DATABASE "${scratchDatabase}"`); - } finally { - await maintenance.end(); - } - const parsed = new URL(scratchUrl); - await runMigrations( - { - host: parsed.hostname, - port: Number(parsed.port || 5432), - user: decodeURIComponent(parsed.username), - password: decodeURIComponent(parsed.password), - database: scratchDatabase, - }, - { schema: "public" }, - ); - await applyWebhookTriggersMigrations(scratchUrl); - }); - - afterAll(async () => { - const maintenanceUrl = new URL(scratchUrl); - maintenanceUrl.pathname = "/postgres"; - const maintenance = postgres(maintenanceUrl.toString(), { - max: 1, - onnotice: () => undefined, - }); - try { - await maintenance.unsafe(`DROP DATABASE IF EXISTS "${scratchDatabase}"`); - } finally { - await maintenance.end(); - } - }); - - test("two concurrent calls for the same repo mint exactly one grant and one trigger", async () => { - const client = postgres(scratchUrl, { - max: 10, - onnotice: () => undefined, - }); - try { - const db = drizzle(client, { schema: intxSchema }); - const webhookStore = createDrizzleWebhookTriggerStore( - db, - createNoopCredentialCipher(), - ); - const leaseStore = createDrizzleRepoReviewLeaseStore(db); - - const roleId = "role_race_member"; - await client`INSERT INTO "tenant" (id, name, slug, domain) VALUES (${TENANT_ID}, 'Acme', 'acme-race', 'acme-race.example')`; - await client`INSERT INTO "role" (id, tenant_id, name) VALUES (${roleId}, ${TENANT_ID}, 'member')`; - - // Mirrors apps/hub/src/index.ts's real wiring exactly: the lease - // is acquired first, and mintRepoGrant is a bare insert against - // Interchange's own `grant` table -- no onConflict, no index of - // ours on their table. Only the lease serializes the two racing - // calls below. - const buildPorts = (): ConnectGithubSetupPorts => ({ - acquireRepoReviewLease: (repo) => - leaseStore.acquire(TENANT_ID, repo.name), - releaseRepoReviewLease: (repo) => - leaseStore.release(TENANT_ID, repo.name), - hasRepoGrant: async (repo) => { - const existing = await db.query.grant.findFirst({ - where: and( - eq(grantTable.tenantId, TENANT_ID), - eq(grantTable.resource, `repo:${repo.name}`), - eq(grantTable.action, "read"), - ), - columns: { id: true }, - }); - return existing !== undefined; - }, - mintRepoGrant: async (repo) => { - const memberRole = await db.query.role.findFirst({ - where: and( - eq(roleTable.tenantId, TENANT_ID), - eq(roleTable.name, "member"), - ), - columns: { id: true }, - }); - if (memberRole === undefined) throw new Error("no member role"); - await db.insert(grantTable).values({ - id: `grant_${crypto.randomUUID()}`, - tenantId: TENANT_ID, - roleId: memberRole.id, - resource: `repo:${repo.name}`, - action: "read", - effect: "allow", - origin: "system", - }); - }, - hasWebhookTrigger: async (repo) => { - const triggers = await webhookStore.list(TENANT_ID); - const name = webhookTriggerName(repo); - return triggers.some( - (t) => t.workflowDefinitionId === DEFINITION_ID && t.name === name, - ); - }, - createWebhookTrigger: async (repo) => { - const row = await webhookStore.ensure({ - id: `wht_${crypto.randomUUID()}`, - tenantId: TENANT_ID, - name: webhookTriggerName(repo), - workflowDefinitionId: DEFINITION_ID, - inputTemplate: - "Review the pull request at {{pull_request.html_url}}", - secret: crypto.randomUUID(), - createdBy: "user_1", - }); - return { id: row.id }; - }, - persistSelectedRepos: async () => {}, - }); - - const [first, second] = await Promise.all([ - startReviewingRepos(["1"], [REPO], buildPorts()), - startReviewingRepos(["1"], [REPO], buildPorts()), - ]); - - // Whether the second call gets skipped by the lease or simply - // finds the work already done (CL-7134's fast path) depends on - // real, non-deterministic timing between the two real Postgres - // round trips -- both are correct outcomes. The property this - // test actually cares about is that at most one trigger is ever - // created, and the database never ends up with a duplicate. - const totalCreated = - first.createdTriggerIds.length + second.createdTriggerIds.length; - expect(totalCreated).toBe(1); - - const grantRows = await client` - SELECT id FROM "grant" - WHERE tenant_id = ${TENANT_ID} AND resource = 'repo:acme/widgets' - `; - expect(grantRows).toHaveLength(1); - - const triggerRows = await client` - SELECT id FROM "webhook_triggers"."webhook_trigger" - WHERE tenant_id = ${TENANT_ID} AND workflow_definition_id = ${DEFINITION_ID} - `; - expect(triggerRows).toHaveLength(1); - } finally { - await client.end(); - } - }); -}); diff --git a/packages/workflow-catalog/test/instantiate.test.ts b/packages/workflow-catalog/test/instantiate.test.ts deleted file mode 100644 index cc11ccac5..000000000 --- a/packages/workflow-catalog/test/instantiate.test.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { expect, test } from "bun:test"; -import { CODE_REVIEW_REVIEWERS } from "@corbits/code-review/reviewers"; - -import { - CODE_REVIEW_TEMPLATE, - DUE_DILIGENCE_TEMPLATE, - GTM_TEMPLATE, - type WorkbenchOnboardingStep, -} from "../src/index"; -import { - instantiateWorkbenchTemplate, - type WorkbenchTemplateInstantiationPorts, -} from "../src/instantiate"; - -function fakePorts( - existingHandles: readonly string[] = [], - alreadyDeployedBlocks: readonly string[] = [], -): WorkbenchTemplateInstantiationPorts & { - readonly created: string[]; - readonly invited: string[]; - readonly recordedConnections: (readonly string[])[]; - readonly deployedBlocks: string[]; - readonly calls: string[]; - readonly onboardingSteps: (readonly WorkbenchOnboardingStep[])[]; -} { - const created: string[] = []; - const invited: string[] = []; - const recordedConnections: (readonly string[])[] = []; - const deployedBlocks: string[] = []; - const calls: string[] = []; - const onboardingSteps: (readonly WorkbenchOnboardingStep[])[] = []; - return { - created, - invited, - recordedConnections, - deployedBlocks, - calls, - onboardingSteps, - async listAgentHandles() { - return existingHandles.map((handle) => ({ handle, id: `def-${handle}` })); - }, - async createParticipantAgent(request) { - created.push(request.handle); - return { id: `def-${request.handle}` }; - }, - async deployBlockWorkflow(block) { - deployedBlocks.push(block.assetName); - return { created: !alreadyDeployedBlocks.includes(block.assetName) }; - }, - async inviteParticipantAgent(id) { - invited.push(id); - calls.push(`invite:${id}`); - }, - async recordPendingConnections(pendingConnections) { - recordedConnections.push(pendingConnections); - calls.push("recordPendingConnections"); - }, - async beginOnboarding(steps) { - onboardingSteps.push(steps); - calls.push("beginOnboarding"); - }, - }; -} - -test("instantiating the code-review definition creates the three reviewer definitions, never Myra", () => { - const ports = fakePorts(); - return instantiateWorkbenchTemplate(CODE_REVIEW_TEMPLATE, ports).then( - (result) => { - expect(result.createdHandles).toEqual( - CODE_REVIEW_REVIEWERS.map((reviewer) => reviewer.handle), - ); - expect(ports.created).toEqual( - CODE_REVIEW_REVIEWERS.map((reviewer) => reviewer.handle), - ); - expect(ports.created).not.toContain("myra"); - expect(result.skippedHandles).toEqual([]); - }, - ); -}); - -test("instantiating the code-review template records its required connections as pending", async () => { - const ports = fakePorts(); - const result = await instantiateWorkbenchTemplate( - CODE_REVIEW_TEMPLATE, - ports, - ); - expect(result.pendingConnections).toEqual(["github"]); - expect(ports.recordedConnections).toEqual([["github"]]); -}); - -test("instantiating the code-review template skips a reviewer that already exists", async () => { - const ports = fakePorts(["architecture-reviewer"]); - const result = await instantiateWorkbenchTemplate( - CODE_REVIEW_TEMPLATE, - ports, - ); - expect(result.skippedHandles).toEqual(["architecture-reviewer"]); - expect(result.createdHandles).toEqual([ - "correctness-reviewer", - "release-risk-reviewer", - ]); - expect(ports.created).not.toContain("architecture-reviewer"); -}); - -// A reviewer whose agent-directory definition already exists (a second -// workbench from the same template) still has to become a participant -// of THIS new room — an existing definition is not an existing -// invitation, so skipping the create must never skip the invite too. -test("instantiating the code-review definition invites every reviewer into the room, created or skipped alike", async () => { - const ports = fakePorts(["architecture-reviewer"]); - const result = await instantiateWorkbenchTemplate( - CODE_REVIEW_TEMPLATE, - ports, - ); - expect(result.invitedHandles).toEqual( - CODE_REVIEW_REVIEWERS.map((reviewer) => reviewer.handle), - ); - expect(ports.invited).toEqual( - expect.arrayContaining([ - "def-architecture-reviewer", - "def-correctness-reviewer", - "def-release-risk-reviewer", - ]), - ); - expect(ports.invited).toHaveLength(3); -}); - -test("the code-review definition never invites or creates Myra — nobody hosts a template room", async () => { - const ports = fakePorts(["assistant"]); - const result = await instantiateWorkbenchTemplate( - CODE_REVIEW_TEMPLATE, - ports, - ); - expect(ports.invited).not.toContain("def-assistant"); - expect(ports.created).not.toContain("myra"); - expect(ports.created).not.toContain("assistant"); - expect(result.invitedHandles).not.toContain("myra"); -}); - -test("the due-diligence definition still invites an existing assistant as Myra", async () => { - const ports = fakePorts(["assistant"]); - const result = await instantiateWorkbenchTemplate( - DUE_DILIGENCE_TEMPLATE, - ports, - ); - expect(ports.invited).toContain("def-assistant"); - expect(ports.created).not.toContain("myra"); - expect(result.invitedHandles).toEqual(["myra", "scout"]); -}); - -test("instantiating the code-review definition begins its walkthrough, in definition order, once everything else is in place", async () => { - const ports = fakePorts(); - const result = await instantiateWorkbenchTemplate( - CODE_REVIEW_TEMPLATE, - ports, - ); - expect(ports.onboardingSteps).toEqual([CODE_REVIEW_TEMPLATE.onboardingSteps]); - expect(result.onboardingSteps).toEqual(CODE_REVIEW_TEMPLATE.onboardingSteps); - expect(ports.calls.at(-1)).toBe("beginOnboarding"); - expect(ports.calls.at(-2)).toBe("recordPendingConnections"); - expect(ports.calls.filter((call) => call.startsWith("invite:"))).toHaveLength( - 3, - ); -}); - -test("a definition with no onboarding steps never begins a walkthrough", async () => { - const ports = fakePorts(); - const result = await instantiateWorkbenchTemplate( - DUE_DILIGENCE_TEMPLATE, - ports, - ); - expect(ports.onboardingSteps).toEqual([]); - expect(ports.calls).not.toContain("beginOnboarding"); - expect(result.onboardingSteps).toEqual([]); -}); - -test("instantiating the code-review template deploys its referenced code-review block workflow", async () => { - const ports = fakePorts(); - const result = await instantiateWorkbenchTemplate( - CODE_REVIEW_TEMPLATE, - ports, - ); - expect(ports.deployedBlocks).toEqual(["code-review"]); - expect(result.deployedBlockAssetNames).toEqual(["code-review"]); - expect(result.skippedBlockAssetNames).toEqual([]); -}); - -test("a block workflow the tenant already deployed is reported as skipped, never re-deployed", async () => { - const ports = fakePorts([], ["code-review"]); - const result = await instantiateWorkbenchTemplate( - CODE_REVIEW_TEMPLATE, - ports, - ); - expect(result.deployedBlockAssetNames).toEqual([]); - expect(result.skippedBlockAssetNames).toEqual(["code-review"]); -}); - -test("instantiating a definition with an agent outside the reviewer roster throws rather than silently skipping it", () => { - return expect( - instantiateWorkbenchTemplate(GTM_TEMPLATE, fakePorts()), - ).rejects.toThrow(/has no known create-agent request/); -}); - -test("instantiating the due-diligence template creates Scout, never Myra, with no credential connected", async () => { - const ports = fakePorts(); - const result = await instantiateWorkbenchTemplate( - DUE_DILIGENCE_TEMPLATE, - ports, - ); - expect(result.createdHandles).toEqual(["scout"]); - expect(ports.created).toEqual(["scout"]); - expect(ports.created).not.toContain("myra"); - expect(result.skippedHandles).toEqual([]); - // No block to deploy and nothing required up front: seeding never - // fails for want of a connected credential (Exa's MCP preset is - // keyless, and this template requires nothing at all). - expect(ports.deployedBlocks).toEqual([]); - expect(result.pendingConnections).toEqual([]); -}); - -test("instantiating the due-diligence template twice never creates Scout a second time", async () => { - const first = fakePorts(); - await instantiateWorkbenchTemplate(DUE_DILIGENCE_TEMPLATE, first); - const second = fakePorts(["scout"]); - const result = await instantiateWorkbenchTemplate( - DUE_DILIGENCE_TEMPLATE, - second, - ); - expect(result.createdHandles).toEqual([]); - expect(result.skippedHandles).toEqual(["scout"]); - expect(second.created).toEqual([]); -}); - -test("Scout's create request carries its tool package pins", async () => { - const requests: { handle: string; toolPackagePins?: readonly string[] }[] = - []; - const ports: WorkbenchTemplateInstantiationPorts = { - async listAgentHandles() { - return []; - }, - async createParticipantAgent(request) { - requests.push(request); - return { id: `def-${request.handle}` }; - }, - async deployBlockWorkflow() { - return { created: true }; - }, - async inviteParticipantAgent() { - /* noop */ - }, - async recordPendingConnections() { - /* noop */ - }, - async beginOnboarding() { - /* noop */ - }, - }; - await instantiateWorkbenchTemplate(DUE_DILIGENCE_TEMPLATE, ports); - const scout = requests.find((request) => request.handle === "scout"); - expect(scout?.toolPackagePins).toEqual( - expect.arrayContaining([ - "@corbits/memory-tools", - "@corbits/web-search-tools", - "@corbits/scout-agent", - ]), - ); -}); - -// No shipped definition names Jimmy (he is not a "kind of workbench"); -// `@corbits/chat-ui`'s "Add Jimmy" quick-create row calls -// `jimmyAgentRequest()` directly instead of going through one. This -// proves `instantiateWorkbenchTemplate`'s request map still resolves his -// handle, so a future template naming him works with no new plumbing. -test("a definition naming Jimmy's handle still resolves and creates him", async () => { - const definitionNamingJimmy = { - ...DUE_DILIGENCE_TEMPLATE, - agents: [ - { handle: "jimmy", displayName: "Jimmy", role: "Replies with a GIF." }, - ], - }; - const ports = fakePorts(); - const result = await instantiateWorkbenchTemplate( - definitionNamingJimmy, - ports, - ); - expect(result.createdHandles).toEqual(["jimmy"]); - expect(ports.created).toEqual(["jimmy"]); -}); diff --git a/packages/workflow-catalog/test/reconcile-duplicate-repo-grants.test.ts b/packages/workflow-catalog/test/reconcile-duplicate-repo-grants.test.ts deleted file mode 100644 index e4cffd809..000000000 --- a/packages/workflow-catalog/test/reconcile-duplicate-repo-grants.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -// DB-gated: skipped when no DATABASE_URL is reachable. Runs against -// its own scratch database. -// -// CL-7242: `reconcileDuplicateRepoGrants` is the one-time cleanup for -// duplicate `repo:` grants a database can already carry -// from before the `repo_review_lease` fix started preventing new -// ones. Proves it keeps exactly one grant per group (never zero, -// matching the "a repo never ends up with fewer grants than it had" -// bar), never touches an unrelated grant, and is safe to re-run. -import { afterAll, beforeAll, expect, test } from "bun:test"; -import postgres from "postgres"; - -import { runMigrations } from "@intx/db"; - -import { e2eDatabaseUrl } from "../../../scripts/e2e/harness"; -import { reconcileDuplicateRepoGrants } from "../src/reconcile-duplicate-repo-grants"; -import { dbGate } from "../../../scripts/e2e/db-gate"; - -function scratchUrlFor(e2eUrl: string): string { - const url = new URL(e2eUrl); - const database = url.pathname.replace(/^\//, ""); - url.pathname = `/${database}_reconcile_duplicate_repo_grants_test`; - return url.toString(); -} - -const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = dbGate(databaseUrl, import.meta.path); - -const TENANT_ID = "tnt_reconcile"; -const ROLE_ID = "role_reconcile_member"; - -describeIfDb("reconcileDuplicateRepoGrants", () => { - const scratchUrl = scratchUrlFor( - databaseUrl ?? "postgres://localhost:5432/unused", - ); - const scratchTarget = new URL(scratchUrl); - const scratchDatabase = scratchTarget.pathname.replace(/^\//, ""); - - beforeAll(async () => { - const maintenanceUrl = new URL(scratchUrl); - maintenanceUrl.pathname = "/postgres"; - const maintenance = postgres(maintenanceUrl.toString(), { - max: 1, - onnotice: () => undefined, - }); - try { - await maintenance.unsafe(`DROP DATABASE IF EXISTS "${scratchDatabase}"`); - await maintenance.unsafe(`CREATE DATABASE "${scratchDatabase}"`); - } finally { - await maintenance.end(); - } - const parsed = new URL(scratchUrl); - await runMigrations( - { - host: parsed.hostname, - port: Number(parsed.port || 5432), - user: decodeURIComponent(parsed.username), - password: decodeURIComponent(parsed.password), - database: scratchDatabase, - }, - { schema: "public" }, - ); - const sql = postgres(scratchUrl, { max: 1, onnotice: () => undefined }); - try { - await sql`INSERT INTO "tenant" (id, name, slug, domain) VALUES (${TENANT_ID}, 'Acme', 'acme-reconcile', 'acme-reconcile.example')`; - await sql`INSERT INTO "role" (id, tenant_id, name) VALUES (${ROLE_ID}, ${TENANT_ID}, 'member')`; - } finally { - await sql.end(); - } - }); - - afterAll(async () => { - const maintenanceUrl = new URL(scratchUrl); - maintenanceUrl.pathname = "/postgres"; - const maintenance = postgres(maintenanceUrl.toString(), { - max: 1, - onnotice: () => undefined, - }); - try { - await maintenance.unsafe(`DROP DATABASE IF EXISTS "${scratchDatabase}"`); - } finally { - await maintenance.end(); - } - }); - - test("keeps exactly one grant per duplicated (tenant, resource, action) and leaves other grants alone", async () => { - const sql = postgres(scratchUrl, { max: 1, onnotice: () => undefined }); - try { - // Two duplicate repo grants from the pre-lease race, one pair from - // each generation of `mintRepoGrant`: the direct-insert path this - // PR replaced wrote `'system'`-origin rows; the current - // `mintRepoGrantViaHttp` path writes `'creator'`-origin rows. - await sql` - INSERT INTO "grant" (id, tenant_id, role_id, resource, action, effect, origin, created_at) - VALUES - ('grant_dup_1', ${TENANT_ID}, ${ROLE_ID}, 'repo:acme/widgets', 'read', 'allow', 'system', now() - interval '1 hour'), - ('grant_dup_2', ${TENANT_ID}, ${ROLE_ID}, 'repo:acme/widgets', 'read', 'allow', 'creator', now()) - `; - // A non-duplicated repo grant, a non-repo system grant, and a - // repo grant with an origin this feature never writes -- none - // should ever be touched. - await sql` - INSERT INTO "grant" (id, tenant_id, role_id, resource, action, effect, origin) - VALUES - ('grant_solo', ${TENANT_ID}, ${ROLE_ID}, 'repo:acme/gadgets', 'read', 'allow', 'creator'), - ('grant_unrelated', ${TENANT_ID}, ${ROLE_ID}, 'workbench:room-1', 'read', 'allow', 'system'), - ('grant_other_origin', ${TENANT_ID}, ${ROLE_ID}, 'repo:acme/gizmos', 'read', 'allow', 'role') - `; - - const first = await reconcileDuplicateRepoGrants(scratchUrl); - expect(first.removedIds).toEqual(["grant_dup_2"]); - - const widgetsRows = await sql` - SELECT id FROM "grant" WHERE tenant_id = ${TENANT_ID} AND resource = 'repo:acme/widgets' - `; - expect(widgetsRows).toHaveLength(1); - expect(widgetsRows[0]?.["id"]).toBe("grant_dup_1"); - - const untouchedRows = await sql` - SELECT id FROM "grant" WHERE id IN ('grant_solo', 'grant_unrelated', 'grant_other_origin') - ORDER BY id - `; - expect(untouchedRows.map((r) => r["id"])).toEqual([ - "grant_other_origin", - "grant_solo", - "grant_unrelated", - ]); - - // Idempotent: nothing left to remove on a second run. - const second = await reconcileDuplicateRepoGrants(scratchUrl); - expect(second.removedIds).toEqual([]); - } finally { - await sql.end(); - } - }); -}); diff --git a/packages/workflow-catalog/test/settings.test.ts b/packages/workflow-catalog/test/settings.test.ts deleted file mode 100644 index b9ea0b0cd..000000000 --- a/packages/workflow-catalog/test/settings.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { expect, test } from "bun:test"; - -import { templateSettingsPatch } from "../src/settings"; - -test("builds a valid template/* settings patch", () => { - expect(templateSettingsPatch("code-review", ["github"])).toEqual({ - "template/id": "code-review", - "template/pendingConnections": ["github"], - }); -}); - -test("rejects an empty template id", () => { - expect(() => templateSettingsPatch("", ["github"])).toThrow( - /invalid template settings patch/, - ); -}); diff --git a/packages/workflow-catalog/test/templates.test.ts b/packages/workflow-catalog/test/templates.test.ts deleted file mode 100644 index 21a941a32..000000000 --- a/packages/workflow-catalog/test/templates.test.ts +++ /dev/null @@ -1,306 +0,0 @@ -import { expect, test } from "bun:test"; -import { CONNECTOR_REGISTRY } from "@workbench/connections/registry"; -import { MCP_PRESETS } from "@workbench/connections/mcp-presets"; -import { SCOUT_TOOL_PACKAGE_PINS } from "@corbits/scout-agent/definition"; - -import { - CODE_REVIEW_TEMPLATE, - DUE_DILIGENCE_TEMPLATE, - GTM_TEMPLATE, - WORKBENCH_TEMPLATES, - WORKFLOW_CATALOG, - templateBlockAssetNames, - workbenchTemplate, - parseWorkbenchDefinition, - serializeWorkbenchDefinition, - workflowCatalogEntry, -} from "../src/index"; - -test("every template's blocks are real catalog workflows", () => { - const known = new Set(WORKFLOW_CATALOG.map((entry) => entry.assetName)); - for (const template of WORKBENCH_TEMPLATES) { - for (const assetName of templateBlockAssetNames(template)) { - expect(known).toContain(assetName); - } - } -}); - -test("every connector a template names is a real connector or MCP preset", () => { - const known = new Set([ - ...Object.keys(CONNECTOR_REGISTRY), - ...MCP_PRESETS.map((preset) => preset.slug), - ]); - for (const template of WORKBENCH_TEMPLATES) { - for (const connector of [ - ...template.plugins.required, - ...template.plugins.optional, - ]) { - expect(known).toContain(connector); - } - } -}); - -test("a template never both requires and merely offers the same connector", () => { - for (const template of WORKBENCH_TEMPLATES) { - const required = new Set(template.plugins.required); - for (const optional of template.plugins.optional) { - expect(required).not.toContain(optional); - } - } -}); - -test("only automatable blocks get put on a schedule", () => { - for (const template of WORKBENCH_TEMPLATES) { - for (const routine of template.routines) { - expect(workflowCatalogEntry(routine.blockAssetName)?.automatable).toBe( - true, - ); - } - } -}); - -test("the GTM template installs the four ported v1 workflows plus the call write-up backbone", () => { - expect(workbenchTemplate("gtm")).toBe(GTM_TEMPLATE); - expect(templateBlockAssetNames(GTM_TEMPLATE)).toEqual([ - "granola-call", - "process-granola-call", - "attio-task-agent", - "exa-topic-watch", - "pain-point-collateral", - ]); -}); - -test("the GTM template blocks the create on the two connectors nothing works without", () => { - expect(GTM_TEMPLATE.plugins.required).toEqual(["attio", "exa"]); - expect(GTM_TEMPLATE.plugins.optional).toEqual(["granola"]); -}); - -test("the GTM template schedules call discovery and the web watch, and nothing else", () => { - expect(GTM_TEMPLATE.routines.map((routine) => routine.key)).toEqual([ - "call-discovery", - "topic-watch", - ]); - expect( - GTM_TEMPLATE.routines.map((routine) => routine.blockAssetName), - ).toEqual(["granola-call", "exa-topic-watch"]); -}); - -test("the watch's one open input reaches the routine that needs it", () => { - expect(GTM_TEMPLATE.openInputs).toHaveLength(1); - const [topic] = GTM_TEMPLATE.openInputs; - expect(topic?.key).toBe("topic"); - expect(topic?.required).toBe(true); - expect(topic?.appliesToRoutine).toBe("topic-watch"); -}); - -test("every routine-scoped open input names a field the block's own trigger contract carries", () => { - for (const template of WORKBENCH_TEMPLATES) { - for (const input of template.openInputs) { - if (input.appliesToRoutine === undefined) continue; - const routine = template.routines.find( - (candidate) => candidate.key === input.appliesToRoutine, - ); - const fields = - workflowCatalogEntry(routine?.blockAssetName ?? "")?.triggerFields ?? - []; - expect(fields.map((field) => field.key)).toContain(input.key); - } - } -}); - -test("every webhook-trigger-scoped open input names a real webhook trigger", () => { - for (const template of WORKBENCH_TEMPLATES) { - for (const input of template.openInputs) { - if (input.appliesToWebhookTrigger === undefined) continue; - const trigger = template.webhookTriggers.find( - (candidate) => candidate.key === input.appliesToWebhookTrigger, - ); - expect(trigger).toBeDefined(); - } - } -}); - -test("every webhook trigger's field names a field the block's own trigger contract carries", () => { - for (const template of WORKBENCH_TEMPLATES) { - for (const trigger of template.webhookTriggers) { - const fields = - workflowCatalogEntry(trigger.blockAssetName)?.triggerFields ?? []; - expect(fields.map((field) => field.key)).toContain( - trigger.triggerFieldKey, - ); - } - } -}); - -test("the code-review template installs the code-review workflow and requires github", () => { - expect(workbenchTemplate("code-review")).toBe(CODE_REVIEW_TEMPLATE); - expect(templateBlockAssetNames(CODE_REVIEW_TEMPLATE)).toEqual([ - "code-review", - ]); - expect(CODE_REVIEW_TEMPLATE.plugins.required).toEqual(["github"]); -}); - -test("the code-review template fires from a pull-request webhook, not a clock", () => { - expect(CODE_REVIEW_TEMPLATE.routines).toEqual([]); - expect(CODE_REVIEW_TEMPLATE.webhookTriggers).toHaveLength(1); - const [trigger] = CODE_REVIEW_TEMPLATE.webhookTriggers; - expect(trigger?.key).toBe("pull-request-opened"); - expect(trigger?.blockAssetName).toBe("code-review"); - expect(trigger?.triggerFieldKey).toBe("pullRequestUrl"); -}); - -test("the code-review definition's agents are the three reviewers and nobody else", () => { - expect(CODE_REVIEW_TEMPLATE.agents.map((agent) => agent.handle)).toEqual([ - "correctness-reviewer", - "architecture-reviewer", - "release-risk-reviewer", - ]); -}); - -test("the code-review definition names the GitHub tool package its reviewers need", () => { - expect(CODE_REVIEW_TEMPLATE.tools).toEqual(["@corbits/github-tools"]); -}); - -test("the code-review walkthrough is connect GitHub, pick repos, start reviewing — in that order", () => { - expect(CODE_REVIEW_TEMPLATE.onboardingSteps.map((step) => step.kind)).toEqual( - ["connect-plugin", "pick-github-repos", "start-webhook-trigger"], - ); - for (const step of CODE_REVIEW_TEMPLATE.onboardingSteps) { - expect(step.title.length).toBeGreaterThan(0); - expect(step.why.length).toBeGreaterThan(0); - } -}); - -test("agents are addressable by a distinct handle", () => { - for (const template of WORKBENCH_TEMPLATES) { - const handles = template.agents.map((agent) => agent.handle); - expect(new Set(handles).size).toBe(handles.length); - } -}); - -test("a definition never names the same tool package twice", () => { - for (const template of WORKBENCH_TEMPLATES) { - expect(new Set(template.tools).size).toBe(template.tools.length); - } -}); - -test("the due-diligence definition's agents are Myra and Scout, neither backed by a block", () => { - expect(workbenchTemplate("due-diligence")).toBe(DUE_DILIGENCE_TEMPLATE); - expect(DUE_DILIGENCE_TEMPLATE.agents.map((agent) => agent.handle)).toEqual([ - "myra", - "scout", - ]); - expect(templateBlockAssetNames(DUE_DILIGENCE_TEMPLATE)).toEqual([]); - for (const agent of DUE_DILIGENCE_TEMPLATE.agents) { - expect(agent.blockAssetName).toBeUndefined(); - } -}); - -test("the due-diligence definition names Scout's tool packages and needs no walkthrough", () => { - expect(DUE_DILIGENCE_TEMPLATE.tools).toEqual( - SCOUT_TOOL_PACKAGE_PINS.map((pin) => pin.name), - ); - expect(DUE_DILIGENCE_TEMPLATE.onboardingSteps).toEqual([]); -}); - -test("the due-diligence template blocks the create on nothing — Exa is offered, never required", () => { - expect(DUE_DILIGENCE_TEMPLATE.plugins.required).toEqual([]); - expect(DUE_DILIGENCE_TEMPLATE.plugins.optional).toEqual(["exa"]); -}); - -test("Jimmy is not a workbench template — the picker offers no such kind of workbench", () => { - expect(workbenchTemplate("default-teammates")).toBeUndefined(); - for (const template of WORKBENCH_TEMPLATES) { - expect(template.agents.some((agent) => agent.handle === "jimmy")).toBe( - false, - ); - } -}); - -test("every shipped definition survives the seed round trip verbatim", () => { - for (const template of WORKBENCH_TEMPLATES) { - const parsed = parseWorkbenchDefinition( - serializeWorkbenchDefinition(template), - ); - expect(parsed).toEqual(template); - } -}); - -test("a malformed library row fails to parse instead of half-loading", () => { - expect(() => parseWorkbenchDefinition('{"id":"code-review"}')).toThrow( - /failed to parse/, - ); - const firstRoutine = GTM_TEMPLATE.routines[0]; - if (!firstRoutine) throw new Error("GTM_TEMPLATE has no routines"); - const orphanRoutine = { - ...GTM_TEMPLATE, - routines: [{ ...firstRoutine, blockAssetName: "not-installed" }], - }; - expect(() => - parseWorkbenchDefinition(serializeWorkbenchDefinition(orphanRoutine)), - ).toThrow(/does not install/); -}); - -test("a required plugin with no step to connect it fails loud, naming the plugin", () => { - const unaskedPlugin = { - ...DUE_DILIGENCE_TEMPLATE, - plugins: { required: ["exa"], optional: [] }, - }; - expect(() => - parseWorkbenchDefinition(serializeWorkbenchDefinition(unaskedPlugin)), - ).toThrow(/requires plugin "exa" but its onboarding never asks/); -}); - -test("a walkthrough step naming a connector outside the definition's plugins fails loud", () => { - const strayConnector = { - ...DUE_DILIGENCE_TEMPLATE, - onboardingSteps: [ - { - kind: "connect-plugin" as const, - connectorId: "attio", - title: "Connect Attio", - why: "Nothing here reads Attio.", - }, - ], - }; - expect(() => - parseWorkbenchDefinition(serializeWorkbenchDefinition(strayConnector)), - ).toThrow(/connects "attio", which is not one of its plugins/); -}); - -test("a start-webhook-trigger step naming an unknown trigger fails loud", () => { - const unknownTrigger = { - ...CODE_REVIEW_TEMPLATE, - onboardingSteps: [ - ...CODE_REVIEW_TEMPLATE.onboardingSteps.slice(0, 2), - { - kind: "start-webhook-trigger" as const, - webhookTriggerKey: "issue-opened", - title: "Start reviewing", - why: "Nothing fires on issues.", - }, - ], - }; - expect(() => - parseWorkbenchDefinition(serializeWorkbenchDefinition(unknownTrigger)), - ).toThrow(/"issue-opened", which the definition does not create/); -}); - -test("picking repos before GitHub is connected fails loud — the walkthrough is ordered", () => { - const outOfOrder = { - ...CODE_REVIEW_TEMPLATE, - onboardingSteps: [ - { - kind: "pick-github-repos" as const, - title: "Pick your repos", - why: "Only the repositories you choose get reviewed.", - }, - ...CODE_REVIEW_TEMPLATE.onboardingSteps.filter( - (step) => step.kind !== "pick-github-repos", - ), - ], - }; - expect(() => - parseWorkbenchDefinition(serializeWorkbenchDefinition(outOfOrder)), - ).toThrow(/asks for repos before its onboarding connects "github"/); -}); diff --git a/packages/workflow-catalog/tsconfig.json b/packages/workflow-catalog/tsconfig.json deleted file mode 100644 index d7611c122..000000000 --- a/packages/workflow-catalog/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "test"], - "compilerOptions": { - "types": ["bun"], - "noEmit": true - } -} diff --git a/scripts/checks/error-envelope.ts b/scripts/checks/error-envelope.ts index aa72932a7..8ded99938 100644 --- a/scripts/checks/error-envelope.ts +++ b/scripts/checks/error-envelope.ts @@ -9,7 +9,8 @@ // Documented exceptions live in ALLOWLIST below. Each entry is an // explicit ruling that the named file may keep a helper because it // already wraps the canonical `makeErrorEnvelope` (onboarding, -// workflow-catalog) or *is* the canonical helper (hub-client). +// connections, the hub's own template routes) or *is* the canonical +// helper (hub-client). import { Glob } from "bun"; import path from "node:path"; import { @@ -24,8 +25,8 @@ const SCAN_DIRS = ["apps", "packages", "workflows"]; const ALLOWLIST = new Set([ "packages/hub-client/src/error-envelope.ts", "packages/onboarding/src/routes.ts", - "packages/workflow-catalog/src/connect-github-routes.ts", - "packages/workflow-catalog/src/template-block-routes.ts", + "packages/connections/src/connect-github-routes.ts", + "apps/hub/src/templates/template-block-routes.ts", ]); // Arrow: `const ErrorEnvelope = (code: string, message: string) => ({ diff --git a/scripts/checks/report-error-baseline.txt b/scripts/checks/report-error-baseline.txt index 67c25e6f4..5678e68a8 100644 --- a/scripts/checks/report-error-baseline.txt +++ b/scripts/checks/report-error-baseline.txt @@ -34,6 +34,7 @@ apps/hub/src/routine-scheduler.ts 1 const reason = err instanceof Error ? err.me apps/hub/src/routine-scheduler.ts 1 log.error`marking routine ${claimed.id}'s failed fire also failed: ${ apps/hub/src/routine-scheduler.ts 1 log.error`routine scheduler tick failed: ${ apps/hub/src/shutdown.ts 1 return { kind: "failed", error }; +apps/hub/src/templates/template-block-routes.ts 1 const message = cause instanceof Error ? cause.message : String(cause); apps/sidecar/src/atomic-write.ts 1 logger.warn`parent-dir fsync failed for ${path}; durability is degraded but the file is renamed and fsynced — ${err instanceof Error ? err.message : String(err)}`; apps/sidecar/src/concurrency.ts 1 failures.push({ item, error }); apps/sidecar/src/conversation-state.ts 1 logger.warn`connector route for ${opts.agentKey} could not parse the inbound sender; leaving the thread unadvanced: ${cause instanceof Error ? cause.message : String(cause)}`; @@ -157,6 +158,8 @@ packages/connections-tools/src/tool.ts 1 return errorResult(call.id, err); packages/connections-tools/src/tool.ts 2 return errorResult(call.id, err); packages/connections-tools/src/tool.ts 3 return errorResult(call.id, err); packages/connections-tools/src/tool.ts 4 return errorResult(call.id, err); +packages/connections/src/connect-github-routes.ts 1 // A repoId `state.repos` doesn't carry means the card's own +packages/connections/src/connect-github-routes.ts 1 // The GitHub client's own errors (transport, HTTP status, shape packages/error-sink/src/index.ts 1 return context.refId ?? UNKNOWN_OPERATION; packages/folded-run-one-shot/src/one-shot-reply.ts 1 log.error`one-shot run ${triggerAddress}: undeploy failed during teardown (${reason}): ${ packages/folded-run-one-shot/src/one-shot-reply.ts 1 void settle("planning-run-send-failed", () => { @@ -226,6 +229,8 @@ packages/routines-tools/src/tool.ts 1 return once; packages/routines-tools/src/tool.ts 1 return value; packages/routines-tools/src/tool.ts 2 return errorResult(call.id, err); packages/routines-tools/src/tool.ts 3 return errorResult(call.id, err); +packages/routines-tools/src/tool.ts 4 return errorResult(call.id, err); +packages/routines-tools/src/tool.ts 5 return errorResult(call.id, err); packages/routines/src/cron.ts 1 return false; packages/routines/src/cron.ts 2 return false; packages/routines/src/routes.ts 1 log.error( @@ -258,9 +263,6 @@ packages/web-search-tools/src/tool.ts 1 return null; packages/web-search-tools/src/tool.ts 1 return { packages/webhook-triggers/src/ingress-routes.ts 1 return c.json( packages/webhook-triggers/src/signature.ts 1 return false; -packages/workflow-catalog/src/connect-github-routes.ts 1 // A repoId `state.repos` doesn't carry means the card's own -packages/workflow-catalog/src/connect-github-routes.ts 1 // The GitHub client's own errors (transport, HTTP status, shape -packages/workflow-catalog/src/template-block-routes.ts 1 const message = cause instanceof Error ? cause.message : String(cause); workflows/attio-task-agent/src/finalize-tool.ts 1 return { workflows/collateral-generation/src/finalize-tool.ts 1 return { workflows/diligence-brief/src/finalize-tool.ts 1 return { diff --git a/scripts/db-setup.ts b/scripts/db-setup.ts index 35ac52433..06cbde988 100644 --- a/scripts/db-setup.ts +++ b/scripts/db-setup.ts @@ -37,7 +37,7 @@ import { listWorkbenchLaunchFoldedRunIds, } from "../packages/chat/src/migrations"; import { applyWebhookTriggersMigrations } from "../packages/webhook-triggers/src/migrations"; -import { reconcileDuplicateRepoGrants } from "../packages/workflow-catalog/src/reconcile-duplicate-repo-grants"; +import { reconcileDuplicateRepoGrants } from "../packages/connections/src/reconcile-duplicate-repo-grants"; import { applyNotifyMigrations } from "../packages/notify/src/migrations"; import { applyRoutineMigrations } from "../packages/routines/src/migrations"; import { From d26b39b79c0bfc31d75496091b37b62679d53d13 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 2 Sep 2026 09:23:02 -0700 Subject: [PATCH 4/7] Regenerate report-error baseline after workflow-catalog removal (CL-7373) --- scripts/checks/report-error-baseline.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/checks/report-error-baseline.txt b/scripts/checks/report-error-baseline.txt index 5678e68a8..6aa7d7d49 100644 --- a/scripts/checks/report-error-baseline.txt +++ b/scripts/checks/report-error-baseline.txt @@ -229,8 +229,6 @@ packages/routines-tools/src/tool.ts 1 return once; packages/routines-tools/src/tool.ts 1 return value; packages/routines-tools/src/tool.ts 2 return errorResult(call.id, err); packages/routines-tools/src/tool.ts 3 return errorResult(call.id, err); -packages/routines-tools/src/tool.ts 4 return errorResult(call.id, err); -packages/routines-tools/src/tool.ts 5 return errorResult(call.id, err); packages/routines/src/cron.ts 1 return false; packages/routines/src/cron.ts 2 return false; packages/routines/src/routes.ts 1 log.error( From 513986519c7eee611908ce6fc5200e72bfcbf85b Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 2 Sep 2026 09:27:24 -0700 Subject: [PATCH 5/7] Regenerate bun.lock with the pinned bun version (CL-7373) --- bun.lock | 164 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 84 insertions(+), 80 deletions(-) diff --git a/bun.lock b/bun.lock index 74e74fca3..cc2b325a3 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,5 @@ { - "lockfileVersion": 2, + "lockfileVersion": 1, "configVersion": 1, "workspaces": { "": { @@ -2128,13 +2128,13 @@ "@better-fetch/fetch": ["@better-fetch/fetch@1.3.1", "", {}, "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g=="], - "@bufbuild/protobuf": ["@bufbuild/protobuf@2.14.1", "", {}, "sha512-agRJn3+EJDUe8AvxTx/LnHA/GErvLE62pSaSk7+MwFOtOv8eWBu/qCq2qoZjBjVZ3C2aiFJCveuSs17KMkYGOw=="], + "@bufbuild/protobuf": ["@bufbuild/protobuf@2.14.0", "", {}, "sha512-C3UGsiCwSprE2NKIIFA3hCDlpXTMCAXRZuEVp88L1GY36Y41+rYL5fryE+nOFhp4p4JPQvdV8PQ4DWgHgeTE+w=="], - "@chat-adapter/shared": ["@chat-adapter/shared@4.39.0", "", { "dependencies": { "chat": "4.39.0" } }, "sha512-zc5mB5rYqbp4ts+Z3tG/YwkCypPYphriYpG95ZMRNGiEYx3cYZJZFmAFFM4YLfdz3VIQ99Fd0HFDYVNyAMV7cg=="], + "@chat-adapter/shared": ["@chat-adapter/shared@4.38.1", "", { "dependencies": { "chat": "4.38.1" } }, "sha512-zJiMl43fiS3a2RjLFdCyTUTqcDY2xss1Acbzc9yfsw8YAkS0LZRdrNmp9/RjLhZB0+FzLarNAayvdKqF2GwnQg=="], - "@chat-adapter/slack": ["@chat-adapter/slack@4.39.0", "", { "dependencies": { "@chat-adapter/shared": "4.39.0", "@slack/socket-mode": "^2.0.5", "@slack/web-api": "^7.18.0", "chat": "4.39.0" } }, "sha512-nDBYFEmD9MUobUH+HJbOYwKYcaeoCFJDXVvd41flh5sslpixSUG2SFcz7F3py62jJ9c0YYNEdwprIExO20IyZQ=="], + "@chat-adapter/slack": ["@chat-adapter/slack@4.38.1", "", { "dependencies": { "@chat-adapter/shared": "4.38.1", "@slack/socket-mode": "^2.0.5", "@slack/web-api": "^7.18.0", "chat": "4.38.1" } }, "sha512-xMZ43YUQKzqOAiePR5mEFjhJPI6/jsEHCMq9Y+Rrm+N4+DotBddQDpbGMzb5oiogZD6iyMVCMvWSPLST7ZX0sg=="], - "@chat-adapter/state-memory": ["@chat-adapter/state-memory@4.39.0", "", { "dependencies": { "chat": "4.39.0" } }, "sha512-sTslMdRA9MG2MYUF9anD+eatZ1o3Nu64TkQCJ9GrGWfRCefM8DnIG/Op+Ej9XS6XjqOORb3eDJvy6IpvWrfT4A=="], + "@chat-adapter/state-memory": ["@chat-adapter/state-memory@4.38.1", "", { "dependencies": { "chat": "4.38.1" } }, "sha512-XYRmnnuckJ7HubQIRqKTwOn2XbpK+GQclN7mRJEpiGk/4LXD46g0XKrYPrQMGe8PGLMdSQ4139Huu34rvyfgSw=="], "@connectrpc/connect": ["@connectrpc/connect@2.1.2", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.7.0" } }, "sha512-MXkBijtcX09R10Eb6sFeIetc6w6746eio6xtfuyVOH7oQAacT1X0GzMIQFux6Qy8cq3W/T5qX5Bei8YbFtmRGA=="], @@ -2414,7 +2414,7 @@ "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], - "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.13.2", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.13.2" } }, "sha512-pGO7WhgHVLi7mi7zCLyJ+FkTad2aPTcjUXhRbgA4T5n1vLRJ/9uw3EsLieScNSX6HOfPF947bOP7N42l+KrWjg=="], + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.11.6", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.11.6" } }, "sha512-ZQ47qUTeNbGhHkCGExJ1oZhruoxKRaxO44RgFETl3T4c1rRxIBlAOnM3SAH4XHu7Ue2owJXP+jOx1vOuKuxcSg=="], "@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="], @@ -2486,21 +2486,21 @@ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@logtape/hono": ["@logtape/hono@2.3.2", "", { "peerDependencies": { "@logtape/logtape": "^2.3.2", "hono": "^4.0.0" } }, "sha512-fwjfktpQSucHQCO6OYCHUrcgwHPhU41VKTJt2gtqSoW3MjYMUtdZJTGJx2JTAj+8zyQ1cXqugkEKVG07wwSU4g=="], + "@logtape/hono": ["@logtape/hono@2.3.1", "", { "peerDependencies": { "@logtape/logtape": "^2.3.1", "hono": "^4.0.0" } }, "sha512-ryqg3HBdd79M0/wkiDIcFyB6RXBM1laauYDGWVqAVMrUu53mqRp5lDXrlQYmbIZBtyAyZK/TS2YJR22/79NzpQ=="], - "@logtape/logtape": ["@logtape/logtape@2.3.2", "", {}, "sha512-SfxHfSdDlTp6tLpYP5uezNitSCTQUB3FZ1MXDbgDBN9t9kvF+XL0rOGTIa/pJcDMy1o9FELaZ25E/Ypmsf5fNQ=="], + "@logtape/logtape": ["@logtape/logtape@2.3.1", "", {}, "sha512-nYLsBWMKk0Llp62U6cDbQxNcEKc0E0s5hWbAxkmUM8ax6VchGkPSjPrVp3ixFQUuvUB9ku7MSgPzFhmVTSh2Jw=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="], "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], - "@noble/ciphers": ["@noble/ciphers@2.4.0", "", {}, "sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw=="], + "@noble/ciphers": ["@noble/ciphers@2.3.0", "", {}, "sha512-Clu/xdfgVTf9o7ngLOURaxePwR0j8sjclKEtVij10/jGulwFsPWCvvRgG/XjUVf8Nei+jLG6uwyXzUTGY1DQrw=="], - "@noble/hashes": ["@noble/hashes@2.4.0", "", {}, "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA=="], + "@noble/hashes": ["@noble/hashes@2.3.0", "", {}, "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ=="], "@npmcli/agent": ["@npmcli/agent@4.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg=="], @@ -2574,55 +2574,55 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.1", "", { "os": "android", "cpu": "arm" }, "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.5", "", { "os": "android", "cpu": "arm" }, "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.1", "", { "os": "android", "cpu": "arm64" }, "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.5", "", { "os": "android", "cpu": "arm64" }, "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.63.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.63.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.63.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.63.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.63.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.63.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.63.1", "", { "os": "none", "cpu": "arm64" }, "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.5", "", { "os": "none", "cpu": "arm64" }, "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.63.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.63.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg=="], "@slack/logger": ["@slack/logger@4.0.1", "", { "dependencies": { "@types/node": ">=18" } }, "sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ=="], @@ -2668,9 +2668,9 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], - "@tanstack/query-core": ["@tanstack/query-core@5.102.8", "", {}, "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg=="], + "@tanstack/query-core": ["@tanstack/query-core@5.101.4", "", {}, "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw=="], - "@tanstack/react-query": ["@tanstack/react-query@5.102.8", "", { "dependencies": { "@tanstack/query-core": "5.102.8" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-TYBea4OuXWD7MhaSHq069TWbFe7rcwWN6kzT7JF0OKi1K6c1gTv2IzD6A6ExJsCMozdkqBWeuIUZmu4KQg0O5A=="], + "@tanstack/react-query": ["@tanstack/react-query@5.101.4", "", { "dependencies": { "@tanstack/query-core": "5.101.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -2694,11 +2694,11 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@26.4.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA=="], + "@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="], "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], - "@types/react-dom": ["@types/react-dom@19.2.5", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg=="], + "@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="], "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], @@ -2712,25 +2712,25 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.69.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.69.0", "@typescript-eslint/type-utils": "8.69.0", "@typescript-eslint/utils": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.69.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.67.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.67.0", "@typescript-eslint/type-utils": "8.67.0", "@typescript-eslint/utils": "8.67.0", "@typescript-eslint/visitor-keys": "8.67.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.67.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.69.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.69.0", "@typescript-eslint/types": "8.69.0", "@typescript-eslint/typescript-estree": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.67.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.67.0", "@typescript-eslint/types": "8.67.0", "@typescript-eslint/typescript-estree": "8.67.0", "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.69.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.69.0", "@typescript-eslint/types": "^8.69.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.67.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.67.0", "@typescript-eslint/types": "^8.67.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.69.0", "", { "dependencies": { "@typescript-eslint/types": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0" } }, "sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "@typescript-eslint/visitor-keys": "8.67.0" } }, "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.69.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.67.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.69.0", "", { "dependencies": { "@typescript-eslint/types": "8.69.0", "@typescript-eslint/typescript-estree": "8.69.0", "@typescript-eslint/utils": "8.69.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "@typescript-eslint/typescript-estree": "8.67.0", "@typescript-eslint/utils": "8.67.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.69.0", "", {}, "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.67.0", "", {}, "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.69.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.69.0", "@typescript-eslint/tsconfig-utils": "8.69.0", "@typescript-eslint/types": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.67.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.67.0", "@typescript-eslint/tsconfig-utils": "8.67.0", "@typescript-eslint/types": "8.67.0", "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.69.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.69.0", "@typescript-eslint/types": "8.69.0", "@typescript-eslint/typescript-estree": "8.69.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.67.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.67.0", "@typescript-eslint/types": "8.67.0", "@typescript-eslint/typescript-estree": "8.67.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.69.0", "", { "dependencies": { "@typescript-eslint/types": "8.69.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], @@ -2786,7 +2786,7 @@ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "axios": ["axios@1.20.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg=="], + "axios": ["axios@1.19.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], @@ -2794,7 +2794,7 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.11.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H/bNPUFHewJHyCTdjn1n3Pit5+2GmWT6mmeHImPX+8MA9NA6b67jO4gYmi4jTbCJb2otq34KMZnovndDPqJwhQ=="], "better-auth": ["better-auth@1.6.29", "", { "dependencies": { "@better-auth/core": "1.6.29", "@better-auth/drizzle-adapter": "1.6.29", "@better-auth/kysely-adapter": "1.6.29", "@better-auth/memory-adapter": "1.6.29", "@better-auth/mongo-adapter": "1.6.29", "@better-auth/prisma-adapter": "1.6.29", "@better-auth/telemetry": "1.6.29", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.4.0", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-7i2kOry+Jb1mRUR14kCDVS23GCKUGWxayZRXULbNz/6nFLPT6XSuGJVrwo/d+Qfq/iubA0/4Acw2/eg/tPCCnA=="], @@ -2824,7 +2824,7 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -2832,7 +2832,7 @@ "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], - "chat": ["chat@4.39.0", "", { "dependencies": { "@workflow/serde": "4.1.0-beta.2", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "remend": "^1.2.1", "unified": "^11.0.5" }, "peerDependencies": { "ai": "^6.0.182 || ^7.0.0", "workflow": "^5.0.0-beta.35", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai", "workflow", "zod"] }, "sha512-o7D+oAbQAYFVoc9+OH8wya11609ybRThNTF3s/5C1LAABGGi6IrxoQQEN/aIe+4h5pmKYzUjXeqTcuOZcpIAZA=="], + "chat": ["chat@4.38.1", "", { "dependencies": { "@workflow/serde": "4.1.0-beta.2", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "remend": "^1.2.1", "unified": "^11.0.5" }, "peerDependencies": { "ai": "^6.0.182 || ^7.0.0", "workflow": "^5.0.0-beta.35", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai", "workflow", "zod"] }, "sha512-FD6vNLT8clXHKhN3I1m+3Bn2nz0rsB6/bmNPsJeh6Cr7kwWLg0MYl2uor9YaKBp38F77sBRVKdYKVA6ihUby3w=="], "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], @@ -2908,11 +2908,11 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "e2b": ["e2b@2.46.1", "", { "dependencies": { "@bufbuild/protobuf": "^2.14.0", "@connectrpc/connect": "^2.1.2", "@connectrpc/connect-web": "^2.1.2", "chalk": "^5.3.0", "compare-versions": "^6.1.0", "dockerfile-ast": "^0.7.1", "glob": "^13.0.6", "openapi-fetch": "^0.14.1", "platform": "^1.3.6", "tar": "^7.5.22", "undici": "^7.29.0" }, "optionalDependencies": { "undici8": "npm:undici@8.10.0" } }, "sha512-OqYovS2oFrt4mk737CgfW/RoMadBYK84l5qjKpvbEoOB9KKxaZIXm7YUwOKSRTlijrrwDRX7oZlyPoVXiCpyTw=="], + "e2b": ["e2b@2.44.1", "", { "dependencies": { "@bufbuild/protobuf": "^2.12.1", "@connectrpc/connect": "^2.1.2", "@connectrpc/connect-web": "^2.1.2", "chalk": "^5.3.0", "compare-versions": "^6.1.0", "dockerfile-ast": "^0.7.1", "glob": "^13.0.6", "openapi-fetch": "^0.14.1", "platform": "^1.3.6", "tar": "^7.5.19", "undici": "^7.29.0" }, "optionalDependencies": { "undici8": "npm:undici@8.10.0" } }, "sha512-6yx3FjYnR2zlidvF5QLaFQAtv7WndKAIrP8NEi9lhJm5+98Iinb7zHYel/Ug/o9BEZTb7ODYUvD/0P9b2Q6ufg=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.420", "", {}, "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.411", "", {}, "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -2938,7 +2938,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@10.9.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A=="], + "eslint": ["eslint@10.8.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ=="], "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], @@ -2968,7 +2968,7 @@ "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "express-rate-limit": ["express-rate-limit@8.7.0", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g=="], + "express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -2980,7 +2980,7 @@ "fast-text-encoding": ["fast-text-encoding@1.0.6", "", {}, "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w=="], - "fast-uri": ["fast-uri@3.1.7", "", {}, "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg=="], + "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -3032,7 +3032,7 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "happy-dom": ["happy-dom@20.13.2", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-VC9HoEaT3Jf3K/1g/GjMYGAZCCg+ULgGVcXJlAeSzdTj8mIoPJRp14KVclP5gpUcF/kauuaVB2ri2/WxXbl4vg=="], + "happy-dom": ["happy-dom@20.11.6", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-Hldbg8AdAa5a5oDcZpjqnGitp7JB0hqWmfv/8qr+kft4vzSD8BHsbdRfzYvL/0QcbKcURC/yyoygbeDQarPvYg=="], "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], @@ -3066,7 +3066,7 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ip-address": ["ip-address@10.7.0", "", {}, "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA=="], + "ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -3090,7 +3090,7 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "isomorphic-git": ["isomorphic-git@1.41.9", "", { "dependencies": { "async-lock": "^1.4.1", "clean-git-ref": "^2.0.1", "crc-32": "^1.2.0", "diff3": "0.0.3", "ignore": "^5.1.4", "minimisted": "^2.0.0", "pako": "^1.0.10", "pify": "^4.0.1", "readable-stream": "^4.0.0", "sha.js": "^2.4.12", "simple-get": "^4.0.1" }, "bin": { "isogit": "cli.cjs" } }, "sha512-WOh4ujm5mznHphzQvwj9bVj+pQpV0oZ8H4lAnh4dSaie+lN/lJ2rb6rNOOcP+2m4z8IOQp186TkEULD28NMBJg=="], + "isomorphic-git": ["isomorphic-git@1.41.7", "", { "dependencies": { "async-lock": "^1.4.1", "clean-git-ref": "^2.0.1", "crc-32": "^1.2.0", "diff3": "0.0.3", "ignore": "^5.1.4", "minimisted": "^2.0.0", "pako": "^1.0.10", "pify": "^4.0.1", "readable-stream": "^4.0.0", "sha.js": "^2.4.12", "simple-get": "^4.0.1" }, "bin": { "isogit": "cli.cjs" } }, "sha512-ZCyMNg0eLUmuhGXdVQFi56fM93kwIqqlUSVnWbgzf1oDYidrQbWfrf+hEvn2E8xyo191zRPQyDCIlwg6YJ6isQ=="], "isomorphic-textencoder": ["isomorphic-textencoder@1.0.1", "", { "dependencies": { "fast-text-encoding": "^1.0.0" } }, "sha512-676hESgHullDdHDsj469hr+7t3i/neBKU9J7q1T4RHaWwLAsaQnywC0D1dIUId0YZ+JtVrShzuBk1soo0+GVcQ=="], @@ -3098,7 +3098,7 @@ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "jose": ["jose@6.2.10", "", {}, "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g=="], + "jose": ["jose@6.2.9", "", {}, "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -3158,7 +3158,7 @@ "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], - "lucide-react": ["lucide-react@1.39.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y8nXoEwvqqIsF927NBWXODa4bfMrcUeEb/9sgpwFqg0gUjgn3j5Hznk+v7STmPgZ2iQ11JKlbQGdFRuTOwvYkA=="], + "lucide-react": ["lucide-react@1.33.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-MTRwMy0ZlL8Ur/vOAiJ9XGHE+kFPC7brq6MxAm0GiGXEBj0qy0jA/pG4N675oSzciO/UCdX8T+5yUQdmDeTLxg=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -3284,13 +3284,13 @@ "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], - "nanostores": ["nanostores@1.5.3", "", {}, "sha512-rQLB6eV4f2AW/n3L0JmwCROpaisYy9EDEADvEFSd1C/qG8hB6O5TPlh9A791JRbJr4CnMQBzptDcvD9OR1+6WA=="], + "nanostores": ["nanostores@1.5.2", "", {}, "sha512-B0UbxzK1s0CN8Xht6r+7iT5+xV8PTaRERR1nATeplRv1Rw5YLWfVAid0hkqY3EceqpG4RjTk8GAwIxQY39Rnwg=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], - "node-releases": ["node-releases@2.0.54", "", {}, "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ=="], + "node-releases": ["node-releases@2.0.53", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="], "npm-install-checks": ["npm-install-checks@7.1.2", "", { "dependencies": { "semver": "^7.1.1" } }, "sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ=="], @@ -3324,7 +3324,7 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "p-map": ["p-map@7.0.7", "", {}, "sha512-VaWRu2i4FJNRtiRWCuuQRgfQ1B7a6+gMSrO+3j0EQi/k0ULfS9kosRxGoiqwzIjZTDI02tGfk5mXXltLg6QtfQ=="], + "p-map": ["p-map@7.0.6", "", {}, "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg=="], "p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], @@ -3346,7 +3346,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], @@ -3374,9 +3374,9 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "puppeteer-core": ["puppeteer-core@25.9.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.1", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1666840", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.3" } }, "sha512-U61rCwSMha62CA/Opy6tCx2Fx+ck7ouiKnbpEApzSoLYMoEu9F71nuFpHL55vmIt33/GYm6eKZVhH2ev0nAIeg=="], + "puppeteer-core": ["puppeteer-core@25.8.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.1", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1666840", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.1" } }, "sha512-LDOrawV8vfCVk+yLj2ozvajNP4Sv3OV9y3Tpiyy2g2Z+aQlbcozP6KJfI4iSBq7YQER+86ihEtPa5ioiZyWxMQ=="], - "qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="], + "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -3404,7 +3404,7 @@ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], - "remend": ["remend@1.3.1", "", {}, "sha512-N3DiY5qbRPoa5vkxn1oDLMyXOVTeo6Hp+XOj6SIqJAYUgLS0Q587gILPMom/qm86AQ/ZrcOdwEIzCz8V3J0nxQ=="], + "remend": ["remend@1.3.0", "", {}, "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -3412,7 +3412,7 @@ "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], - "rollup": ["rollup@4.63.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.1", "@rollup/rollup-android-arm64": "4.63.1", "@rollup/rollup-darwin-arm64": "4.63.1", "@rollup/rollup-darwin-x64": "4.63.1", "@rollup/rollup-freebsd-arm64": "4.63.1", "@rollup/rollup-freebsd-x64": "4.63.1", "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", "@rollup/rollup-linux-arm-musleabihf": "4.63.1", "@rollup/rollup-linux-arm64-gnu": "4.63.1", "@rollup/rollup-linux-arm64-musl": "4.63.1", "@rollup/rollup-linux-loong64-gnu": "4.63.1", "@rollup/rollup-linux-loong64-musl": "4.63.1", "@rollup/rollup-linux-ppc64-gnu": "4.63.1", "@rollup/rollup-linux-ppc64-musl": "4.63.1", "@rollup/rollup-linux-riscv64-gnu": "4.63.1", "@rollup/rollup-linux-riscv64-musl": "4.63.1", "@rollup/rollup-linux-s390x-gnu": "4.63.1", "@rollup/rollup-linux-x64-gnu": "4.63.1", "@rollup/rollup-linux-x64-musl": "4.63.1", "@rollup/rollup-openbsd-x64": "4.63.1", "@rollup/rollup-openharmony-arm64": "4.63.1", "@rollup/rollup-win32-arm64-msvc": "4.63.1", "@rollup/rollup-win32-ia32-msvc": "4.63.1", "@rollup/rollup-win32-x64-gnu": "4.63.1", "@rollup/rollup-win32-x64-msvc": "4.63.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg=="], + "rollup": ["rollup@4.62.5", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.5", "@rollup/rollup-android-arm64": "4.62.5", "@rollup/rollup-darwin-arm64": "4.62.5", "@rollup/rollup-darwin-x64": "4.62.5", "@rollup/rollup-freebsd-arm64": "4.62.5", "@rollup/rollup-freebsd-x64": "4.62.5", "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", "@rollup/rollup-linux-arm-musleabihf": "4.62.5", "@rollup/rollup-linux-arm64-gnu": "4.62.5", "@rollup/rollup-linux-arm64-musl": "4.62.5", "@rollup/rollup-linux-loong64-gnu": "4.62.5", "@rollup/rollup-linux-loong64-musl": "4.62.5", "@rollup/rollup-linux-ppc64-gnu": "4.62.5", "@rollup/rollup-linux-ppc64-musl": "4.62.5", "@rollup/rollup-linux-riscv64-gnu": "4.62.5", "@rollup/rollup-linux-riscv64-musl": "4.62.5", "@rollup/rollup-linux-s390x-gnu": "4.62.5", "@rollup/rollup-linux-x64-gnu": "4.62.5", "@rollup/rollup-linux-x64-musl": "4.62.5", "@rollup/rollup-openbsd-x64": "4.62.5", "@rollup/rollup-openharmony-arm64": "4.62.5", "@rollup/rollup-win32-arm64-msvc": "4.62.5", "@rollup/rollup-win32-ia32-msvc": "4.62.5", "@rollup/rollup-win32-x64-gnu": "4.62.5", "@rollup/rollup-win32-x64-msvc": "4.62.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw=="], "rou3": ["rou3@0.9.2", "", {}, "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ=="], @@ -3454,7 +3454,7 @@ "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], - "simple-icons": ["simple-icons@16.29.0", "", {}, "sha512-4H94f5ZgcCcgJroc902TFlFdgPu2IU2eD7+WebN2Z14xKYrKHeJ4UQcZwzgSuH4Rgzw+jp7sbHl40NefuIEYsg=="], + "simple-icons": ["simple-icons@16.28.0", "", {}, "sha512-sQPR5AtK/ijRjou7zw7mlLp08oB6FH7i0lOy5XJ2zp9mJs/yejgiOn7KvQoe2q4YJIx6VmgUSW5AOefebPt5kg=="], "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], @@ -3500,7 +3500,7 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tsx": ["tsx@4.23.13", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw=="], + "tsx": ["tsx@4.23.12", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -3512,7 +3512,7 @@ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "typescript-eslint": ["typescript-eslint@8.69.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.69.0", "@typescript-eslint/parser": "8.69.0", "@typescript-eslint/typescript-estree": "8.69.0", "@typescript-eslint/utils": "8.69.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-B3MltX0VqjUBNEe3b3sSuiRbfa6XrfHFtBiPamjT5AsW/dfq+y+bc0wyuS9DxAS1LyzCxRp2+rxzpLUvqM2BvA=="], + "typescript-eslint": ["typescript-eslint@8.67.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.67.0", "@typescript-eslint/parser": "8.67.0", "@typescript-eslint/typescript-estree": "8.67.0", "@typescript-eslint/utils": "8.67.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg=="], "undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], @@ -3532,7 +3532,7 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - "update-browserslist-db": ["update-browserslist-db@1.3.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw=="], + "update-browserslist-db": ["update-browserslist-db@1.3.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -3550,9 +3550,9 @@ "vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="], - "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.14", "", {}, "sha512-EQyqJMi552E4ZTf46izQ4Fj6XquqxCySR3J5ZSD1SisMf6RfpeOWHxGBE8Gr6V0/3GHIGdAzDn8F8+1nTGCnoQ=="], + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], - "vscode-languageserver-types": ["vscode-languageserver-types@3.18.3", "", {}, "sha512-XIlzJ7Qp/jzSI1ds7/FwPAWrPeTZA7pAtlW4hdJ1J6xXWJL6dR9QYnDhJOdLzdKhUQ5Mm6mvUMw+3DcOQQasPw=="], + "vscode-languageserver-types": ["vscode-languageserver-types@3.18.0", "", {}, "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g=="], "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.2", "", {}, "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA=="], @@ -3584,7 +3584,7 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], @@ -3596,6 +3596,8 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@corbits/artifacts-hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#81049ed", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/types": "^0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-81049ed", "sha512-oTE0iFDyQdz0ifG1epo39pwaCaYaw19YcKXwfaZqAEQ56a1g9YIozXwH9CG4NaUTwcJKUeYGuNls6oJsMPisCw=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -3616,7 +3618,9 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.8", "", {}, "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], + + "@workbench/hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#81049ed", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/types": "^0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-81049ed", "sha512-oTE0iFDyQdz0ifG1epo39pwaCaYaw19YcKXwfaZqAEQ56a1g9YIozXwH9CG4NaUTwcJKUeYGuNls6oJsMPisCw=="], "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], From fdaef044a7958ec870f33a786211713317ea28d1 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 2 Sep 2026 09:32:53 -0700 Subject: [PATCH 6/7] Move the workflow catalog into @corbits/workflows so routines never depends on templates (CL-7373) --- bun.lock | 7 +++-- packages/routines/package.json | 1 - packages/routines/src/client.ts | 4 +-- packages/routines/src/targets.ts | 2 -- packages/workflows/package.json | 4 ++- .../workflows/src}/catalog.test.ts | 0 .../workflows/src}/catalog.ts | 30 +++++++++---------- packages/workflows/src/client.ts | 12 ++++++++ packages/workflows/src/index.ts | 12 ++++++++ templates/index.ts | 2 +- templates/package.json | 2 +- 11 files changed, 50 insertions(+), 26 deletions(-) rename {templates => packages/workflows/src}/catalog.test.ts (100%) rename {templates => packages/workflows/src}/catalog.ts (94%) diff --git a/bun.lock b/bun.lock index cc2b325a3..a3069c029 100644 --- a/bun.lock +++ b/bun.lock @@ -1209,7 +1209,6 @@ "@intx/log": "0.3.0", "@intx/types": "workspace:*", "@workbench/hub-client": "workspace:*", - "@workbench/templates": "workspace:*", "arktype": "catalog:", "cronstrue": "^3.24.0", "drizzle-orm": "catalog:", @@ -1545,6 +1544,7 @@ }, "devDependencies": { "@types/bun": "catalog:", + "@workbench/connections": "workspace:*", "typescript": "catalog:", }, }, @@ -1555,6 +1555,7 @@ "@corbits/code-review": "workspace:*", "@corbits/jimmy-agent": "workspace:*", "@corbits/scout-agent": "workspace:*", + "@corbits/workflows": "workspace:*", "arktype": "catalog:", }, "devDependencies": { @@ -3596,7 +3597,7 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@corbits/artifacts-hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#81049ed", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/types": "^0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-81049ed", "sha512-oTE0iFDyQdz0ifG1epo39pwaCaYaw19YcKXwfaZqAEQ56a1g9YIozXwH9CG4NaUTwcJKUeYGuNls6oJsMPisCw=="], + "@corbits/memory-hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#9e6f213", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-9e6f213", "sha512-utnM4ZT2zmslcPXYWAAqxlDNLcpGsXFiTOtj8h7+OXnhCP0Eaw8yl25+yCTyHpvt3jcdeG4h5uFsSj7ou0BZCA=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], @@ -3620,7 +3621,7 @@ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], - "@workbench/hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#81049ed", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/types": "^0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-81049ed", "sha512-oTE0iFDyQdz0ifG1epo39pwaCaYaw19YcKXwfaZqAEQ56a1g9YIozXwH9CG4NaUTwcJKUeYGuNls6oJsMPisCw=="], + "@workbench/hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#9e6f213", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-9e6f213", "sha512-utnM4ZT2zmslcPXYWAAqxlDNLcpGsXFiTOtj8h7+OXnhCP0Eaw8yl25+yCTyHpvt3jcdeG4h5uFsSj7ou0BZCA=="], "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], diff --git a/packages/routines/package.json b/packages/routines/package.json index 0e2a20e80..a7404ea16 100644 --- a/packages/routines/package.json +++ b/packages/routines/package.json @@ -22,7 +22,6 @@ "@corbits/migration-runner": "workspace:*", "@corbits/slug": "workspace:*", "cronstrue": "^3.24.0", - "@workbench/templates": "workspace:*", "@corbits/workflows": "workspace:*", "@intx/authz": "0.3.0", "@intx/db": "workspace:*", diff --git a/packages/routines/src/client.ts b/packages/routines/src/client.ts index ce75542f9..b3534c098 100644 --- a/packages/routines/src/client.ts +++ b/packages/routines/src/client.ts @@ -151,7 +151,7 @@ export type CreateRoutineInput = { readonly trigger: RoutineTriggerT; readonly scope: "personal" | "bench"; /** Omitted only for a workflow whose result never posts to a workbench - * (see `@workbench/templates`'s `WorkflowCatalogEntry.deliveryMode`) + * (see `@corbits/workflows`'s `WorkflowCatalogEntry.deliveryMode`) * — the server itself still rejects a missing value for every other * workflow (`deliveryWorkbenchRequired`, routes.ts). */ readonly deliveryWorkbenchId?: string; @@ -198,7 +198,7 @@ export function routineRunsPath(tenantId: string, id: string): string { // `kind` groups the picker without changing execution semantics: an // "agent" is a single-step conversational fold, everything else a // "workflow". `assetName` is the raw catalog key -// (`@workbench/templates`'s `workflowCatalogEntry`); `name` is the +// (`@corbits/workflows`'s `workflowCatalogEntry`); `name` is the // display label. export const RoutineTargetKind = type("'agent' | 'workflow'"); export type RoutineTargetKind = typeof RoutineTargetKind.infer; diff --git a/packages/routines/src/targets.ts b/packages/routines/src/targets.ts index 649008f2b..783c1c193 100644 --- a/packages/routines/src/targets.ts +++ b/packages/routines/src/targets.ts @@ -20,8 +20,6 @@ import { isAutomatableWorkflowName, isConversationalWorkflowName, workflowDisplayName, -} from "@workbench/templates"; -import { listLaunchableDefinitions, type LaunchableDefinition, } from "@corbits/workflows"; diff --git a/packages/workflows/package.json b/packages/workflows/package.json index d561f7c95..222647f09 100644 --- a/packages/workflows/package.json +++ b/packages/workflows/package.json @@ -7,7 +7,8 @@ "type": "module", "exports": { ".": "./src/index.ts", - "./client": "./src/client.ts" + "./client": "./src/client.ts", + "./catalog": "./src/catalog.ts" }, "scripts": { "typecheck": "tsc --noEmit", @@ -29,6 +30,7 @@ }, "devDependencies": { "@types/bun": "catalog:", + "@workbench/connections": "workspace:*", "typescript": "catalog:" } } diff --git a/templates/catalog.test.ts b/packages/workflows/src/catalog.test.ts similarity index 100% rename from templates/catalog.test.ts rename to packages/workflows/src/catalog.test.ts diff --git a/templates/catalog.ts b/packages/workflows/src/catalog.ts similarity index 94% rename from templates/catalog.ts rename to packages/workflows/src/catalog.ts index e6ac9fe5b..8d2fa4770 100644 --- a/templates/catalog.ts +++ b/packages/workflows/src/catalog.ts @@ -16,21 +16,21 @@ // only here, so there is no second copy for them to drift from. import { type } from "arktype"; -import echoPkg from "../workflows/echo/package.json"; -import codeReviewPkg from "../workflows/code-review/package.json"; -import assistantPkg from "../workflows/assistant/package.json"; -import heartbeatPkg from "../workflows/heartbeat/package.json"; -import workbenchDigestPkg from "../workflows/workbench-digest/package.json"; -import granolaCallPkg from "../workflows/granola-call/package.json"; -import processGranolaCallPkg from "../workflows/process-granola-call/package.json"; -import morningBriefPkg from "../workflows/morning-brief/package.json"; -import painPointCollateralPkg from "../workflows/pain-point-collateral/package.json"; -import collateralGenerationPkg from "../workflows/collateral-generation/package.json"; -import redditOpportunityScannerPkg from "../workflows/reddit-opportunity-scanner/package.json"; -import last30DaysResearchPkg from "../workflows/last-30-days-research/package.json"; -import diligenceBriefPkg from "../workflows/diligence-brief/package.json"; -import exaTopicWatchPkg from "../workflows/exa-topic-watch/package.json"; -import attioTaskAgentPkg from "../workflows/attio-task-agent/package.json"; +import echoPkg from "../../../workflows/echo/package.json"; +import codeReviewPkg from "../../../workflows/code-review/package.json"; +import assistantPkg from "../../../workflows/assistant/package.json"; +import heartbeatPkg from "../../../workflows/heartbeat/package.json"; +import workbenchDigestPkg from "../../../workflows/workbench-digest/package.json"; +import granolaCallPkg from "../../../workflows/granola-call/package.json"; +import processGranolaCallPkg from "../../../workflows/process-granola-call/package.json"; +import morningBriefPkg from "../../../workflows/morning-brief/package.json"; +import painPointCollateralPkg from "../../../workflows/pain-point-collateral/package.json"; +import collateralGenerationPkg from "../../../workflows/collateral-generation/package.json"; +import redditOpportunityScannerPkg from "../../../workflows/reddit-opportunity-scanner/package.json"; +import last30DaysResearchPkg from "../../../workflows/last-30-days-research/package.json"; +import diligenceBriefPkg from "../../../workflows/diligence-brief/package.json"; +import exaTopicWatchPkg from "../../../workflows/exa-topic-watch/package.json"; +import attioTaskAgentPkg from "../../../workflows/attio-task-agent/package.json"; const CorbitsWorkflowBlock = type({ assetName: "string > 0", diff --git a/packages/workflows/src/client.ts b/packages/workflows/src/client.ts index 9147776e2..897be0772 100644 --- a/packages/workflows/src/client.ts +++ b/packages/workflows/src/client.ts @@ -34,3 +34,15 @@ export { type WorkflowLifecycle, type WorkflowLifecycleResult, } from "./detail/definition-lifecycle"; +export { + WorkflowTriggerField, + WORKFLOW_CATALOG, + isAutomatableWorkflowName, + isConversationalWorkflowName, + deliveryWorkbenchRequiredForWorkflowName, + workflowCatalogEntry, + workflowDisplayName, + validateTriggerFieldsAtCreate, + type WorkflowCatalogEntry, + type TriggerFieldsValidation, +} from "./catalog"; diff --git a/packages/workflows/src/index.ts b/packages/workflows/src/index.ts index 70fab5001..d46073880 100644 --- a/packages/workflows/src/index.ts +++ b/packages/workflows/src/index.ts @@ -18,3 +18,15 @@ export { type LaunchableDefinitionResolution, type LaunchableDefinitionResolver, } from "./launchable/target"; +export { + WorkflowTriggerField, + WORKFLOW_CATALOG, + isAutomatableWorkflowName, + isConversationalWorkflowName, + deliveryWorkbenchRequiredForWorkflowName, + workflowCatalogEntry, + workflowDisplayName, + validateTriggerFieldsAtCreate, + type WorkflowCatalogEntry, + type TriggerFieldsValidation, +} from "./catalog"; diff --git a/templates/index.ts b/templates/index.ts index 5281ab3fc..407c0c405 100644 --- a/templates/index.ts +++ b/templates/index.ts @@ -218,7 +218,7 @@ export { validateTriggerFieldsAtCreate, type WorkflowCatalogEntry, type TriggerFieldsValidation, -} from "./catalog"; +} from "@corbits/workflows/catalog"; import { GTM_TEMPLATE } from "./gtm"; import { CODE_REVIEW_TEMPLATE } from "./code-review"; diff --git a/templates/package.json b/templates/package.json index f84b96ad7..3f26e9076 100644 --- a/templates/package.json +++ b/templates/package.json @@ -7,7 +7,6 @@ "type": "module", "exports": { ".": "./index.ts", - "./catalog": "./catalog.ts", "./instantiate": "./instantiate.ts", "./settings": "./settings.ts" }, @@ -19,6 +18,7 @@ "@corbits/code-review": "workspace:*", "@corbits/jimmy-agent": "workspace:*", "@corbits/scout-agent": "workspace:*", + "@corbits/workflows": "workspace:*", "arktype": "catalog:" }, "devDependencies": { From f0a698e6466062b9d05cdefa143044b40285fdb6 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 2 Sep 2026 09:37:12 -0700 Subject: [PATCH 7/7] Fix formatting (CL-7373) --- docs/GLOSSARY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index 5d0255431..1ec2ee4fa 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -17,7 +17,7 @@ lower-level name left to list. | **DM** | kind: chat | The one 1:1 tenant with that agent — two opens never clone a second DM | | **Channel** | kind: workbench | A shared room between people and agents (multi-principal tenant). `+` mints an empty one with nobody hosted; named templates mint the same empty channel, then instantiate their Workbench Definition and run its onboarding walkthrough in the room | | **Definition** | workflow definition | A deployable unit of agent behavior, authored as code — not a template you mint into many conversations | -| **Workbench Definition** | — | The arktype description a named template instantiates: its default agents, routines, tools, required/optional plugins, and its ordered onboarding walkthrough — see `templates/index.ts`'s `WorkbenchDefinitionSchema` | +| **Workbench Definition** | — | The arktype description a named template instantiates: its default agents, routines, tools, required/optional plugins, and its ordered onboarding walkthrough — see `templates/index.ts`'s `WorkbenchDefinitionSchema` | | **Run** | workflow run | A definition executing in a bench; interactive runs carry conversations | | **Routine** | — | The named parent entity over runs of one definition — a trigger (or none), launch input, a delivery workbench, enabled state, and an explicit target: the definition's asset id, resolved at each launch to its latest approved deployment; see [`@corbits/routines`](../packages/routines/README.md) and [workflow-model.md](workflow-model.md) | | **Approval** | approval | A human decision gating an external side effect |