diff --git a/docs/bench-provisioning.md b/docs/bench-provisioning.md index bcf9245def..390bfd17d2 100644 --- a/docs/bench-provisioning.md +++ b/docs/bench-provisioning.md @@ -77,8 +77,10 @@ Connecting hands the person forward immediately, and the wait on the other side is as short as it can honestly be: the setup agent (Myra) leads `DEFAULT_WORKFLOWS`, so she is deployed before anything else, and the land hop drops the person into her room the moment she can answer. -The digest, recurring-task and research seeds finish behind them — -nobody needs a scheduled digest in their first second. +The other catalog workflows (digest, recurring-task, research) are not +seeded at all — they are deployable through the catalog instantiate route +(CL-7073) once someone picks one, not on a critical path nobody is waiting +on. While she is still coming up, the shared warm loader is the whole screen: one honest headline and a rotating tip, never an internal count. diff --git a/docs/local-rip.md b/docs/local-rip.md index 1c0c851af0..4e93f30670 100644 --- a/docs/local-rip.md +++ b/docs/local-rip.md @@ -86,10 +86,12 @@ Either path: `packages/connections/src/credential-test.ts`) before storing anything; 2. plants it as a credential on your bench alongside that provider's curated model catalog; -3. deploys and (unlike the OAuth callback's own fast half) confirms every - default workflow the platform ships: **echo**, **assistant**, and - **workbench-digest** (`packages/seeding/src/seed.ts`'s - `DEFAULT_WORKFLOWS`). +3. deploys and (unlike the OAuth callback's own fast half) confirms the + one default workflow every real signup gets: **assistant** + (`packages/seeding/src/seed.ts`'s `DEFAULT_WORKFLOWS`, CL-7074). + **echo** and **workbench-digest** are on-demand `CATALOG_WORKFLOWS` + entries now — deployed only when something asks for them by name, + never automatically at signup. Expect the page to show a short "setting up your workbench" wait while `/complete-setup` polls, then land on a "Your first routines are running" @@ -109,8 +111,10 @@ through the hub's native asset REST routes). Descendants inherit it; `seedTenant` does not pack. Isolated tests run with no explicit tenant config so the walkthrough's personal bench is itself the root — then the same publish happens once onto that -bench, and **echo**, **workbench-digest**, and **assistant** all come up -live. `scripts/e2e/local-rip.test.ts` asserts exactly that. The default +bench, and **assistant** comes up live by default; `scripts/e2e/local-rip.test.ts` +also deploys **echo** and **workbench-digest** from `CATALOG_WORKFLOWS` +onto that same bench (the on-demand path, exercised directly rather +than assumed) to prove they still come up live too. The default self-serve story is the other way: the hub ensures a root tenant at boot (`WORKBENCH_DEFAULT_TENANT`, default `workbench`), and first-login personal benches parent under it. diff --git a/docs/seed-reconciliation.md b/docs/seed-reconciliation.md index f893dbc357..2814c4ca41 100644 --- a/docs/seed-reconciliation.md +++ b/docs/seed-reconciliation.md @@ -50,10 +50,30 @@ answers 503 — never a 404, which would read as "no such template". ## Default scheduled workflows (onboarding / boot-time seeding) Seed never POSTs `/routines`. Native `ScheduleTrigger` ticks digest; -last-30-days-research stays a deployed workflow in `DEFAULT_WORKFLOWS`, -not a wrapper row. `scripts/db-setup.ts` drops a leftover `routines` -schema after the digest enablement handoff. There is no preset-wrapper -prune: seed does not plant wrapper rows. +last-30-days-research stays a deployed workflow, not a wrapper row. +`scripts/db-setup.ts` drops a leftover `routines` schema after the +digest enablement handoff. There is no preset-wrapper prune: seed does +not plant wrapper rows. + +## Default vs. on-demand catalog workflows (CL-7074) + +`DEFAULT_WORKFLOWS` (`packages/seeding/src/seed.ts`) is the set every +real signup gets automatically: `assistant` (Myra), and nothing else. A +fresh bench used to also pay a git push and a sidecar probe for `echo`, +`workbench-digest`, and `last-30-days-research` — three workflows +nobody had asked for yet. Those three now live in `CATALOG_WORKFLOWS`, +same shape (`DefinitionWithAgentSteps`-backed `DefaultWorkflow` +entries), deployed the same way (`ensureWorkflowAsset` → `pushWorkflow` +→ `ensureDeployment`), but only when something asks for one by name — +the catalog/instantiate surface, or a test standing up its own fixture +bench — never automatically at signup. + +There is no orphan-retire for an entry that moved from +`DEFAULT_WORKFLOWS` to `CATALOG_WORKFLOWS`: an asset already deployed +on an existing bench from before the move is left exactly as it is. +`CATALOG_TEST_WORKFLOWS` remains the separate, never-reaches-a-real- +signup set for workflows that exist only to exercise the platform +continuously. ## Default skills (boot-time seeding) diff --git a/packages/onboarding/test/complete-credential.test.ts b/packages/onboarding/test/complete-credential.test.ts index fd88e78456..c9ac97c2db 100644 --- a/packages/onboarding/test/complete-credential.test.ts +++ b/packages/onboarding/test/complete-credential.test.ts @@ -478,12 +478,7 @@ describe("completeCredentialSetup", () => { kind: "seeded", tenantId: TENANT_ID, tenantSlug: TENANT_SLUG, - workflows: [ - "assistant", - "echo", - "workbench-digest", - "last-30-days-research", - ], + workflows: ["assistant"], }); expect(seedCatalogCalls).toHaveLength(1); expect(seedTenantCalls[0]?.model.provider).toBe("anthropic"); @@ -528,12 +523,7 @@ describe("completeCredentialSetup", () => { kind: "seeded", tenantId: TENANT_ID, tenantSlug: TENANT_SLUG, - workflows: [ - "assistant", - "echo", - "workbench-digest", - "last-30-days-research", - ], + workflows: ["assistant"], }); expect(seedCatalogCalls).toHaveLength(1); expect(seedTenantCalls).toHaveLength(1); @@ -578,12 +568,7 @@ describe("completeCredentialSetup", () => { kind: "seeded", tenantId: TENANT_ID, tenantSlug: TENANT_SLUG, - workflows: [ - "assistant", - "echo", - "workbench-digest", - "last-30-days-research", - ], + workflows: ["assistant"], }); expect(seedCatalogCalls).toHaveLength(1); expect(seedCatalogCalls[0]?.provider).toBe("groq"); @@ -970,12 +955,7 @@ describe("completeCredentialSetup", () => { expect(result.kind).toBe("seeded"); if (result.kind === "seeded") { - expect(result.workflows).toEqual([ - "assistant", - "echo", - "workbench-digest", - "last-30-days-research", - ]); + expect(result.workflows).toEqual(["assistant"]); } }); @@ -1458,8 +1438,8 @@ describe("completeCredentialSetup", () => { // (one POST each for model and offering); the rest of the chain is // still a single provider/credential row. const anthropicCatalogSize = CATALOG_SEEDS.anthropic.models.length; - expect(assetCreatePosts).toBe(4); - expect(deploymentCreatePosts).toBe(4); + expect(assetCreatePosts).toBe(1); + expect(deploymentCreatePosts).toBe(1); expect(catalogModelCreatePosts).toBe(anthropicCatalogSize); expect(catalogProviderCreatePosts).toBe(1); expect(catalogOfferingCreatePosts).toBe(anthropicCatalogSize); @@ -1468,8 +1448,8 @@ describe("completeCredentialSetup", () => { // existing row rather than leaving it untouched (the CL-6103 fix, // updated by CL-6123 to no longer require a probe first). expect(credentialRotatePatches).toBe(1); - expect(assets.length).toBe(4); - expect(deployments.length).toBe(4); + expect(assets.length).toBe(1); + expect(deployments.length).toBe(1); }); test("a pasted key with no metadata stays an ordinary api_key credential", async () => { @@ -1563,12 +1543,7 @@ describe("completeCredentialSetup", () => { principalId: PRINCIPAL_ID, tenantDomain: "alice-user1.bench.local", deployed: [], - pending: [ - "assistant", - "echo", - "workbench-digest", - "last-30-days-research", - ], + pending: ["assistant"], message: "Your workbench is ready — agents will come online shortly.", }); }); @@ -1767,12 +1742,7 @@ describe("ensureSeeded (the slow half)", () => { expect(result).toEqual({ kind: "seeded", - workflows: [ - "assistant", - "echo", - "workbench-digest", - "last-30-days-research", - ], + workflows: ["assistant"], }); expectNoConfirmation(seedTenantCalls); expect(seedTenantCalls[0]?.model.provider).toBe("anthropic"); @@ -1783,6 +1753,9 @@ describe("ensureSeeded (the slow half)", () => { // (bench-provisioning's `runOnce` → `ensureSeeded` → `seedTenant`), // and `seedTenant` works through the list in order at ~20s each — so // the order handed in here is the order a fresh signup experiences. + // CL-7074 narrowed DEFAULT_WORKFLOWS to just the setup agent, so + // "before anything else" is now trivially satisfied by being the + // only entry — this still pins that fact rather than assuming it. const workflowOrder: string[] = []; await ensureSeeded({ @@ -1806,7 +1779,6 @@ describe("ensureSeeded (the slow half)", () => { }); expect(workflowOrder[0]).toBe(SETUP_AGENT_ASSET_NAME); - expect(workflowOrder.length).toBeGreaterThan(1); }); test("two overlapping calls for the same tenant never double-deploy — the same 409-then-list tolerance seedTenant already has", async () => { @@ -1984,10 +1956,10 @@ describe("ensureSeeded (the slow half)", () => { expect(first.kind).toBe("seeded"); expect(second.kind).toBe("seeded"); - expect(assetCreatePosts).toBe(4); - expect(deploymentCreatePosts).toBe(4); - expect(assets.length).toBe(4); - expect(deployments.length).toBe(4); + expect(assetCreatePosts).toBe(1); + expect(deploymentCreatePosts).toBe(1); + expect(assets.length).toBe(1); + expect(deployments.length).toBe(1); }); // CL-6264: tonight's live failure — completeCredentialSetup -> @@ -2056,8 +2028,8 @@ describe("ensureSeeded (the slow half)", () => { expect(result).toEqual({ kind: "seeded-pending-agents", - deployed: ["assistant", "echo"], - pending: ["workbench-digest", "last-30-days-research"], + deployed: ["assistant"], + pending: [], message: "Your workbench is ready — agents will come online shortly.", }); }); diff --git a/packages/onboarding/test/complete-setup-routes.test.ts b/packages/onboarding/test/complete-setup-routes.test.ts index c23a5328f4..4306ade0be 100644 --- a/packages/onboarding/test/complete-setup-routes.test.ts +++ b/packages/onboarding/test/complete-setup-routes.test.ts @@ -631,7 +631,13 @@ describe("POST /complete-setup", () => { // itself — deploying only what is missing on a later pass — is // covered by "a half-provisioned bench keeps its row and converges on // a later pass" in ./bench-provisioning.test.ts. - test("a bench whose agents are not all live keeps its pending row for the drain", async () => { + // + // CL-7074 narrowed DEFAULT_WORKFLOWS to just the setup agent, so + // "partway through" no longer means "one of several live, the rest + // pending" — it means the one default workflow's asset exists but + // has not gone live yet (the sidecar push landed, the deploy + // confirmation has not). + test("a bench whose agent is not yet live keeps its pending row for the drain", async () => { const liveWorkflow = DEFAULT_WORKFLOWS[0]; if (liveWorkflow === undefined) { throw new Error("DEFAULT_WORKFLOWS is empty"); @@ -653,10 +659,9 @@ describe("POST /complete-setup", () => { })), ), ); - // Only the first workflow made it live; the rest are still waiting - // on the drain. + // The asset exists, but no deployment has gone live yet. hub.get(`/api/tenants/${TENANT_ID}/workflows/deployments`, (c) => - c.json([{ definitionAssetId: "ast_0", status: "deployed" }]), + c.json([]), ); const server = Bun.serve({ port: 0, fetch: hub.fetch }); const pendingSeedStore = createInMemoryPendingSeedStore(testCipher()); @@ -691,11 +696,9 @@ describe("POST /complete-setup", () => { kind: "provisioning", tenantId: TENANT_ID, tenantSlug: TENANT_SLUG, - // The one live workflow here IS the setup agent — she deploys - // first (CL-6462), so this bench is already chat-ready. - setupAgentReady: true, - deployed: [liveWorkflow.assetName], - pending: DEFAULT_WORKFLOWS.slice(1).map((w) => w.assetName), + setupAgentReady: false, + deployed: [], + pending: [liveWorkflow.assetName], }); // Not finished yet — clearing the row here would strand the diff --git a/packages/onboarding/test/connect-deploys-nothing.test.ts b/packages/onboarding/test/connect-deploys-nothing.test.ts index 4219fc837e..fcfe054eef 100644 --- a/packages/onboarding/test/connect-deploys-nothing.test.ts +++ b/packages/onboarding/test/connect-deploys-nothing.test.ts @@ -282,8 +282,17 @@ describe("POST /complete — connecting deploys nothing", () => { }); describe("GET /provisioning-status", () => { - test("reports live progress while agents are still coming online", async () => { - const { hub } = fakeHub({ seededWorkflows: ALL_WORKFLOWS.slice(0, 2) }); + // CL-7074 narrowed DEFAULT_WORKFLOWS to just the setup agent, so the + // "some deployed, some still pending" progress bar this route used to + // report (CL-6462, when the default set was echo/assistant/ + // workbench-digest) can no longer happen for a real bench: with one + // default workflow, provisioning-status is binary — nothing deployed + // yet (`provisioning`, setup agent not ready) or fully deployed + // (`ready`). This test replaces the old two-test "partial progress" + // coverage with that binary reality; `setupAgentReady` and + // `kind: "ready"` are asserted at their other call sites below. + test("reports provisioning, with the setup agent not yet ready, before she deploys", async () => { + const { hub } = fakeHub({ seededWorkflows: [] }); const server = Bun.serve({ port: 0, fetch: hub.fetch }); const store = createInMemoryPendingSeedStore(testCipher()); try { @@ -296,42 +305,16 @@ describe("GET /provisioning-status", () => { const response = await app.request("/api/onboarding/provisioning-status"); const body = (await response.json()) as { kind: string; + setupAgentReady: boolean; deployed: string[]; pending: string[]; }; expect(response.status).toBe(200); expect(body.kind).toBe("provisioning"); - expect(body.deployed).toEqual(ALL_WORKFLOWS.slice(0, 2)); - expect(body.pending).toEqual(ALL_WORKFLOWS.slice(2)); - } finally { - server.stop(true); - } - }); - - test("says the setup agent is ready the moment she is live, while the rest are still pending", async () => { - // CL-6462: this one flag is what lets the land hop drop someone into - // a conversation without waiting out the whole seed set. - const { hub } = fakeHub({ seededWorkflows: [SETUP_AGENT_ASSET_NAME] }); - const server = Bun.serve({ port: 0, fetch: hub.fetch }); - const store = createInMemoryPendingSeedStore(testCipher()); - try { - const app = mountAuthenticated( - createOnboardingRoutes( - routeDeps({ hubUrl: `http://localhost:${server.port}`, store }), - ), - ); - - const response = await app.request("/api/onboarding/provisioning-status"); - const body = (await response.json()) as { - kind: string; - setupAgentReady: boolean; - pending: string[]; - }; - - expect(body.kind).toBe("provisioning"); - expect(body.setupAgentReady).toBe(true); - expect(body.pending.length).toBeGreaterThan(0); + expect(body.setupAgentReady).toBe(false); + expect(body.deployed).toEqual([]); + expect(body.pending).toEqual(ALL_WORKFLOWS); } finally { server.stop(true); } diff --git a/packages/onboarding/test/provision.test.ts b/packages/onboarding/test/provision.test.ts index 4e2fd52f85..af1594cbc6 100644 --- a/packages/onboarding/test/provision.test.ts +++ b/packages/onboarding/test/provision.test.ts @@ -860,11 +860,10 @@ describe("provisionPersonalTenantIfNeeded", () => { seeded: true, tenantId: "ten_new", }); - // Attempt 1 fails creating the echo asset. The retry re-runs from - // scratch: one create call per default workflow — echo, assistant, - // workbench-digest, last-30-days-research — on top of the one failed - // attempt. - expect(assetCreateAttempts).toBe(5); + // Attempt 1 fails creating the (only) default workflow's asset. The + // retry re-runs from scratch: one create call for the default set + // (assistant, CL-7074), on top of the one failed attempt. + expect(assetCreateAttempts).toBe(2); }); test("a fully seeded personal bench reports existing-member with seeded: true, and backfills a grant added to SEED_GRANTS after it was provisioned", async () => { diff --git a/packages/seeding/src/index.ts b/packages/seeding/src/index.ts index 07292b3eb7..2139ce0ac6 100644 --- a/packages/seeding/src/index.ts +++ b/packages/seeding/src/index.ts @@ -19,6 +19,7 @@ export type { } from "./seed"; export { CATALOG_TEST_WORKFLOWS, + CATALOG_WORKFLOWS, DEFAULT_WORKFLOWS, SEED_GRANTS, inferenceCredentialName, diff --git a/packages/seeding/src/seed.ts b/packages/seeding/src/seed.ts index 2c2e11baa9..a0a6ba760e 100644 --- a/packages/seeding/src/seed.ts +++ b/packages/seeding/src/seed.ts @@ -221,13 +221,19 @@ export const SETUP_AGENT_ASSET_NAME = "assistant"; /** * The workflow set every real tenant starts with: the general-purpose - * assistant, the echo walking-skeleton, and the workbench-digest - * automation the Routines picker can honestly offer. This is what + * assistant, and nothing else (CL-7074). This is what * `provisionPersonalTenantIfNeeded` (`@workbench/onboarding`) deploys * on first login for every real user — growing it is adding an entry * here, nothing more, but an entry here reaches every signup, so it is - * never the place for a workflow that exists only to exercise the - * platform itself. See `CATALOG_TEST_WORKFLOWS` for those. + * never the place for a workflow that is not something every person + * needs the moment they land. `echo`, `workbench-digest`, and + * `last-30-days-research` used to live here; a signup paid a git push + * and a sidecar probe for each of them even though nobody asked for + * them. They now live in `CATALOG_WORKFLOWS`, deployable through the + * catalog instantiate route (CL-7073) rather than seeded onto every + * bench. See `CATALOG_TEST_WORKFLOWS` + * for the platform-exercise set, which never reaches a real signup at + * all. * * Order is a product decision, not a formality (CL-6462): `seedTenant` * deploys this array in sequence at roughly 20s each, and the setup @@ -235,10 +241,6 @@ export const SETUP_AGENT_ASSET_NAME = "assistant"; * It goes first so a fresh signup lands in a working conversation in * seconds while the rest converge behind them; a signup that waited on * the whole set stared at a progress screen for minutes. - * - * workbench-digest is the seed automation: schedulable, not a chat host, - * friendly display name. It uses the tenant's real model so a scheduled - * run can produce a real digest line. */ export const DEFAULT_WORKFLOWS: readonly DefaultWorkflow[] = [ { @@ -256,6 +258,20 @@ export const DEFAULT_WORKFLOWS: readonly DefaultWorkflow[] = [ }), ), }, +]; + +/** + * Workflows every real tenant CAN have, but none is deployed by default + * (CL-7074) — a caller deploys one on demand the same way `seedTenant` + * deploys any `DefaultWorkflow`: `ensureWorkflowAsset` → + * `pushWorkflow` → `ensureDeployment`. `CL-7073` is the caller that + * offers these from a catalog/instantiate surface; nothing here reaches + * a bench until something asks for it by name. An asset already + * deployed on an existing bench (a prior seed run, before these moved + * out of `DEFAULT_WORKFLOWS`) is untouched — there is no orphan-retire + * for these entries, on purpose (see `docs/seed-reconciliation.md`). + */ +export const CATALOG_WORKFLOWS: readonly DefaultWorkflow[] = [ { assetName: "echo", displayName: catalogDisplayName("echo"), @@ -290,9 +306,9 @@ export const DEFAULT_WORKFLOWS: readonly DefaultWorkflow[] = [ assetName: "last-30-days-research", displayName: catalogDisplayName("last-30-days-research"), automatable: catalogAutomatable("last-30-days-research"), - // Deployed automation in the default set. Seed no longer POSTs a - // wrapper row; last-30-days-research stays a deployed workflow - // without a native ScheduleTrigger. + // Deployed automation, on demand. Seed never POSTs a wrapper row; + // last-30-days-research stays a deployed workflow without a native + // ScheduleTrigger. buildJson: (tenantDomain, model) => serializeLast30DaysResearchWorkflow( buildLast30DaysResearchWorkflow({ @@ -316,9 +332,10 @@ export const DEFAULT_WORKFLOWS: readonly DefaultWorkflow[] = [ * explicit, dev/CI-specific caller (`workbench seed` with * `WORKBENCH_SEED_CATALOG_TEST_WORKFLOWS` set) opts in. * - * workbench-digest used to live here as a platform exercise; it is now the - * seed automation in `DEFAULT_WORKFLOWS` so every personal bench has an - * honest Routines-picker option. + * workbench-digest used to live here as a platform exercise, then moved + * to `DEFAULT_WORKFLOWS`; it now lives in `CATALOG_WORKFLOWS` (CL-7074), + * deployable through the catalog instantiate route (CL-7073) rather than + * seeded onto every bench. */ export const CATALOG_TEST_WORKFLOWS: readonly DefaultWorkflow[] = [ { diff --git a/packages/seeding/test/routine-launchability.test.ts b/packages/seeding/test/routine-launchability.test.ts index 75d95a17fe..ce339aaff5 100644 --- a/packages/seeding/test/routine-launchability.test.ts +++ b/packages/seeding/test/routine-launchability.test.ts @@ -18,7 +18,11 @@ // dangling reference nothing would ever execute. import { expect, test } from "bun:test"; -import { DEFAULT_WORKFLOWS, type ModelSource } from "../src/seed"; +import { + CATALOG_WORKFLOWS, + DEFAULT_WORKFLOWS, + type ModelSource, +} from "../src/seed"; const FAKE_MODEL: ModelSource = { provider: "ollama", @@ -32,8 +36,8 @@ type SerializedStepDefinition = { readonly steps: Readonly>; }; -test("every default workflow's deployed definition is a well-formed, launchable step graph", () => { - for (const workflow of DEFAULT_WORKFLOWS) { +test("every default and on-demand catalog workflow's deployed definition is a well-formed, launchable step graph", () => { + for (const workflow of [...DEFAULT_WORKFLOWS, ...CATALOG_WORKFLOWS]) { const json = workflow.buildJson("example.test", FAKE_MODEL); const definition = JSON.parse(json) as SerializedStepDefinition; diff --git a/packages/seeding/test/seed.test.ts b/packages/seeding/test/seed.test.ts index efd7ca4297..2d7e28f397 100644 --- a/packages/seeding/test/seed.test.ts +++ b/packages/seeding/test/seed.test.ts @@ -6,6 +6,7 @@ import { } from "@corbits/hub-api-client"; import { CATALOG_TEST_WORKFLOWS, + CATALOG_WORKFLOWS, DEFAULT_WORKFLOWS, NOOP_MODEL_SOURCE, reconcileSeedGrants, @@ -255,7 +256,7 @@ describe("seedTenant", () => { return undefined; }; - const echoOnly = DEFAULT_WORKFLOWS.filter((w) => w.assetName === "echo"); + const echoOnly = CATALOG_WORKFLOWS.filter((w) => w.assetName === "echo"); await seedTenant( args({ api: fakeAPI(handler), @@ -344,7 +345,7 @@ describe("seedTenant", () => { return undefined; }; - const echoOnly = DEFAULT_WORKFLOWS.filter((w) => w.assetName === "echo"); + const echoOnly = CATALOG_WORKFLOWS.filter((w) => w.assetName === "echo"); await seedTenant( args({ api: fakeAPI(handler), @@ -413,7 +414,7 @@ describe("seedTenant", () => { return undefined; }; - const echoOnly = DEFAULT_WORKFLOWS.filter((w) => w.assetName === "echo"); + const echoOnly = CATALOG_WORKFLOWS.filter((w) => w.assetName === "echo"); await seedTenant( args({ api: fakeAPI(handler), @@ -573,7 +574,7 @@ describe("seedTenant", () => { return baseRoutes(method, path); }; - const echoOnly = DEFAULT_WORKFLOWS.filter((w) => w.assetName === "echo"); + const echoOnly = CATALOG_WORKFLOWS.filter((w) => w.assetName === "echo"); await seedTenant( args({ api: fakeAPI(handler), @@ -674,7 +675,7 @@ describe("seedTenant", () => { return baseRoutes(method, path); }; - const echoOnly = DEFAULT_WORKFLOWS.filter((w) => w.assetName === "echo"); + const echoOnly = CATALOG_WORKFLOWS.filter((w) => w.assetName === "echo"); await seedTenant( args({ api: fakeAPI(handler), @@ -840,8 +841,11 @@ describe("seedTenant", () => { expect(DEFAULT_WORKFLOWS[0]?.assetName).toBe(SETUP_AGENT_ASSET_NAME); }); - test("the default set also includes the echo walking skeleton", () => { - expect(DEFAULT_WORKFLOWS.map((w) => w.assetName)).toContain("echo"); + test("the default set is Myra only; echo is deployed on demand from the catalog", () => { + // CL-7074: a fresh signup no longer pays a git push and sidecar + // probe for workflows nobody asked for. + expect(DEFAULT_WORKFLOWS.map((w) => w.assetName)).toEqual(["assistant"]); + expect(CATALOG_WORKFLOWS.map((w) => w.assetName)).toContain("echo"); }); test("the seeded assistant is productized under the Myra display name", () => { @@ -865,37 +869,40 @@ describe("seedTenant", () => { }); test("echo and assistant carry no modelSource override, so they deploy against the tenant's real model", () => { - const realModelWorkflows = DEFAULT_WORKFLOWS.filter( - (w) => w.assetName === "echo" || w.assetName === "assistant", - ); + const realModelWorkflows = [ + ...CATALOG_WORKFLOWS, + ...DEFAULT_WORKFLOWS, + ].filter((w) => w.assetName === "echo" || w.assetName === "assistant"); expect(realModelWorkflows).toHaveLength(2); for (const workflow of realModelWorkflows) { expect(workflow.modelSource).toBeUndefined(); } }); - test("the default set consumed by real tenant provisioning is assistant, echo, workbench-digest, and last-30-days-research", () => { + test("the default set consumed by real tenant provisioning is assistant only; echo, workbench-digest, and last-30-days-research are on-demand catalog entries", () => { // provisionPersonalTenantIfNeeded (@workbench/onboarding) deploys - // DEFAULT_WORKFLOWS for every real signup. workbench-digest is the - // seed automation the native ScheduleTrigger ticks. last-30-days- - // research is a deployed automation, not a routine wrapper. - // The remaining catalog-test workflows exist only to - // exercise the platform continuously and must never reach a real - // user through this array — they are seeded only via the explicit + // DEFAULT_WORKFLOWS for every real signup (CL-7074: Myra only). The + // remaining catalog workflows — echo, workbench-digest, + // last-30-days-research — deploy on demand (CL-7073) from + // CATALOG_WORKFLOWS, never automatically onto a real signup. The + // catalog-test workflows exist only to exercise the platform + // continuously and must never reach a real user through either + // array — they are seeded only via the explicit // CATALOG_TEST_WORKFLOWS opt-in. - expect(DEFAULT_WORKFLOWS.map((w) => w.assetName)).toEqual([ - "assistant", + expect(DEFAULT_WORKFLOWS.map((w) => w.assetName)).toEqual(["assistant"]); + expect(CATALOG_WORKFLOWS.map((w) => w.assetName)).toEqual([ "echo", "workbench-digest", "last-30-days-research", ]); }); - test("catalog-test workflows declare a modelSource override; defaults do not", () => { - // Defaults (echo, assistant, workbench-digest) deploy against the + test("catalog-test workflows declare a modelSource override; defaults and on-demand catalog workflows do not", () => { + // Defaults (assistant) and the on-demand catalog (echo, + // workbench-digest, last-30-days-research) deploy against the // tenant's real model. Catalog-test entries stay free via // NOOP_MODEL_SOURCE. - for (const workflow of DEFAULT_WORKFLOWS) { + for (const workflow of [...DEFAULT_WORKFLOWS, ...CATALOG_WORKFLOWS]) { expect(workflow.modelSource).toBeUndefined(); } for (const workflow of CATALOG_TEST_WORKFLOWS) { @@ -1004,14 +1011,14 @@ describe("seedTenant", () => { expect(output).toContain("confirmed workflow heartbeat: run run_1 started"); }); - test("the default set includes the workbench-digest automation", () => { - expect(DEFAULT_WORKFLOWS.map((w) => w.assetName)).toContain( + test("the on-demand catalog includes the workbench-digest automation", () => { + expect(CATALOG_WORKFLOWS.map((w) => w.assetName)).toContain( "workbench-digest", ); }); test("workbench-digest is automatable with a friendly display name and no noop pin", () => { - const workbenchDigest = DEFAULT_WORKFLOWS.find( + const workbenchDigest = CATALOG_WORKFLOWS.find( (w) => w.assetName === "workbench-digest", ); if (!workbenchDigest) @@ -1024,7 +1031,9 @@ describe("seedTenant", () => { test("echo and assistant are not automatable", () => { for (const name of ["echo", "assistant"] as const) { - const workflow = DEFAULT_WORKFLOWS.find((w) => w.assetName === name); + const workflow = [...CATALOG_WORKFLOWS, ...DEFAULT_WORKFLOWS].find( + (w) => w.assetName === name, + ); if (!workflow) throw new Error(`expected ${name}`); expect(workflow.automatable).toBe(false); expect(workflow.displayName.length).toBeGreaterThan(0); @@ -1087,7 +1096,7 @@ describe("seedTenant", () => { return undefined; }; - const digestOnly = DEFAULT_WORKFLOWS.filter( + const digestOnly = CATALOG_WORKFLOWS.filter( (w) => w.assetName === "workbench-digest", ); await seedTenant( @@ -1204,7 +1213,7 @@ describe("seedTenant", () => { args({ api: fakeAPI(handler), log, - workflows: DEFAULT_WORKFLOWS.filter( + workflows: CATALOG_WORKFLOWS.filter( (w) => w.assetName === "workbench-digest", ), }), @@ -1294,7 +1303,7 @@ describe("seedTenant", () => { args({ api: fakeAPI(handler), log, - workflows: DEFAULT_WORKFLOWS.filter( + workflows: CATALOG_WORKFLOWS.filter( (w) => w.assetName === "workbench-digest", ), confirmDeployments: false, @@ -1358,16 +1367,23 @@ describe("seedTenant", () => { return undefined; }; + // Deploy the full default set plus the on-demand catalog (which + // still carries workbench-digest's startStopped handshake) so this + // test keeps covering both a plain deploy and the stop-pristine + // path with confirmDeployments off, regardless of which set a real + // signup deploys by default. + const workflows = [...DEFAULT_WORKFLOWS, ...CATALOG_WORKFLOWS]; await seedTenant( args({ api: fakeAPI(handler), pushWorkflow: push, log, confirmDeployments: false, + workflows, }), ); - expect(pushes).toHaveLength(DEFAULT_WORKFLOWS.length); + expect(pushes).toHaveLength(workflows.length); // CL-6462: deploy order is the product decision — the setup agent is // pushed and deployed before any other seeded workflow, so someone // who just connected can start talking while the rest converge. @@ -1375,11 +1391,11 @@ describe("seedTenant", () => { `/assets/workflow/${SETUP_AGENT_ASSET_NAME}.git`, ); const output = lines.join("\n"); - for (const workflow of DEFAULT_WORKFLOWS) { + for (const workflow of workflows) { expect(output).not.toContain(`confirmed workflow ${workflow.assetName}`); } expect(output).toContain( - `seed complete: ${DEFAULT_WORKFLOWS.length} workflow(s) deployed`, + `seed complete: ${workflows.length} workflow(s) deployed`, ); expect(output).not.toContain("deployed and confirmed"); expect(output).toContain( @@ -1509,7 +1525,7 @@ describe("default skills seeding", () => { return workflowRoutes(method, path); }; - const echoOnly = DEFAULT_WORKFLOWS.filter((w) => w.assetName === "echo"); + const echoOnly = CATALOG_WORKFLOWS.filter((w) => w.assetName === "echo"); await seedTenant( args({ api: fakeAPI(handler), @@ -1545,7 +1561,7 @@ describe("default skills seeding", () => { return workflowRoutes(method, path); }; - const echoOnly = DEFAULT_WORKFLOWS.filter((w) => w.assetName === "echo"); + const echoOnly = CATALOG_WORKFLOWS.filter((w) => w.assetName === "echo"); await seedTenant( args({ api: fakeAPI(handler), @@ -1590,7 +1606,7 @@ describe("default skills seeding", () => { return workflowRoutes(method, path); }; - const echoOnly = DEFAULT_WORKFLOWS.filter((w) => w.assetName === "echo"); + const echoOnly = CATALOG_WORKFLOWS.filter((w) => w.assetName === "echo"); await seedTenant( args({ api: fakeAPI(handler), diff --git a/packages/seeding/test/tool-registry-coverage.test.ts b/packages/seeding/test/tool-registry-coverage.test.ts index 3edec62125..8bb9481752 100644 --- a/packages/seeding/test/tool-registry-coverage.test.ts +++ b/packages/seeding/test/tool-registry-coverage.test.ts @@ -13,6 +13,7 @@ import { describe, expect, test } from "bun:test"; import { CORBITS_TOOL_PACKAGE_DIRS } from "@corbits/tool-registry-publish"; import { CATALOG_TEST_WORKFLOWS, + CATALOG_WORKFLOWS, DEFAULT_WORKFLOWS, type DefaultWorkflow, } from "../src/seed"; @@ -85,7 +86,11 @@ function pinsFor(workflows: readonly DefaultWorkflow[]): { describe("corbits-tools registry coverage", () => { test("every @corbits pin among the workflows seedTenant deploys is published by CORBITS_TOOL_PACKAGE_DIRS", () => { const published = publishedCorbitsPackageNames(); - const deployed = pinsFor([...DEFAULT_WORKFLOWS, ...CATALOG_TEST_WORKFLOWS]); + const deployed = pinsFor([ + ...DEFAULT_WORKFLOWS, + ...CATALOG_WORKFLOWS, + ...CATALOG_TEST_WORKFLOWS, + ]); const missing = deployed.filter(({ pin }) => !published.has(pin.name)); if (missing.length > 0) { @@ -118,9 +123,11 @@ describe("corbits-tools registry coverage", () => { test("catalog-wide (informational): unpublished @corbits pins outside the deployed set are exactly the known, tracked gap", () => { const published = publishedCorbitsPackageNames(); const deployedAssetNames = new Set( - [...DEFAULT_WORKFLOWS, ...CATALOG_TEST_WORKFLOWS].map( - (workflow) => workflow.assetName, - ), + [ + ...DEFAULT_WORKFLOWS, + ...CATALOG_WORKFLOWS, + ...CATALOG_TEST_WORKFLOWS, + ].map((workflow) => workflow.assetName), ); const workflowsDir = path.join(REPO_ROOT, "workflows"); const pinPattern = /name:\s*"(@corbits\/[a-zA-Z0-9_-]+)"/g; diff --git a/scripts/e2e/local-rip.test.ts b/scripts/e2e/local-rip.test.ts index 46e01f915f..371cff7c2b 100644 --- a/scripts/e2e/local-rip.test.ts +++ b/scripts/e2e/local-rip.test.ts @@ -55,6 +55,7 @@ import { describe, expect, test } from "bun:test"; import { resetSchema, setupDatabase } from "../db-setup.ts"; import { + CATALOG_WORKFLOWS, createGitWorkflowPusher, DEFAULT_WORKFLOWS, isLiveDeploymentStatus, @@ -423,10 +424,11 @@ describe.skipIf(databaseUrl === undefined)( ); await hop( - "every default workflow — echo, workbench-digest, and assistant — deploys and goes live", + "the default workflow (assistant) plus the on-demand catalog (echo, workbench-digest) deploy and go live (CL-7074: only assistant is seeded automatically; the rest deploy here via the same seeding-library path a real on-demand deploy would use)", async () => { - await deploySeededWorkflows(DEFAULT_WORKFLOWS); - for (const workflow of DEFAULT_WORKFLOWS) { + const workflows = [...DEFAULT_WORKFLOWS, ...CATALOG_WORKFLOWS]; + await deploySeededWorkflows(workflows); + for (const workflow of workflows) { const assetsRes = await api( hub.baseUrl, "GET", diff --git a/workflows/heartbeat/README.md b/workflows/heartbeat/README.md index 727d6da831..deb44d44b7 100644 --- a/workflows/heartbeat/README.md +++ b/workflows/heartbeat/README.md @@ -48,5 +48,7 @@ const json = serializeHeartbeatWorkflow(definition); ``` See [`workflows/README.md`](../README.md#status-note) for what -registration/automatable/seeded mean — this one is seeded by default for -every tenant, same as `workbench-digest`. +registration/automatable/seeded mean — this one is a +`CATALOG_TEST_WORKFLOWS` entry, kept for tests only, never seeded and +never deployed onto a real bench. `workbench-digest` is not seeded either; +it is deployable through the catalog instantiate route (CL-7073). diff --git a/workflows/morning-brief/README.md b/workflows/morning-brief/README.md index 7244af5788..07a7cb5788 100644 --- a/workflows/morning-brief/README.md +++ b/workflows/morning-brief/README.md @@ -143,4 +143,5 @@ markdown brief. See [`workflows/README.md`](../README.md#status-note) for what registration/automatable/seeded mean — this one is `automatable: true` but not seeded, since it needs real Granola/Linear credentials to be -useful, unlike `workbench-digest`. +useful. `workbench-digest` is not seeded either; both are deployable +through the catalog instantiate route (CL-7073). diff --git a/workflows/workbench-digest/README.md b/workflows/workbench-digest/README.md index 866d9cec00..044a25aecc 100644 --- a/workflows/workbench-digest/README.md +++ b/workflows/workbench-digest/README.md @@ -50,5 +50,6 @@ const json = serializeWorkbenchDigestWorkflow(definition); ``` See [`workflows/README.md`](../README.md#status-note) for what -registration/automatable/seeded mean — this one is seeded by default for -every tenant. +registration/automatable/seeded mean — this one is `automatable`, and +deployable through the catalog instantiate route (CL-7073) from +`CATALOG_WORKFLOWS` (CL-7074), not seeded by default onto every tenant.