From d01e503a663d764188280a5343f6b0a4e49e70f6 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 15:40:00 -0700 Subject: [PATCH 1/2] Delete the shipped workflow catalog; Myra creates workflows dynamically Owner ruling (2026-09-17, LESS IS MORE): the goal is to launch Myra (agents/assistant) and make her strong with the tool packages and MCP; everything else she creates dynamically. Delete every seeded workflow package except workflows/echo, kept only as the e2e smoke fixture, and trim @corbits/workflows/catalog's WORKFLOW_CATALOG to echo plus Myra. --- PRODUCT.md | 9 +- apps/hub/package.json | 11 - bun.lock | 242 +----------- .../src/connections-pinned-by.test.ts | 30 -- .../settings-ui/src/connections-pinned-by.ts | 25 +- packages/workflows/src/catalog.test.ts | 147 +------- packages/workflows/src/catalog.ts | 251 +------------ scripts/e2e/heartbeat.test.ts | 290 --------------- scripts/e2e/smoke-webhook.test.ts | 26 +- scripts/e2e/workbench-digest.test.ts | 306 --------------- tsconfig.build.json | 39 -- workflows/README.md | 6 + workflows/attio-task-agent/LICENSE | 176 --------- workflows/attio-task-agent/README.md | 90 ----- workflows/attio-task-agent/package.json | 49 --- .../src/artifact-client.test.ts | 59 --- .../attio-task-agent/src/artifact-client.ts | 62 ---- .../src/finalize-tool.test.ts | 161 -------- .../attio-task-agent/src/finalize-tool.ts | 157 -------- workflows/attio-task-agent/src/index.ts | 203 ---------- .../attio-task-agent/src/prompts.test.ts | 67 ---- workflows/attio-task-agent/src/prompts.ts | 89 ----- .../attio-task-agent/test/boundary.test.ts | 51 --- .../attio-task-agent/test/definition.test.ts | 110 ------ workflows/attio-task-agent/tsconfig.json | 15 - workflows/attio-task-agent/tsconfig.src.json | 12 - workflows/code-review/LICENSE | 176 --------- workflows/code-review/README.md | 83 ----- workflows/code-review/package.json | 49 --- workflows/code-review/src/index.ts | 193 ---------- workflows/code-review/test/definition.test.ts | 74 ---- workflows/code-review/tsconfig.json | 15 - workflows/code-review/tsconfig.src.json | 12 - workflows/collateral-generation/LICENSE | 176 --------- workflows/collateral-generation/README.md | 145 -------- workflows/collateral-generation/package.json | 49 --- .../src/artifact-client.test.ts | 59 --- .../src/artifact-client.ts | 62 ---- .../src/finalize-tool.test.ts | 237 ------------ .../src/finalize-tool.ts | 191 ---------- workflows/collateral-generation/src/index.ts | 351 ------------------ .../test/boundary.test.ts | 52 --- .../test/definition.test.ts | 156 -------- workflows/collateral-generation/tsconfig.json | 15 - .../collateral-generation/tsconfig.src.json | 12 - workflows/credential-expiry/LICENSE | 176 --------- workflows/credential-expiry/README.md | 71 ---- workflows/credential-expiry/package.json | 48 --- workflows/credential-expiry/src/client.ts | 105 ------ .../credential-expiry/src/decide.test.ts | 85 ----- workflows/credential-expiry/src/decide.ts | 53 --- workflows/credential-expiry/src/index.ts | 162 -------- workflows/credential-expiry/src/tool.ts | 83 ----- workflows/credential-expiry/tsconfig.json | 15 - workflows/credential-expiry/tsconfig.src.json | 12 - workflows/exa-topic-watch/LICENSE | 176 --------- workflows/exa-topic-watch/README.md | 78 ---- workflows/exa-topic-watch/package.json | 49 --- .../src/artifact-client.test.ts | 59 --- .../exa-topic-watch/src/artifact-client.ts | 62 ---- .../exa-topic-watch/src/finalize-tool.test.ts | 194 ---------- .../exa-topic-watch/src/finalize-tool.ts | 149 -------- workflows/exa-topic-watch/src/index.ts | 210 ----------- .../exa-topic-watch/test/boundary.test.ts | 51 --- .../exa-topic-watch/test/definition.test.ts | 128 ------- workflows/exa-topic-watch/tsconfig.json | 15 - workflows/exa-topic-watch/tsconfig.src.json | 12 - workflows/granola-call/LICENSE | 176 --------- workflows/granola-call/README.md | 124 ------- workflows/granola-call/package.json | 49 --- .../granola-call/src/artifact-client.test.ts | 59 --- workflows/granola-call/src/artifact-client.ts | 62 ---- .../granola-call/src/finalize-tool.test.ts | 144 ------- workflows/granola-call/src/finalize-tool.ts | 163 -------- workflows/granola-call/src/index.ts | 206 ---------- workflows/granola-call/test/boundary.test.ts | 49 --- .../granola-call/test/definition.test.ts | 136 ------- workflows/granola-call/tsconfig.json | 15 - workflows/granola-call/tsconfig.src.json | 12 - workflows/heartbeat/LICENSE | 176 --------- workflows/heartbeat/README.md | 72 ---- workflows/heartbeat/package.json | 45 --- workflows/heartbeat/src/index.ts | 140 ------- workflows/heartbeat/test/boundary.test.ts | 49 --- workflows/heartbeat/test/definition.test.ts | 95 ----- workflows/heartbeat/tsconfig.json | 15 - workflows/heartbeat/tsconfig.src.json | 12 - workflows/last-30-days-research/LICENSE | 176 --------- workflows/last-30-days-research/README.md | 209 ----------- workflows/last-30-days-research/package.json | 48 --- .../src/artifact-client.test.ts | 59 --- .../src/artifact-client.ts | 62 ---- .../src/finalize-tool.test.ts | 200 ---------- .../src/finalize-tool.ts | 162 -------- workflows/last-30-days-research/src/index.ts | 264 ------------- .../last-30-days-research/src/prompts.test.ts | 106 ------ .../last-30-days-research/src/prompts.ts | 97 ----- .../test/boundary.test.ts | 52 --- .../test/definition.test.ts | 168 --------- workflows/last-30-days-research/tsconfig.json | 15 - .../last-30-days-research/tsconfig.src.json | 12 - workflows/morning-brief/LICENSE | 176 --------- workflows/morning-brief/README.md | 168 --------- workflows/morning-brief/package.json | 49 --- .../morning-brief/src/artifact-client.test.ts | 59 --- .../morning-brief/src/artifact-client.ts | 62 ---- .../morning-brief/src/finalize-tool.test.ts | 198 ---------- workflows/morning-brief/src/finalize-tool.ts | 162 -------- workflows/morning-brief/src/index.ts | 262 ------------- workflows/morning-brief/test/boundary.test.ts | 52 --- .../morning-brief/test/definition.test.ts | 178 --------- .../morning-brief/test/deploy-wiring.test.ts | 30 -- workflows/morning-brief/tsconfig.json | 15 - workflows/morning-brief/tsconfig.src.json | 12 - workflows/pain-point-collateral/LICENSE | 176 --------- workflows/pain-point-collateral/README.md | 170 --------- workflows/pain-point-collateral/package.json | 49 --- .../src/artifact-client.test.ts | 59 --- .../src/artifact-client.ts | 62 ---- .../src/finalize-tool.test.ts | 203 ---------- .../src/finalize-tool.ts | 170 --------- workflows/pain-point-collateral/src/index.ts | 238 ------------ .../src/intake-tool.test.ts | 128 ------- .../pain-point-collateral/src/intake-tool.ts | 102 ----- .../test/boundary.test.ts | 49 --- .../test/definition.test.ts | 127 ------- workflows/pain-point-collateral/tsconfig.json | 15 - .../pain-point-collateral/tsconfig.src.json | 12 - workflows/process-granola-call/LICENSE | 176 --------- workflows/process-granola-call/README.md | 108 ------ workflows/process-granola-call/package.json | 49 --- .../src/artifact-client.test.ts | 59 --- .../src/artifact-client.ts | 62 ---- .../src/finalize-tool.test.ts | 234 ------------ .../process-granola-call/src/finalize-tool.ts | 205 ---------- workflows/process-granola-call/src/index.ts | 217 ----------- .../test/boundary.test.ts | 49 --- .../test/definition.test.ts | 144 ------- workflows/process-granola-call/tsconfig.json | 15 - .../process-granola-call/tsconfig.src.json | 12 - workflows/reddit-opportunity-scanner/LICENSE | 176 --------- .../reddit-opportunity-scanner/README.md | 179 --------- .../reddit-opportunity-scanner/package.json | 48 --- .../src/artifact-client.test.ts | 63 ---- .../src/artifact-client.ts | 66 ---- .../src/finalize-tool.test.ts | 339 ----------------- .../src/finalize-tool.ts | 350 ----------------- .../reddit-opportunity-scanner/src/index.ts | 263 ------------- .../test/boundary.test.ts | 51 --- .../test/definition.test.ts | 119 ------ .../reddit-opportunity-scanner/tsconfig.json | 15 - .../tsconfig.src.json | 12 - workflows/workbench-digest/LICENSE | 176 --------- workflows/workbench-digest/README.md | 76 ---- workflows/workbench-digest/package.json | 45 --- workflows/workbench-digest/src/index.ts | 134 ------- .../workbench-digest/test/boundary.test.ts | 49 --- .../workbench-digest/test/definition.test.ts | 101 ----- workflows/workbench-digest/tsconfig.json | 15 - workflows/workbench-digest/tsconfig.src.json | 12 - 160 files changed, 55 insertions(+), 16759 deletions(-) delete mode 100644 scripts/e2e/heartbeat.test.ts delete mode 100644 scripts/e2e/workbench-digest.test.ts delete mode 100644 workflows/attio-task-agent/LICENSE delete mode 100644 workflows/attio-task-agent/README.md delete mode 100644 workflows/attio-task-agent/package.json delete mode 100644 workflows/attio-task-agent/src/artifact-client.test.ts delete mode 100644 workflows/attio-task-agent/src/artifact-client.ts delete mode 100644 workflows/attio-task-agent/src/finalize-tool.test.ts delete mode 100644 workflows/attio-task-agent/src/finalize-tool.ts delete mode 100644 workflows/attio-task-agent/src/index.ts delete mode 100644 workflows/attio-task-agent/src/prompts.test.ts delete mode 100644 workflows/attio-task-agent/src/prompts.ts delete mode 100644 workflows/attio-task-agent/test/boundary.test.ts delete mode 100644 workflows/attio-task-agent/test/definition.test.ts delete mode 100644 workflows/attio-task-agent/tsconfig.json delete mode 100644 workflows/attio-task-agent/tsconfig.src.json delete mode 100644 workflows/code-review/LICENSE delete mode 100644 workflows/code-review/README.md delete mode 100644 workflows/code-review/package.json delete mode 100644 workflows/code-review/src/index.ts delete mode 100644 workflows/code-review/test/definition.test.ts delete mode 100644 workflows/code-review/tsconfig.json delete mode 100644 workflows/code-review/tsconfig.src.json delete mode 100644 workflows/collateral-generation/LICENSE delete mode 100644 workflows/collateral-generation/README.md delete mode 100644 workflows/collateral-generation/package.json delete mode 100644 workflows/collateral-generation/src/artifact-client.test.ts delete mode 100644 workflows/collateral-generation/src/artifact-client.ts delete mode 100644 workflows/collateral-generation/src/finalize-tool.test.ts delete mode 100644 workflows/collateral-generation/src/finalize-tool.ts delete mode 100644 workflows/collateral-generation/src/index.ts delete mode 100644 workflows/collateral-generation/test/boundary.test.ts delete mode 100644 workflows/collateral-generation/test/definition.test.ts delete mode 100644 workflows/collateral-generation/tsconfig.json delete mode 100644 workflows/collateral-generation/tsconfig.src.json delete mode 100644 workflows/credential-expiry/LICENSE delete mode 100644 workflows/credential-expiry/README.md delete mode 100644 workflows/credential-expiry/package.json delete mode 100644 workflows/credential-expiry/src/client.ts delete mode 100644 workflows/credential-expiry/src/decide.test.ts delete mode 100644 workflows/credential-expiry/src/decide.ts delete mode 100644 workflows/credential-expiry/src/index.ts delete mode 100644 workflows/credential-expiry/src/tool.ts delete mode 100644 workflows/credential-expiry/tsconfig.json delete mode 100644 workflows/credential-expiry/tsconfig.src.json delete mode 100644 workflows/exa-topic-watch/LICENSE delete mode 100644 workflows/exa-topic-watch/README.md delete mode 100644 workflows/exa-topic-watch/package.json delete mode 100644 workflows/exa-topic-watch/src/artifact-client.test.ts delete mode 100644 workflows/exa-topic-watch/src/artifact-client.ts delete mode 100644 workflows/exa-topic-watch/src/finalize-tool.test.ts delete mode 100644 workflows/exa-topic-watch/src/finalize-tool.ts delete mode 100644 workflows/exa-topic-watch/src/index.ts delete mode 100644 workflows/exa-topic-watch/test/boundary.test.ts delete mode 100644 workflows/exa-topic-watch/test/definition.test.ts delete mode 100644 workflows/exa-topic-watch/tsconfig.json delete mode 100644 workflows/exa-topic-watch/tsconfig.src.json delete mode 100644 workflows/granola-call/LICENSE delete mode 100644 workflows/granola-call/README.md delete mode 100644 workflows/granola-call/package.json delete mode 100644 workflows/granola-call/src/artifact-client.test.ts delete mode 100644 workflows/granola-call/src/artifact-client.ts delete mode 100644 workflows/granola-call/src/finalize-tool.test.ts delete mode 100644 workflows/granola-call/src/finalize-tool.ts delete mode 100644 workflows/granola-call/src/index.ts delete mode 100644 workflows/granola-call/test/boundary.test.ts delete mode 100644 workflows/granola-call/test/definition.test.ts delete mode 100644 workflows/granola-call/tsconfig.json delete mode 100644 workflows/granola-call/tsconfig.src.json delete mode 100644 workflows/heartbeat/LICENSE delete mode 100644 workflows/heartbeat/README.md delete mode 100644 workflows/heartbeat/package.json delete mode 100644 workflows/heartbeat/src/index.ts delete mode 100644 workflows/heartbeat/test/boundary.test.ts delete mode 100644 workflows/heartbeat/test/definition.test.ts delete mode 100644 workflows/heartbeat/tsconfig.json delete mode 100644 workflows/heartbeat/tsconfig.src.json delete mode 100644 workflows/last-30-days-research/LICENSE delete mode 100644 workflows/last-30-days-research/README.md delete mode 100644 workflows/last-30-days-research/package.json delete mode 100644 workflows/last-30-days-research/src/artifact-client.test.ts delete mode 100644 workflows/last-30-days-research/src/artifact-client.ts delete mode 100644 workflows/last-30-days-research/src/finalize-tool.test.ts delete mode 100644 workflows/last-30-days-research/src/finalize-tool.ts delete mode 100644 workflows/last-30-days-research/src/index.ts delete mode 100644 workflows/last-30-days-research/src/prompts.test.ts delete mode 100644 workflows/last-30-days-research/src/prompts.ts delete mode 100644 workflows/last-30-days-research/test/boundary.test.ts delete mode 100644 workflows/last-30-days-research/test/definition.test.ts delete mode 100644 workflows/last-30-days-research/tsconfig.json delete mode 100644 workflows/last-30-days-research/tsconfig.src.json delete mode 100644 workflows/morning-brief/LICENSE delete mode 100644 workflows/morning-brief/README.md delete mode 100644 workflows/morning-brief/package.json delete mode 100644 workflows/morning-brief/src/artifact-client.test.ts delete mode 100644 workflows/morning-brief/src/artifact-client.ts delete mode 100644 workflows/morning-brief/src/finalize-tool.test.ts delete mode 100644 workflows/morning-brief/src/finalize-tool.ts delete mode 100644 workflows/morning-brief/src/index.ts delete mode 100644 workflows/morning-brief/test/boundary.test.ts delete mode 100644 workflows/morning-brief/test/definition.test.ts delete mode 100644 workflows/morning-brief/test/deploy-wiring.test.ts delete mode 100644 workflows/morning-brief/tsconfig.json delete mode 100644 workflows/morning-brief/tsconfig.src.json delete mode 100644 workflows/pain-point-collateral/LICENSE delete mode 100644 workflows/pain-point-collateral/README.md delete mode 100644 workflows/pain-point-collateral/package.json delete mode 100644 workflows/pain-point-collateral/src/artifact-client.test.ts delete mode 100644 workflows/pain-point-collateral/src/artifact-client.ts delete mode 100644 workflows/pain-point-collateral/src/finalize-tool.test.ts delete mode 100644 workflows/pain-point-collateral/src/finalize-tool.ts delete mode 100644 workflows/pain-point-collateral/src/index.ts delete mode 100644 workflows/pain-point-collateral/src/intake-tool.test.ts delete mode 100644 workflows/pain-point-collateral/src/intake-tool.ts delete mode 100644 workflows/pain-point-collateral/test/boundary.test.ts delete mode 100644 workflows/pain-point-collateral/test/definition.test.ts delete mode 100644 workflows/pain-point-collateral/tsconfig.json delete mode 100644 workflows/pain-point-collateral/tsconfig.src.json delete mode 100644 workflows/process-granola-call/LICENSE delete mode 100644 workflows/process-granola-call/README.md delete mode 100644 workflows/process-granola-call/package.json delete mode 100644 workflows/process-granola-call/src/artifact-client.test.ts delete mode 100644 workflows/process-granola-call/src/artifact-client.ts delete mode 100644 workflows/process-granola-call/src/finalize-tool.test.ts delete mode 100644 workflows/process-granola-call/src/finalize-tool.ts delete mode 100644 workflows/process-granola-call/src/index.ts delete mode 100644 workflows/process-granola-call/test/boundary.test.ts delete mode 100644 workflows/process-granola-call/test/definition.test.ts delete mode 100644 workflows/process-granola-call/tsconfig.json delete mode 100644 workflows/process-granola-call/tsconfig.src.json delete mode 100644 workflows/reddit-opportunity-scanner/LICENSE delete mode 100644 workflows/reddit-opportunity-scanner/README.md delete mode 100644 workflows/reddit-opportunity-scanner/package.json delete mode 100644 workflows/reddit-opportunity-scanner/src/artifact-client.test.ts delete mode 100644 workflows/reddit-opportunity-scanner/src/artifact-client.ts delete mode 100644 workflows/reddit-opportunity-scanner/src/finalize-tool.test.ts delete mode 100644 workflows/reddit-opportunity-scanner/src/finalize-tool.ts delete mode 100644 workflows/reddit-opportunity-scanner/src/index.ts delete mode 100644 workflows/reddit-opportunity-scanner/test/boundary.test.ts delete mode 100644 workflows/reddit-opportunity-scanner/test/definition.test.ts delete mode 100644 workflows/reddit-opportunity-scanner/tsconfig.json delete mode 100644 workflows/reddit-opportunity-scanner/tsconfig.src.json delete mode 100644 workflows/workbench-digest/LICENSE delete mode 100644 workflows/workbench-digest/README.md delete mode 100644 workflows/workbench-digest/package.json delete mode 100644 workflows/workbench-digest/src/index.ts delete mode 100644 workflows/workbench-digest/test/boundary.test.ts delete mode 100644 workflows/workbench-digest/test/definition.test.ts delete mode 100644 workflows/workbench-digest/tsconfig.json delete mode 100644 workflows/workbench-digest/tsconfig.src.json diff --git a/PRODUCT.md b/PRODUCT.md index d4a2d7753b..7bda7bf583 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -193,10 +193,11 @@ frozen projection carries a native `ScheduleTrigger`. Cadence is set from inside conversation, not from a separate scheduling console: a person names what should happen again, on what schedule, and where the result should land. See `@corbits/workflows` for the schedule/cron -helpers and the hub's `workflow-scheduler.ts` for the poller. The -Routines page also offers an Available section (CL-7073): every -catalog workflow this workbench hasn't added yet, with an Add action -that deploys it in place — no separate create flow. +helpers and the hub's `workflow-scheduler.ts` for the poller. Owner +ruling (2026-09-17, LESS IS MORE): the shipped workflow catalog is +deleted — `WORKFLOW_CATALOG` now carries only Myra and `echo` (the +e2e smoke fixture), and every other workflow a workbench runs is one +Myra creates dynamically for that workbench's own needs. ## Inbox and approvals diff --git a/apps/hub/package.json b/apps/hub/package.json index aed984cf1d..7ce05f3488 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -17,10 +17,7 @@ "@corbits/approvals": "workspace:*", "@corbits/artifact-ui": "workspace:*", "@corbits/artifacts": "github:corbitsdev/corbits-artifacts#5309a15d4aa504f132c54b4da08bb672b4903873", - "@corbits/attio-task-agent-workflow": "workspace:*", - "@corbits/code-review-workflow": "workspace:*", "@corbits/codex-provider": "workspace:*", - "@corbits/collateral-generation-workflow": "workspace:*", "@corbits/connections": "workspace:*", "@corbits/credential-providers": "workspace:*", "@corbits/cron": "workspace:*", @@ -28,26 +25,18 @@ "@corbits/e2b-sandbox-sidecar": "workspace:*", "@corbits/echo-workflow": "workspace:*", "@corbits/error-sink": "workspace:*", - "@corbits/exa-topic-watch-workflow": "workspace:*", - "@corbits/granola-call-workflow": "workspace:*", "@corbits/hub-api-client": "workspace:*", "@corbits/inbox": "workspace:*", "@corbits/inference-catalog": "workspace:*", - "@corbits/last-30-days-research-workflow": "workspace:*", "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38", "@corbits/mcp-tools": "workspace:*", "@corbits/memory": "github:corbitsdev/corbits-memory#e74da20f148a302dff5400915fe504ee2395e913", - "@corbits/morning-brief-workflow": "workspace:*", "@corbits/notify": "workspace:*", - "@corbits/pain-point-collateral-workflow": "workspace:*", - "@corbits/process-granola-call-workflow": "workspace:*", "@corbits/process-provisioner": "workspace:*", - "@corbits/reddit-opportunity-scanner-workflow": "workspace:*", "@corbits/skills": "workspace:*", "@corbits/tool-registry-publish": "workspace:*", "@corbits/url-path": "workspace:*", "@corbits/webhook-triggers": "workspace:*", - "@corbits/workbench-digest-workflow": "workspace:*", "@corbits/workflows": "workspace:*", "@corbits/xai-provider": "workspace:*", "@intx/authz": "0.3.0", diff --git a/bun.lock b/bun.lock index bdeb06c584..a578e8ebf5 100644 --- a/bun.lock +++ b/bun.lock @@ -59,10 +59,7 @@ "@corbits/approvals": "workspace:*", "@corbits/artifact-ui": "workspace:*", "@corbits/artifacts": "github:corbitsdev/corbits-artifacts#5309a15d4aa504f132c54b4da08bb672b4903873", - "@corbits/attio-task-agent-workflow": "workspace:*", - "@corbits/code-review-workflow": "workspace:*", "@corbits/codex-provider": "workspace:*", - "@corbits/collateral-generation-workflow": "workspace:*", "@corbits/connections": "workspace:*", "@corbits/credential-providers": "workspace:*", "@corbits/cron": "workspace:*", @@ -70,26 +67,18 @@ "@corbits/e2b-sandbox-sidecar": "workspace:*", "@corbits/echo-workflow": "workspace:*", "@corbits/error-sink": "workspace:*", - "@corbits/exa-topic-watch-workflow": "workspace:*", - "@corbits/granola-call-workflow": "workspace:*", "@corbits/hub-api-client": "workspace:*", "@corbits/inbox": "workspace:*", "@corbits/inference-catalog": "workspace:*", - "@corbits/last-30-days-research-workflow": "workspace:*", "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38", "@corbits/mcp-tools": "workspace:*", "@corbits/memory": "github:corbitsdev/corbits-memory#e74da20f148a302dff5400915fe504ee2395e913", - "@corbits/morning-brief-workflow": "workspace:*", "@corbits/notify": "workspace:*", - "@corbits/pain-point-collateral-workflow": "workspace:*", - "@corbits/process-granola-call-workflow": "workspace:*", "@corbits/process-provisioner": "workspace:*", - "@corbits/reddit-opportunity-scanner-workflow": "workspace:*", "@corbits/skills": "workspace:*", "@corbits/tool-registry-publish": "workspace:*", "@corbits/url-path": "workspace:*", "@corbits/webhook-triggers": "workspace:*", - "@corbits/workbench-digest-workflow": "workspace:*", "@corbits/workflows": "workspace:*", "@corbits/xai-provider": "workspace:*", "@intx/authz": "0.3.0", @@ -1507,69 +1496,6 @@ "typescript": "catalog:", }, }, - "workflows/attio-task-agent": { - "name": "@corbits/attio-task-agent-workflow", - "version": "0.0.1", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0", - }, - }, - "workflows/code-review": { - "name": "@corbits/code-review-workflow", - "version": "0.0.1", - "dependencies": { - "@corbits/code-review": "0.0.2", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0", - }, - }, - "workflows/collateral-generation": { - "name": "@corbits/collateral-generation-workflow", - "version": "0.0.1", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0", - }, - }, - "workflows/credential-expiry": { - "name": "@corbits/credential-expiry-workflow", - "version": "0.0.1", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/workflow": "0.3.0", - }, - }, "workflows/echo": { "name": "@corbits/echo-workflow", "version": "0.0.1", @@ -1582,140 +1508,6 @@ "@intx/workflow": "0.3.0", }, }, - "workflows/exa-topic-watch": { - "name": "@corbits/exa-topic-watch-workflow", - "version": "0.0.1", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0", - }, - }, - "workflows/granola-call": { - "name": "@corbits/granola-call-workflow", - "version": "0.0.1", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0", - }, - }, - "workflows/heartbeat": { - "name": "@corbits/heartbeat-workflow", - "version": "0.0.1", - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/workflow": "0.3.0", - }, - }, - "workflows/last-30-days-research": { - "name": "@corbits/last-30-days-research-workflow", - "version": "0.0.1", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/workflow": "0.3.0", - }, - }, - "workflows/morning-brief": { - "name": "@corbits/morning-brief-workflow", - "version": "0.0.1", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0", - }, - }, - "workflows/pain-point-collateral": { - "name": "@corbits/pain-point-collateral-workflow", - "version": "0.0.1", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0", - }, - }, - "workflows/process-granola-call": { - "name": "@corbits/process-granola-call-workflow", - "version": "0.0.1", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0", - }, - }, - "workflows/reddit-opportunity-scanner": { - "name": "@corbits/reddit-opportunity-scanner-workflow", - "version": "0.0.1", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/workflow": "0.3.0", - }, - }, - "workflows/workbench-digest": { - "name": "@corbits/workbench-digest-workflow", - "version": "0.0.1", - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/workflow": "0.3.0", - }, - }, }, "trustedDependencies": [ "@corbits/react-ui", @@ -1855,8 +1647,6 @@ "@corbits/assistant-workflow": ["@corbits/assistant-workflow@workspace:agents/assistant"], - "@corbits/attio-task-agent-workflow": ["@corbits/attio-task-agent-workflow@workspace:workflows/attio-task-agent"], - "@corbits/bench-ui": ["@corbits/bench-ui@workspace:packages/bench-ui"], "@corbits/capability-tools": ["@corbits/capability-tools@workspace:tools/capability"], @@ -1871,12 +1661,8 @@ "@corbits/code-review": ["@corbits/code-review@workspace:packages/code-review"], - "@corbits/code-review-workflow": ["@corbits/code-review-workflow@workspace:workflows/code-review"], - "@corbits/codex-provider": ["@corbits/codex-provider@workspace:packages/codex-provider"], - "@corbits/collateral-generation-workflow": ["@corbits/collateral-generation-workflow@workspace:workflows/collateral-generation"], - "@corbits/collections": ["@corbits/collections@workspace:packages/collections"], "@corbits/command-palette": ["@corbits/command-palette@workspace:packages/command-palette"], @@ -1889,8 +1675,6 @@ "@corbits/context-menu": ["@corbits/context-menu@workspace:packages/context-menu"], - "@corbits/credential-expiry-workflow": ["@corbits/credential-expiry-workflow@workspace:workflows/credential-expiry"], - "@corbits/credential-providers": ["@corbits/credential-providers@workspace:packages/credential-providers"], "@corbits/cron": ["@corbits/cron@workspace:packages/cron"], @@ -1905,18 +1689,12 @@ "@corbits/evals": ["@corbits/evals@workspace:packages/evals"], - "@corbits/exa-topic-watch-workflow": ["@corbits/exa-topic-watch-workflow@workspace:workflows/exa-topic-watch"], - "@corbits/github-tools": ["@corbits/github-tools@workspace:tools/github"], "@corbits/gotenberg-render": ["@corbits/gotenberg-render@workspace:packages/gotenberg-render"], - "@corbits/granola-call-workflow": ["@corbits/granola-call-workflow@workspace:workflows/granola-call"], - "@corbits/granola-tools": ["@corbits/granola-tools@workspace:tools/granola"], - "@corbits/heartbeat-workflow": ["@corbits/heartbeat-workflow@workspace:workflows/heartbeat"], - "@corbits/hub-api-client": ["@corbits/hub-api-client@workspace:packages/hub-api-client"], "@corbits/icons": ["@corbits/icons@workspace:packages/icons"], @@ -1929,8 +1707,6 @@ "@corbits/interaction-tools": ["@corbits/interaction-tools@workspace:tools/interaction"], - "@corbits/last-30-days-research-workflow": ["@corbits/last-30-days-research-workflow@workspace:workflows/last-30-days-research"], - "@corbits/linear-tools": ["@corbits/linear-tools@workspace:tools/linear"], "@corbits/mailbox": ["@corbits/mailbox@github:corbitsdev/corbits-mailbox#e85b81e", { "dependencies": { "@hono/standard-validator": "0.2.3", "@intx/crypto": "0.3.0", "@standard-community/standard-json": "0.3.5", "@standard-community/standard-openapi": "0.2.9", "arktype": "2.1.29", "hono-openapi": "1.3.1" }, "peerDependencies": { "@intx/log": "^0.2.2", "drizzle-orm": "^0.45.2", "hono": "^4.12.0", "postgres": "^3.4.0" } }, "corbitsdev-corbits-mailbox-e85b81e", "sha512-ATPP/pll1sYG1DLKV1HvtboK8q1tFkqayQqRXdUmRUIsFJx5wqzd8YA1XELjXzLu1P2f2uCMe2CjsJHGv6E90Q=="], @@ -1945,8 +1721,6 @@ "@corbits/mocks": ["@corbits/mocks@workspace:packages/mocks"], - "@corbits/morning-brief-workflow": ["@corbits/morning-brief-workflow@workspace:workflows/morning-brief"], - "@corbits/notify": ["@corbits/notify@workspace:packages/notify"], "@corbits/oauth-core": ["@corbits/oauth-core@github:corbitsdev/corbits-oauth-core#e1e69e6", { "dependencies": { "arktype": "2.2.3" } }, "corbitsdev-corbits-oauth-core-e1e69e6", "sha512-wUrD73iVyk/Dtb4yRn3hCh6N8syfsEvAkRz4XYqT0FIB3sYDSMYEWHcDMJx9nq64my/HBOeN1J6PHIOtpjhtPg=="], @@ -1955,20 +1729,14 @@ "@corbits/openai-responses": ["@corbits/openai-responses@github:corbitsdev/corbits-openai-responses#adcde69", { "dependencies": { "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-openai-responses-adcde69", "sha512-AuWgVdjKlNgFu0/9/iw5ruI8UEzd2ZfGlMSlTHrd7ln07VoDV0IO+Njg0LFoENIwdjA6pQYMYTW2DjMXmblEPg=="], - "@corbits/pain-point-collateral-workflow": ["@corbits/pain-point-collateral-workflow@workspace:workflows/pain-point-collateral"], - "@corbits/plugins-ui": ["@corbits/plugins-ui@workspace:packages/plugins-ui"], - "@corbits/process-granola-call-workflow": ["@corbits/process-granola-call-workflow@workspace:workflows/process-granola-call"], - "@corbits/process-provisioner": ["@corbits/process-provisioner@workspace:packages/process-provisioner"], "@corbits/provider-pricing": ["@corbits/provider-pricing@workspace:packages/provider-pricing"], "@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - "@corbits/reddit-opportunity-scanner-workflow": ["@corbits/reddit-opportunity-scanner-workflow@workspace:workflows/reddit-opportunity-scanner"], - "@corbits/reddit-tools": ["@corbits/reddit-tools@workspace:tools/reddit"], "@corbits/sandbox-sidecar": ["@corbits/sandbox-sidecar@workspace:packages/sandbox-sidecar"], @@ -1997,8 +1765,6 @@ "@corbits/webhook-triggers": ["@corbits/webhook-triggers@workspace:packages/webhook-triggers"], - "@corbits/workbench-digest-workflow": ["@corbits/workbench-digest-workflow@workspace:workflows/workbench-digest"], - "@corbits/workflow-authoring-tools": ["@corbits/workflow-authoring-tools@workspace:tools/workflow-authoring"], "@corbits/workflows": ["@corbits/workflows@workspace:packages/workflows"], @@ -3175,14 +2941,12 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@corbits/artifact-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], + "@corbits/inbox/@corbits/mailbox": ["@corbits/mailbox@github:corbitsdev/corbits-mailbox#e85b81e", { "dependencies": { "@hono/standard-validator": "0.2.3", "@intx/crypto": "0.3.0", "@standard-community/standard-json": "0.3.5", "@standard-community/standard-openapi": "0.2.9", "arktype": "2.1.29", "hono-openapi": "1.3.1" }, "peerDependencies": { "@intx/log": "^0.2.2", "drizzle-orm": "^0.45.2", "hono": "^4.12.0", "postgres": "^3.4.0" } }, "corbitsdev-corbits-mailbox-e85b81e", "sha512-ATPP/pll1sYG1DLKV1HvtboK8q1tFkqayQqRXdUmRUIsFJx5wqzd8YA1XELjXzLu1P2f2uCMe2CjsJHGv6E90Q=="], "@corbits/mailbox/hono-openapi": ["hono-openapi@1.3.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.11.2", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-NLVeVkhKZ3drmQNEIPac8HX8Y54uf1hJAgIM/7MfDsaeVVmB+QILWQxx5x3R3NvRHgedcbEbOCGY2uR7WQYyMw=="], - "@corbits/plugins-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/settings-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - "@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=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], @@ -3205,8 +2969,6 @@ "@workbench/sidecar/@corbits/oauth-core": ["@corbits/oauth-core@github:corbitsdev/corbits-oauth-core#e1e69e6", { "dependencies": { "arktype": "2.2.3" } }, "corbitsdev-corbits-oauth-core-e1e69e6", "sha512-wUrD73iVyk/Dtb4yRn3hCh6N8syfsEvAkRz4XYqT0FIB3sYDSMYEWHcDMJx9nq64my/HBOeN1J6PHIOtpjhtPg=="], - "@workbench/web/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - "better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], diff --git a/packages/settings-ui/src/connections-pinned-by.test.ts b/packages/settings-ui/src/connections-pinned-by.test.ts index d7f0cb31e9..203b4541ac 100644 --- a/packages/settings-ui/src/connections-pinned-by.test.ts +++ b/packages/settings-ui/src/connections-pinned-by.test.ts @@ -20,36 +20,6 @@ describe("connections pinned-by", () => { } }); - test("granola pins every Granola-backed GTM workflow, not just the call-notes pair", () => { - expect(CONNECTOR_PINNED_WORKFLOWS.granola).toEqual( - expect.arrayContaining([ - "granola-call", - "process-granola-call", - "morning-brief", - "pain-point-collateral", - "collateral-generation", - ]), - ); - }); - - test("linear pins morning-brief and collateral-generation", () => { - expect(CONNECTOR_PINNED_WORKFLOWS.linear).toEqual( - expect.arrayContaining(["morning-brief", "collateral-generation"]), - ); - }); - - test("exa pins every web-search workflow", () => { - expect(CONNECTOR_PINNED_WORKFLOWS.exa).toEqual(["last-30-days-research", "exa-topic-watch"]); - }); - - test("attio pins the CRM task agent", () => { - expect(CONNECTOR_PINNED_WORKFLOWS.attio).toEqual(["attio-task-agent"]); - }); - - test("scrapecreators pins reddit-opportunity-scanner", () => { - expect(CONNECTOR_PINNED_WORKFLOWS.scrapecreators).toEqual(["reddit-opportunity-scanner"]); - }); - test("manus pins the assistant", () => { expect(CONNECTOR_PINNED_WORKFLOWS.manus).toEqual(["assistant"]); }); diff --git a/packages/settings-ui/src/connections-pinned-by.ts b/packages/settings-ui/src/connections-pinned-by.ts index a57587711d..c0f1daa436 100644 --- a/packages/settings-ui/src/connections-pinned-by.ts +++ b/packages/settings-ui/src/connections-pinned-by.ts @@ -1,24 +1,15 @@ // Which deployed workflow asset names pin a given connector's tool package, -// per CL-5999's toolPackagePins mechanism (see workflows/granola-call/src/index.ts -// and siblings, each exporting a *_TOOL_PACKAGE_PINS const). This is a -// hand-maintained approximation, not a live query against deployed -// definitions — the settings-ui card labels it "Used by workflows:" for -// readability, but carries the approximation caveat on a title/tooltip +// per CL-5999's toolPackagePins mechanism. This is a hand-maintained +// approximation, not a live query against deployed definitions — the +// settings-ui card labels it "Used by workflows:" for readability, but +// carries the approximation caveat on a title/tooltip // (connectionsPinnedByApproximationNote) rather than baking it into the // label itself, until CL-6028's item 9 (tool-package credential-binding // adoption) makes it exact. +// +// The shipped workflow catalog was deleted (owner ruling 2026-09-17): +// Myra creates workflows dynamically, so no connector is pinned by a +// seeded workflow asset name any more. export const CONNECTOR_PINNED_WORKFLOWS: Readonly> = { - granola: [ - "granola-call", - "process-granola-call", - "morning-brief", - "pain-point-collateral", - "collateral-generation", - ], - linear: ["morning-brief", "collateral-generation"], - exa: ["last-30-days-research", "exa-topic-watch"], - attio: ["attio-task-agent"], - scrapecreators: ["reddit-opportunity-scanner"], - github: ["code-review"], manus: ["assistant"], }; diff --git a/packages/workflows/src/catalog.test.ts b/packages/workflows/src/catalog.test.ts index aab32de7b6..998c9e251c 100644 --- a/packages/workflows/src/catalog.test.ts +++ b/packages/workflows/src/catalog.test.ts @@ -1,9 +1,6 @@ import { type } from "arktype"; import { describe, expect, test } from "bun:test"; -import { CONNECTOR_REGISTRY } from "@corbits/connections/registry"; -import { MCP_PRESETS } from "@corbits/connections/mcp-presets"; - import { deliveryWorkbenchRequiredForWorkflowName, isAutomatableWorkflowName, @@ -16,46 +13,11 @@ import { } 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); + test("marks neither echo nor assistant automatable", () => { 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); @@ -63,9 +25,6 @@ describe("workflow catalog", () => { }); 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"); }); @@ -90,12 +49,8 @@ describe("workflow catalog", () => { expect(isConversationalWorkflowName("assistant")).toBe(true); }); - test("marks every workflow utility, automatable or not, non-conversational", () => { + test("marks echo, the mail-triggered wiring check, 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", () => { @@ -103,16 +58,14 @@ describe("workflow catalog", () => { 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 + // CL-6649: a non-automatable utility (echo) is still 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. + // workflow 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); - } + expect(isAutomatableWorkflowName("echo")).toBe(false); + expect(isConversationalWorkflowName("echo")).toBe(false); }); test("every catalog entry declares a conversational flag", () => { @@ -170,43 +123,10 @@ describe("workflow catalog", () => { } }); - 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", () => { @@ -229,46 +149,9 @@ describe("workflow catalog", () => { } }); - 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(); + test("echo and assistant take no human-supplied trigger content", () => { + expect(workflowCatalogEntry("echo")?.triggerFields).toBeUndefined(); + expect(workflowCatalogEntry("assistant")?.triggerFields).toBeUndefined(); }); }); @@ -294,14 +177,6 @@ describe("workflow catalog", () => { 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: " ", diff --git a/packages/workflows/src/catalog.ts b/packages/workflows/src/catalog.ts index e512d11355..8877c0c8dc 100644 --- a/packages/workflows/src/catalog.ts +++ b/packages/workflows/src/catalog.ts @@ -17,19 +17,7 @@ import { type } from "arktype"; import echoPkg from "../../../workflows/echo/package.json"; -import codeReviewPkg from "../../../workflows/code-review/package.json"; import assistantPkg from "../../../agents/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 exaTopicWatchPkg from "../../../workflows/exa-topic-watch/package.json"; -import attioTaskAgentPkg from "../../../workflows/attio-task-agent/package.json"; const CorbitsWorkflowBlock = type({ assetName: "string > 0", @@ -53,13 +41,10 @@ function workflowBlock(pkg: { /** * 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 + * source of truth for what a workflow's trigger actually expects (see a * 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. + * this mirrors). `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]*$/", @@ -84,13 +69,10 @@ export type WorkflowCatalogEntry = { /** * 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`. + * input is its declared trigger contract. `automatable` alone can't + * tell these apart: `echo` is a non-automatable on-demand run, not a + * conversational agent. Only the seeded `assistant`/Myra definition + * is `true`. */ readonly conversational: boolean; /** @@ -145,26 +127,6 @@ export const WORKFLOW_CATALOG: readonly WorkflowCatalogEntry[] = [ 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, @@ -175,199 +137,6 @@ export const WORKFLOW_CATALOG: readonly WorkflowCatalogEntry[] = [ 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(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) { @@ -450,9 +219,9 @@ export type TriggerFieldsValidation = * 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, + * rejection — a workflow with a required trigger field can still be + * created with it 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 diff --git a/scripts/e2e/heartbeat.test.ts b/scripts/e2e/heartbeat.test.ts deleted file mode 100644 index 9410544f97..0000000000 --- a/scripts/e2e/heartbeat.test.ts +++ /dev/null @@ -1,290 +0,0 @@ -// A light end-to-end smoke test for the heartbeat workflow: the real -// hub and sidecar as spawned processes against a real Postgres, a -// heartbeat deployment whose inference source is the hub's own -// `noop-inference` endpoint (not a placeholder, not a real provider), -// and a trigger that starts a run. -// -// This is the proof-by-construction that heartbeat costs nothing to -// run frequently: the deploy's source is a real, reachable endpoint -// (unlike the walking skeleton's `https://inference.invalid` -// placeholder), so a run started against it actually resolves its -// inference call — against `noop-inference`'s constant, locally -// served reply, never a real model. - -import { describe, test } from "bun:test"; - -import { resetSchema, setupDatabase } from "../db-setup.ts"; -import { - HEARTBEAT_STEP_ID, - buildHeartbeatWorkflow, - serializeHeartbeatWorkflow, -} from "../../workflows/heartbeat/src/index.ts"; -import { ensureNoopCatalogOffering, startNoopInferenceServer } from "./noop-inference-server.ts"; -import { - api, - createCleanupHarness, - e2eDatabaseUrl, - expectStatus, - expectStepCompleted, - freePort, - hop, - startHub, - waitForRunCompletion, - type ApiResult, - type HubHandle, -} from "./harness.ts"; -import { pushWorkflowSource, workflowDeployBody } from "./workflow-source.ts"; - -const databaseUrl = e2eDatabaseUrl(); -if (databaseUrl === undefined) { - console.warn( - "heartbeat: DATABASE_URL is not set; suite skipped. " + - "Set DATABASE_URL (see .env.example) to run it; " + - "start Postgres with `docker compose -f compose.test.yml up -d`.", - ); -} - -function stringField(data: unknown, field: string, what: string): string { - if (typeof data === "object" && data !== null && field in data) { - const value = (data as Record)[field]; - if (typeof value === "string" && value !== "") return value; - } - throw new Error(`${what}: missing string field "${field}": ${JSON.stringify(data)}`); -} - -function runIds(data: unknown): string[] { - if ( - typeof data === "object" && - data !== null && - "runIds" in data && - Array.isArray((data as Record)["runIds"]) - ) { - return (data as { runIds: unknown[] }).runIds.filter( - (id): id is string => typeof id === "string", - ); - } - throw new Error(`expected a runIds array: ${JSON.stringify(data)}`); -} - -const { tempDir, track } = createCleanupHarness(); - -describe.skipIf(databaseUrl === undefined)("heartbeat workflow", () => { - // Previously skipped (CL-6004): the first mail trigger against a freshly - // deployed workflow deterministically failed to complete, with the hub - // logging repeated `Workflow-run pack rejected ... source address has no - // live deployment anchor` / `path_violation` warnings for the run's own - // workflow-run repo and the deployment left durably stuck. Both halves of - // that shape are now fixed in the vendored tree (see - // `docs/revendor-inventory.md`): `receiveWorkflowRunPack` no longer gates - // pack acceptance on a live run status, so a settling run can still land the - // bookkeeping that retires its in-flight mail; and the supervisor's turn-2 - // resume path drops mail with no conversation text instead of delivering an - // empty string that throws inside `agent.send` and fails the step with - // `retriesExhausted`. - test("launching heartbeat against the hub's own noop-inference endpoint starts a run", async () => { - const url = databaseUrl; - if (url === undefined) throw new Error("unreachable: suite is skipped"); - - await hop("database setup", async () => { - await resetSchema(url); - await setupDatabase(url); - }); - - const hub: HubHandle = await hop("hub boot", async () => { - const handle = await startHub({ - databaseUrl: url, - port: freePort(), - sessionSecret: Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex"), - dataDir: await tempDir("e2e-heartbeat-hub-data-"), - }); - track(handle); - return handle; - }); - - const user = await hop("sign-up", async () => { - const res = await api(hub.baseUrl, "POST", "/api/auth/sign-up/email", { - name: "Heartbeat Tester", - email: `heartbeat-${crypto.randomUUID()}@example.invalid`, - password: `pw-${crypto.randomUUID()}`, - }); - expectStatus("sign-up", res, 200); - if (res.cookies.length === 0) { - throw new Error("sign-up returned no session cookie"); - } - return res; - }); - - const slug = `e2ehb${crypto.randomUUID().slice(0, 8)}`; - const tenantId = await hop("tenant creation", async () => { - const res = await api( - hub.baseUrl, - "POST", - "/api/tenants", - { name: "Heartbeat Smoke", slug }, - user.cookies, - ); - expectStatus("create tenant", res, 201); - return stringField(res.data, "id", "create tenant"); - }); - - const assetName = "heartbeat"; - const { assetId, commitSha } = await hop("workflow asset publication", async () => { - const created = await api( - hub.baseUrl, - "POST", - `/api/tenants/${tenantId}/assets`, - { kind: "workflow", name: assetName }, - user.cookies, - ); - expectStatus("create workflow asset", created, 201); - const id = stringField(created.data, "id", "create workflow asset"); - - const minted = await api( - hub.baseUrl, - "POST", - `/api/tenants/${tenantId}/git-tokens`, - { - name: "e2e-heartbeat-push", - resource: "asset:*", - refPattern: "**", - actions: ["can_read", "can_push"], - expiresAt: new Date(Date.now() + 10 * 60_000).toISOString(), - }, - user.cookies, - ); - expectStatus("mint git token", minted, 201); - - const definition = buildHeartbeatWorkflow({ - triggerAddress: `heartbeat@${slug}.localhost`, - inferencePreferences: [{ provider: "anthropic", model: "noop" }], - turnTimeoutMs: 30_000, - }); - const pushed = await pushWorkflowSource({ - baseUrl: hub.baseUrl, - tenantId, - assetName, - tokenSecret: stringField(minted.data, "secret", "mint git token"), - workflowJson: serializeHeartbeatWorkflow(definition), - }); - return { assetId: id, commitSha: pushed.commitSha }; - }); - - // The deploy's source is this suite's own, really-reachable noop - // inference server — not a placeholder like the walking skeleton's - // `https://inference.invalid`, and never the hub (CL-8160 dropped - // its noop-inference mount). That distinction is the whole point of - // this suite: a run started against this source actually completes - // an inference call, at zero cost, because the noop server answers - // it locally without reaching a real model. - const noopServer = startNoopInferenceServer(); - track({ - label: "noop-inference-server", - output: () => "", - exited: () => false, - stop: async () => noopServer.stop(), - }); - const offeringId = await hop("noop catalog seeding", () => - ensureNoopCatalogOffering( - (method, path, body, cookies) => api(hub.baseUrl, method, path, body, cookies), - user.cookies, - tenantId, - noopServer.baseUrl, - () => {}, - ), - ); - - const deploymentId = await hop("workflow deploy", async () => { - const body = workflowDeployBody({ - assetId, - commitSha, - sourceOfferingIds: [offeringId], - defaultSourceOfferingId: offeringId, - }); - const deadline = Date.now() + 60_000; - let res: ApiResult; - for (;;) { - if (hub.exited()) { - throw new Error(`hub exited before deploy; output:\n${hub.output()}`); - } - res = await api( - hub.baseUrl, - "POST", - `/api/tenants/${tenantId}/workflows/deployments`, - body, - user.cookies, - ); - if (res.status !== 502) break; - if (Date.now() > deadline) { - throw new Error( - `sidecar never became deployable (hub kept answering 502): ` + - `${JSON.stringify(res.data)}\nhub output:\n${hub.output()}`, - ); - } - await Bun.sleep(200); - } - expectStatus("deploy heartbeat workflow", res, 201); - return stringField(res.data, "id", "deploy heartbeat workflow"); - }); - - const startedRunId = await hop("heartbeat run starts against noop-inference", async () => { - const before = new Set( - runIds( - ( - await api( - hub.baseUrl, - "GET", - `/api/tenants/${tenantId}/workflows/${deploymentId}/runs`, - undefined, - user.cookies, - ) - ).data, - ), - ); - - const triggered = await api( - hub.baseUrl, - "POST", - `/api/tenants/${tenantId}/workflows/${deploymentId}/mail`, - { content: "heartbeat" }, - user.cookies, - ); - expectStatus("trigger heartbeat mail", triggered, 202); - - const deadline = Date.now() + 30_000; - for (;;) { - const listed = await api( - hub.baseUrl, - "GET", - `/api/tenants/${tenantId}/workflows/${deploymentId}/runs`, - undefined, - user.cookies, - ); - const started = runIds(listed.data).find((id) => !before.has(id)); - if (started !== undefined) return started; - if (Date.now() > deadline) { - throw new Error("heartbeat trigger was accepted but no run started within 30s"); - } - await Bun.sleep(200); - } - }); - - // The real gate: a run id proves only that the mail route accepted - // the trigger. Whether the deployment actually resolves — the - // step's agent launching, its turn completing against - // noop-inference, the run reaching a terminal state — is only - // proven by the run's own event log. A broken agent launch or a - // rejected inference call surfaces here as RunFailed (or no - // terminal event at all), failing this loudly instead of a - // "started" run standing in for a working platform. - const events = await hop("heartbeat run completes", () => - waitForRunCompletion(hub.baseUrl, tenantId, deploymentId, startedRunId, user.cookies, 30_000), - ); - expectStepCompleted(events, HEARTBEAT_STEP_ID); - - console.log( - "heartbeat: gate achieved: a run completed against the real, " + - "reachable noop-inference source, proving the deployment " + - "resolves at zero cost.", - ); - }, 180_000); -}); diff --git a/scripts/e2e/smoke-webhook.test.ts b/scripts/e2e/smoke-webhook.test.ts index 5c1e557ee2..057b87f9fb 100644 --- a/scripts/e2e/smoke-webhook.test.ts +++ b/scripts/e2e/smoke-webhook.test.ts @@ -1,8 +1,8 @@ // Smoke scenario 5/5 (CL-6004): webhook trigger. A workflow is bound to // a `@corbits/webhook-triggers` row instead of a cadence; a correctly // HMAC-signed payload delivered to the public ingress route launches a -// run. Zero-cost like `scripts/e2e/workbench-digest.test.ts`: the -// deployed workflow's only inference source is this suite's own local +// run. Zero-cost: the deployed workflow's only inference source is this +// suite's own local // noop inference server (`./noop-inference-server.ts`), so nothing here // ever calls a real model provider. // @@ -24,9 +24,9 @@ import { describe, expect, test } from "bun:test"; import { resetSchema, setupDatabase } from "../db-setup.ts"; import { - buildHeartbeatWorkflow, - serializeHeartbeatWorkflow, -} from "../../workflows/heartbeat/src/index.ts"; + buildEchoWorkflow, + serializeEchoWorkflow, +} from "../../workflows/echo/src/index.ts"; import { ensureNoopCatalogOffering, startNoopInferenceServer } from "./noop-inference-server.ts"; import { publishCorbitsToolsRegistry } from "../../packages/tool-registry-publish/src/publish.ts"; import { createHubAPI } from "../../packages/hub-api-client/src/index.ts"; @@ -132,7 +132,7 @@ describe.skipIf(databaseUrl === undefined)("smoke: webhook trigger", () => { ), ); - const assetName = "heartbeat"; + const assetName = "echo"; const { assetId, commitSha } = await hop("workflow asset publication", async () => { const created = await api( hub.baseUrl, @@ -159,8 +159,8 @@ describe.skipIf(databaseUrl === undefined)("smoke: webhook trigger", () => { ); expectStatus("mint git token", minted, 201); - const definition = buildHeartbeatWorkflow({ - triggerAddress: `heartbeat@${slug}.localhost`, + const definition = buildEchoWorkflow({ + triggerAddress: `echo@${slug}.localhost`, inferencePreferences: [{ provider: "anthropic", model: "noop" }], turnTimeoutMs: 30_000, }); @@ -169,7 +169,7 @@ describe.skipIf(databaseUrl === undefined)("smoke: webhook trigger", () => { tenantId, assetName, tokenSecret: stringField(minted.data, "secret", "mint git token"), - workflowJson: serializeHeartbeatWorkflow(definition), + workflowJson: serializeEchoWorkflow(definition), }); return { assetId: id, commitSha: pushed.commitSha }; }); @@ -226,7 +226,7 @@ describe.skipIf(databaseUrl === undefined)("smoke: webhook trigger", () => { await Bun.sleep(200); continue; } - expectStatus("deploy heartbeat workflow", res, 201); + expectStatus("deploy echo workflow", res, 201); break; } @@ -242,15 +242,15 @@ describe.skipIf(databaseUrl === undefined)("smoke: webhook trigger", () => { typeof listed.data === "object" && listed.data !== null && "data" in listed.data ? (listed.data as { data: unknown[] }).data : (listed.data as unknown[]); - const heartbeat = (rows as { id: string; name?: string }[]).find( + const echo = (rows as { id: string; name?: string }[]).find( (row) => row.name === assetName, ); - if (heartbeat === undefined) { + if (echo === undefined) { throw new Error( `no workflow definition named "${assetName}": ${JSON.stringify(listed.data)}`, ); } - return heartbeat.id; + return echo.id; }); const { triggerId, secret } = await hop("webhook trigger creation", async () => { diff --git a/scripts/e2e/workbench-digest.test.ts b/scripts/e2e/workbench-digest.test.ts deleted file mode 100644 index dc38312dfa..0000000000 --- a/scripts/e2e/workbench-digest.test.ts +++ /dev/null @@ -1,306 +0,0 @@ -// A light end-to-end smoke test for the workbench-digest workflow: the -// real hub and sidecar as spawned processes against a real Postgres, a -// workbench-digest deployment whose inference source is this suite's -// own local noop inference server (not a placeholder, not a real -// provider, and never the hub — CL-8160 dropped its noop-inference -// mount), and a trigger that runs the step to completion. -// -// This is the proof-by-construction that workbench-digest costs nothing -// to run frequently: the deploy's source is a real, reachable endpoint -// (unlike the walking skeleton's `https://inference.invalid` -// placeholder), so a run started against it actually resolves its -// inference call — against the noop server's constant, locally served -// reply, never a real model. - -import { describe, test } from "bun:test"; - -import { resetSchema, setupDatabase } from "../db-setup.ts"; -import { - WORKBENCH_DIGEST_STEP_ID, - buildWorkbenchDigestWorkflow, - serializeWorkbenchDigestWorkflow, -} from "../../workflows/workbench-digest/src/index.ts"; -import { ensureNoopCatalogOffering, startNoopInferenceServer } from "./noop-inference-server.ts"; -import { - api, - createCleanupHarness, - e2eDatabaseUrl, - expectStatus, - expectStepCompleted, - freePort, - hop, - startHub, - waitForRunCompletion, - type ApiResult, - type HubHandle, -} from "./harness.ts"; -import { pushWorkflowSource, workflowDeployBody } from "./workflow-source.ts"; - -const databaseUrl = e2eDatabaseUrl(); -if (databaseUrl === undefined) { - console.warn( - "workbench-digest: DATABASE_URL is not set; suite skipped. " + - "Set DATABASE_URL (see .env.example) to run it; " + - "start Postgres with `docker compose -f compose.test.yml up -d`.", - ); -} - -function stringField(data: unknown, field: string, what: string): string { - if (typeof data === "object" && data !== null && field in data) { - const value = (data as Record)[field]; - if (typeof value === "string" && value !== "") return value; - } - throw new Error(`${what}: missing string field "${field}": ${JSON.stringify(data)}`); -} - -function runIds(data: unknown): string[] { - if ( - typeof data === "object" && - data !== null && - "runIds" in data && - Array.isArray((data as Record)["runIds"]) - ) { - return (data as { runIds: unknown[] }).runIds.filter( - (id): id is string => typeof id === "string", - ); - } - throw new Error(`expected a runIds array: ${JSON.stringify(data)}`); -} - -/** - * `waitForRunCompletion` must outlive the step timeout. When both were - * 30s, a turn that ran to its own timeout was reported as "no terminal - * event" (RunStarted + StepStarted only) instead of RunFailed. The step - * timeout stays 30s to match seed and heartbeat; the waiter is longer so - * a 30s step kill is observed as RunFailed. - */ -const DIGEST_TURN_TIMEOUT_MS = 30_000; -const DIGEST_RUN_COMPLETION_TIMEOUT_MS = 60_000; - -const { tempDir, track } = createCleanupHarness(); - -describe.skipIf(databaseUrl === undefined)("workbench-digest workflow", () => { - // Previously skipped (CL-6004): the same upstream defect documented in - // heartbeat.test.ts — the first mail trigger against a freshly deployed - // single-step workflow deterministically failed with RunFailed / "one or - // more steps failed" regardless of step timeout length, confirming the - // defect was systemic to the mail-triggered single-step deploy path in - // vendor/intx/hub-sessions, not heartbeat-specific. Now fixed in the - // vendored tree (see `docs/revendor-inventory.md`). - test("launching workbench-digest against the hub's own noop-inference endpoint completes a run", async () => { - const url = databaseUrl; - if (url === undefined) throw new Error("unreachable: suite is skipped"); - - await hop("database setup", async () => { - await resetSchema(url); - await setupDatabase(url); - }); - - const hub: HubHandle = await hop("hub boot", async () => { - const handle = await startHub({ - databaseUrl: url, - port: freePort(), - sessionSecret: Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex"), - dataDir: await tempDir("e2e-workbench-digest-hub-data-"), - }); - track(handle); - return handle; - }); - - const user = await hop("sign-up", async () => { - const res = await api(hub.baseUrl, "POST", "/api/auth/sign-up/email", { - name: "Workbench Digest Tester", - email: `workbench-digest-${crypto.randomUUID()}@example.invalid`, - password: `pw-${crypto.randomUUID()}`, - }); - expectStatus("sign-up", res, 200); - if (res.cookies.length === 0) { - throw new Error("sign-up returned no session cookie"); - } - return res; - }); - - const slug = `e2ecd${crypto.randomUUID().slice(0, 8)}`; - const tenantId = await hop("tenant creation", async () => { - const res = await api( - hub.baseUrl, - "POST", - "/api/tenants", - { name: "Workbench Digest Smoke", slug }, - user.cookies, - ); - expectStatus("create tenant", res, 201); - return stringField(res.data, "id", "create tenant"); - }); - - const assetName = "workbench-digest"; - const { assetId, commitSha } = await hop("workflow asset publication", async () => { - const created = await api( - hub.baseUrl, - "POST", - `/api/tenants/${tenantId}/assets`, - { kind: "workflow", name: assetName }, - user.cookies, - ); - expectStatus("create workflow asset", created, 201); - const id = stringField(created.data, "id", "create workflow asset"); - - const minted = await api( - hub.baseUrl, - "POST", - `/api/tenants/${tenantId}/git-tokens`, - { - name: "e2e-workbench-digest-push", - resource: "asset:*", - refPattern: "**", - actions: ["can_read", "can_push"], - expiresAt: new Date(Date.now() + 10 * 60_000).toISOString(), - }, - user.cookies, - ); - expectStatus("mint git token", minted, 201); - - const definition = buildWorkbenchDigestWorkflow({ - triggerAddress: `workbench-digest@${slug}.localhost`, - inferencePreferences: [{ provider: "anthropic", model: "noop" }], - turnTimeoutMs: DIGEST_TURN_TIMEOUT_MS, - }); - const pushed = await pushWorkflowSource({ - baseUrl: hub.baseUrl, - tenantId, - assetName, - tokenSecret: stringField(minted.data, "secret", "mint git token"), - workflowJson: serializeWorkbenchDigestWorkflow(definition), - }); - return { assetId: id, commitSha: pushed.commitSha }; - }); - - // The deploy's source is this suite's own, really-reachable noop - // inference server — not a placeholder like the walking skeleton's - // `https://inference.invalid`, and never the hub (CL-8160 dropped - // its noop-inference mount). That distinction is the whole point of - // this suite: a run started against this source actually completes - // an inference call, at zero cost, because the noop server answers - // it locally without reaching a real model. - const noopServer = startNoopInferenceServer(); - track({ - label: "noop-inference-server", - output: () => "", - exited: () => false, - stop: async () => noopServer.stop(), - }); - const offeringId = await hop("noop catalog seeding", () => - ensureNoopCatalogOffering( - (method, path, body, cookies) => api(hub.baseUrl, method, path, body, cookies), - user.cookies, - tenantId, - noopServer.baseUrl, - () => {}, - ), - ); - - const deploymentId = await hop("workflow deploy", async () => { - const body = workflowDeployBody({ - assetId, - commitSha, - sourceOfferingIds: [offeringId], - defaultSourceOfferingId: offeringId, - }); - const deadline = Date.now() + 60_000; - let res: ApiResult; - for (;;) { - if (hub.exited()) { - throw new Error(`hub exited before deploy; output:\n${hub.output()}`); - } - res = await api( - hub.baseUrl, - "POST", - `/api/tenants/${tenantId}/workflows/deployments`, - body, - user.cookies, - ); - if (res.status !== 502) break; - if (Date.now() > deadline) { - throw new Error( - `sidecar never became deployable (hub kept answering 502): ` + - `${JSON.stringify(res.data)}\nhub output:\n${hub.output()}`, - ); - } - await Bun.sleep(200); - } - expectStatus("deploy workbench-digest workflow", res, 201); - return stringField(res.data, "id", "deploy workbench-digest workflow"); - }); - - const startedRunId = await hop( - "workbench-digest run starts against noop-inference", - async () => { - const before = new Set( - runIds( - ( - await api( - hub.baseUrl, - "GET", - `/api/tenants/${tenantId}/workflows/${deploymentId}/runs`, - undefined, - user.cookies, - ) - ).data, - ), - ); - - const triggered = await api( - hub.baseUrl, - "POST", - `/api/tenants/${tenantId}/workflows/${deploymentId}/mail`, - { content: "message count: 0" }, - user.cookies, - ); - expectStatus("trigger workbench-digest mail", triggered, 202); - - const deadline = Date.now() + 30_000; - for (;;) { - const listed = await api( - hub.baseUrl, - "GET", - `/api/tenants/${tenantId}/workflows/${deploymentId}/runs`, - undefined, - user.cookies, - ); - const started = runIds(listed.data).find((id) => !before.has(id)); - if (started !== undefined) return started; - if (Date.now() > deadline) { - throw new Error("workbench-digest trigger was accepted but no run started within 30s"); - } - await Bun.sleep(200); - } - }, - ); - - // The real gate: a run id proves only that the mail route accepted - // the trigger. Whether the deployment actually resolves — the - // step's agent launching, its turn completing against - // noop-inference, the run reaching a terminal state — is only - // proven by the run's own event log. A broken agent launch or a - // rejected inference call surfaces here as RunFailed (or no - // terminal event at all), failing this loudly instead of a - // "started" run standing in for a working platform. - const events = await hop("workbench-digest run completes", () => - waitForRunCompletion( - hub.baseUrl, - tenantId, - deploymentId, - startedRunId, - user.cookies, - DIGEST_RUN_COMPLETION_TIMEOUT_MS, - ), - ); - expectStepCompleted(events, WORKBENCH_DIGEST_STEP_ID); - - console.log( - "workbench-digest: gate achieved: a run completed against the " + - "real, reachable noop-inference source, proving the " + - "deployment resolves at zero cost.", - ); - }, 180_000); -}); diff --git a/tsconfig.build.json b/tsconfig.build.json index 3471eeffbe..b5149f4fe3 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -211,47 +211,8 @@ { "path": "./vendor/intx/workflow-host/tsconfig.src.json" }, - { - "path": "./workflows/attio-task-agent/tsconfig.src.json" - }, - { - "path": "./workflows/code-review/tsconfig.src.json" - }, - { - "path": "./workflows/collateral-generation/tsconfig.src.json" - }, - { - "path": "./workflows/credential-expiry/tsconfig.src.json" - }, { "path": "./workflows/echo/tsconfig.src.json" - }, - { - "path": "./workflows/exa-topic-watch/tsconfig.src.json" - }, - { - "path": "./workflows/granola-call/tsconfig.src.json" - }, - { - "path": "./workflows/heartbeat/tsconfig.src.json" - }, - { - "path": "./workflows/last-30-days-research/tsconfig.src.json" - }, - { - "path": "./workflows/morning-brief/tsconfig.src.json" - }, - { - "path": "./workflows/pain-point-collateral/tsconfig.src.json" - }, - { - "path": "./workflows/process-granola-call/tsconfig.src.json" - }, - { - "path": "./workflows/reddit-opportunity-scanner/tsconfig.src.json" - }, - { - "path": "./workflows/workbench-digest/tsconfig.src.json" } ] } diff --git a/workflows/README.md b/workflows/README.md index d84e68fda5..181d99fe78 100644 --- a/workflows/README.md +++ b/workflows/README.md @@ -1,5 +1,11 @@ # Workflow packages +Owner ruling (2026-09-17, LESS IS MORE): the shipped workflow catalog is +deleted. Myra (`agents/assistant`) creates workflows dynamically for a +tenant's own needs; there is no seeded catalog of ready-made workflows +any more. `workflows/echo` is kept only as the e2e smoke fixture proving +the mail-triggered contract works — it is not a product offering. + Each subdirectory is a deployable workflow package: an Interchange `defineWorkflow` plus a `corbits.workflow` block in its `package.json` (`assetName`, `displayName`, `automatable`). `@corbits/workflows/catalog`'s diff --git a/workflows/attio-task-agent/LICENSE b/workflows/attio-task-agent/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/workflows/attio-task-agent/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/workflows/attio-task-agent/README.md b/workflows/attio-task-agent/README.md deleted file mode 100644 index 8974950b04..0000000000 --- a/workflows/attio-task-agent/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# attio-task-agent - -Works one CRM task from start to finish: reads the record and the -surrounding context, drafts what the task actually needs, and writes back -to Attio only once a human approves. - -Ported from the OG gtm-workbench's `attio-task-agent` (CL-6349). - -## The run - -One reasoning turn with four named phases (`src/prompts.ts`): - -1. **Ground yourself** — `mcp_list_servers`, `mcp_list_tools`, then - `mcp_read` against the connected Attio server, plus any meetings or - web-search server the workbench also has. `mcp_read` refuses any tool - the server does not mark read-only, so "never mutate while gathering" - is enforced by the tool, not only by the prompt. -2. **Decide what this task needs** — at most four pieces, from a fixed - menu of kinds, each with its own quality bar. -3. **Draft and check** — write each piece in full, then read it back - against what the task asked for. -4. **Save the work** — `attio_task_agent_finalize` once per piece - (`approval: "ask"`), persisting it as a Library artifact. -5. **Offer the CRM write-back** — say what would be written and to which - record, and wait. Only on a yes does it go through `mcp_call`, which - is itself `approval: "ask"`. - -## What this port changed, and why - -- **Twenty-one steps folded into one.** Sixteen of the OG's steps were - `action` or `awaitSignal` primitives. This repo's production host leaves - `invokeAction` undefined, and a gate nobody fulfils hangs a run forever. - The four reasoning agents (planner / executor / reviewer / suggest) - became four phases of one prompt; the selection and clarification gates - became the agent asking in the thread, where the person already is. -- **Attio through the MCP preset**, not a bespoke `@workbench/tools-attio` - package — there is no native Attio tool package in this repo, and - `@corbits/mcp-tools` is a pin a deploy here can actually resolve. One - pin covers what the OG needed three tool packages for. -- **The write-back gate is kept, not weakened.** The OG gated on an - explicit `confirm` signal followed by fatal write actions. Here the same - gate is `mcp_call`'s unconditional `approval: "ask"`: the run suspends - and a human approves the exact call before anything reaches the CRM. -- **The `gamma-presentation` draft kind is dropped.** It existed to hand - off to a tool package this deployment does not have, and a kind nothing - can act on is a promise the run cannot keep. - -## Known gaps - -- **No note idempotency.** The OG passed the task id as `writeNote`'s - `idempotencyKey`, so re-running the same task deduped instead of adding - a second note. `mcp_call` has no equivalent, so a re-run after a partial - write-back can create a duplicate note. The approval gate makes this - visible to a human before it happens, but it is not prevented. -- **No ordering guarantee across the two CRM writes.** The OG ordered - `writeNote` before `writeComplete` so a mid-failure left the task - visibly open rather than "done but empty". Folded into one turn, that - ordering is the prompt's intent, not the runtime's guarantee. -- Multi-step `toolPackagePins` resolution is unproven in this repo; this - definition is single-step, so it stays inside what the deploy path has - actually run. - -## Registration - -See [`workflows/README.md`](../README.md#status-note) for what -registration/automatable/seeded mean — this one is `automatable: false` -(it starts from a specific task a person points at, and its write-back -approval is a poor fit for unattended scheduling) and not seeded by -default: it needs an Attio connection not every tenant has. - -## Deploy from the npm registry - -Once published, deploy this workflow definition to a tenant by name and -version pin: - -``` -POST /api/tenants/:id/workflows/deployments -{ - "source": { "kind": "registry", "registry": "npm" }, - "entry": "./src/index.ts", - "pin": "@corbits/attio-task-agent-workflow@0.0.1", - "sourceOfferingIds": ["", ...], - "defaultSourceOfferingId": "" -} -``` - -`entry` is the package's `interchange.workflow` module path; `pin` is -`"@corbits/attio-task-agent-workflow@0.0.1"` or a semver range on the same name. The hub -installs, probes, gates, and freezes the definition from the registry -tarball before creating the deployment. diff --git a/workflows/attio-task-agent/package.json b/workflows/attio-task-agent/package.json deleted file mode 100644 index 88f48a03a6..0000000000 --- a/workflows/attio-task-agent/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@corbits/attio-task-agent-workflow", - "version": "0.0.1", - "description": "Works one Attio task end to end: grounds itself in the CRM and past calls read-only, drafts what the task needs, and writes back to Attio only once a human approves", - "license": "LGPL-2.1-or-later", - "repository": { - "type": "git", - "url": "https://github.com/corbitsdev/workbench.git", - "directory": "workflows/attio-task-agent" - }, - "files": [ - "src", - "LICENSE", - "README.md" - ], - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "arktype": "catalog:" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0" - }, - "corbits": { - "workflow": { - "assetName": "attio-task-agent", - "displayName": "Attio task agent", - "automatable": false - } - }, - "interchange": { - "workflow": "./src/index.ts" - } -} diff --git a/workflows/attio-task-agent/src/artifact-client.test.ts b/workflows/attio-task-agent/src/artifact-client.test.ts deleted file mode 100644 index 1c86414419..0000000000 --- a/workflows/attio-task-agent/src/artifact-client.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { expect, test } from "bun:test"; - -import { createWorkflowArtifact } from "./artifact-client"; - -const CONFIG = { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - runAddress: "run_1@workflow", -}; - -test("posts to the workflow-artifacts endpoint and returns id/version", async () => { - const captured: { url: string; init: RequestInit | undefined } = { - url: "", - init: undefined, - }; - const fetchImpl = (async (input: URL | string, init?: RequestInit) => { - captured.url = String(input); - captured.init = init; - return new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - }); - }) as unknown as typeof fetch; - - const result = await createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ); - - expect(result).toEqual({ id: "art_1", version: 1 }); - expect(captured.url).toBe("https://hub.example.com/api/workflow-artifacts/artifacts"); - const headers = captured.init?.headers as Record; - expect(headers["authorization"]).toBe("Bearer sc-token"); - expect(headers["x-workflow-run-address"]).toBe("run_1@workflow"); -}); - -test("throws on a non-ok HTTP response", async () => { - const fetchImpl = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/500/); -}); - -test("throws when the response doesn't match the expected shape", async () => { - const fetchImpl = (async () => - new Response(JSON.stringify({ data: {} }), { - status: 201, - })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/did not match the expected shape/); -}); diff --git a/workflows/attio-task-agent/src/artifact-client.ts b/workflows/attio-task-agent/src/artifact-client.ts deleted file mode 100644 index b9d56ca425..0000000000 --- a/workflows/attio-task-agent/src/artifact-client.ts +++ /dev/null @@ -1,62 +0,0 @@ -// A minimal client for the sanctioned workflow-artifacts HTTP surface -// (`@corbits/artifacts-hub`'s `createWorkflowArtifactRoutes`, CL-6000). -// Deliberately duplicated from `@corbits/artifact-tools`' `client.ts` -// rather than imported: this package ships as installable data on the -// native workflow contract (see `./index.ts`'s header comment and -// `test/boundary.test.ts`), so its shipped sources import only `@intx/*`, -// `arktype`, and relative specifiers — never another `@corbits/*` -// package. Authenticates with the sidecar's own bearer token plus the -// run's own mailbox address (both already reach a workflow-process -// child's tool env), never a database handle. -import { type } from "arktype"; - -export interface WorkflowArtifactClientConfig { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly runAddress: string; - /** Override for tests; defaults to the global `fetch`. */ - readonly fetchImpl?: typeof fetch; -} - -export type CreateWorkflowArtifactInput = { - readonly title: string; - readonly kind: string; - readonly content: string; -}; - -export type CreatedWorkflowArtifact = { - readonly id: string; - readonly version: number; -}; - -const CreatedWorkflowArtifactResponse = type({ - data: { id: "string", version: "number" }, -}); - -/** Persists one artifact. Throws on any transport, HTTP, or shape failure. */ -export async function createWorkflowArtifact( - config: WorkflowArtifactClientConfig, - input: CreateWorkflowArtifactInput, -): Promise { - const doFetch = config.fetchImpl ?? fetch; - const response = await doFetch(`${config.hubArtifactsUrl}/api/workflow-artifacts/artifacts`, { - method: "POST", - headers: { - authorization: `Bearer ${config.sidecarToken}`, - "x-workflow-run-address": config.runAddress, - "content-type": "application/json", - }, - body: JSON.stringify(input), - }); - if (!response.ok) { - throw new Error(`Workflow artifact create failed: ${response.status} ${response.statusText}`); - } - const body: unknown = await response.json(); - const parsed = CreatedWorkflowArtifactResponse(body); - if (parsed instanceof type.errors) { - throw new Error( - `Workflow artifact create response did not match the expected shape: ${parsed.summary}`, - ); - } - return parsed.data; -} diff --git a/workflows/attio-task-agent/src/finalize-tool.test.ts b/workflows/attio-task-agent/src/finalize-tool.test.ts deleted file mode 100644 index 636d6480d4..0000000000 --- a/workflows/attio-task-agent/src/finalize-tool.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { expect, test } from "bun:test"; - -import { - ATTIO_TASK_AGENT_FINALIZE_TOOL, - ATTIO_TASK_AGENT_FINALIZE_TOOL_NAME, - buildArtifactPayload, - type WorkflowArtifactEnv, -} from "./finalize-tool"; - -test("the tool's definition marks itself approval-gated", () => { - expect(ATTIO_TASK_AGENT_FINALIZE_TOOL.definitions).toEqual([ - { name: ATTIO_TASK_AGENT_FINALIZE_TOOL_NAME, approval: "ask" }, - ]); -}); - -test("the tool is namespaced under this workflow package, not a shared one", () => { - expect(ATTIO_TASK_AGENT_FINALIZE_TOOL.id).toBe("@corbits/workflow-attio-task-agent/finalize"); -}); - -test("requires the sanctioned workflow-artifacts env keys", () => { - expect(ATTIO_TASK_AGENT_FINALIZE_TOOL.requires).toEqual([ - "hubArtifactsUrl", - "sidecarToken", - "address", - ]); -}); - -test("buildArtifactPayload keeps the draft kind readable in the body, not only in the run", () => { - const payload = buildArtifactPayload({ - outcome: "draft", - title: "Intro to Northwind Robotics", - draftKind: "cold-email", - content: "Subject: A faster path to your pilot", - }); - expect(payload).toEqual({ - title: "Intro to Northwind Robotics", - kind: "text", - content: "Kind: cold-email\n\nSubject: A faster path to your pilot", - }); -}); - -test("buildArtifactPayload marks a nothing-to-draft note as status-note, with no kind prefix", () => { - const payload = buildArtifactPayload({ - outcome: "status-note", - title: "Northwind Robotics task — nothing to draft", - draftKind: "task-explanation", - content: "The task was already closed out on the record.", - }); - expect(payload).toEqual({ - title: "Northwind Robotics task — nothing to draft", - kind: "status-note", - content: "The task was already closed out on the record.", - }); -}); - -function testEnv(): WorkflowArtifactEnv { - return { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - address: "run_1@workflow", - } as unknown as WorkflowArtifactEnv; -} - -test("run persists the draft on real invocation (i.e. after approval re-dispatches the call)", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - })) as unknown as typeof fetch; - - try { - const bundle = ATTIO_TASK_AGENT_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: ATTIO_TASK_AGENT_FINALIZE_TOOL_NAME, - arguments: { - outcome: "draft", - title: "Intro to Northwind Robotics", - draftKind: "cold-email", - content: "Subject: A faster path to your pilot", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(String(result.content)) as { - id: string; - kind: string; - persisted: boolean; - }; - expect(parsed).toMatchObject({ - id: "art_1", - kind: "text", - persisted: true, - }); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run returns an honest error result when persistence fails, never fabricating persisted: true", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - try { - const bundle = ATTIO_TASK_AGENT_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: ATTIO_TASK_AGENT_FINALIZE_TOOL_NAME, - arguments: { - outcome: "draft", - title: "Intro to Northwind Robotics", - draftKind: "cold-email", - content: "Subject: A faster path to your pilot", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(true); - expect(result.content).toContain("Failed to save"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run rejects malformed arguments without throwing", async () => { - const bundle = ATTIO_TASK_AGENT_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: ATTIO_TASK_AGENT_FINALIZE_TOOL_NAME, - arguments: {}, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); - -test("run rejects a draft kind the prompt never taught", async () => { - const bundle = ATTIO_TASK_AGENT_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: ATTIO_TASK_AGENT_FINALIZE_TOOL_NAME, - arguments: { - outcome: "draft", - title: "Intro to Northwind Robotics", - draftKind: "gamma-presentation", - content: "Slide 1", - }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); diff --git a/workflows/attio-task-agent/src/finalize-tool.ts b/workflows/attio-task-agent/src/finalize-tool.ts deleted file mode 100644 index 75e41ec5c5..0000000000 --- a/workflows/attio-task-agent/src/finalize-tool.ts +++ /dev/null @@ -1,157 +0,0 @@ -// The one piece of this workflow's *own* surface gated by human -// approval: saving a finished draft to the Library. The other gate — -// writing back to the CRM — is `@corbits/mcp-tools`' `mcp_call`, which -// is `approval: "ask"` unconditionally; this workflow never re-implements -// it (see `./index.ts`'s header comment). -// -// Approval mechanics: `definitions` marks this tool's one definition -// `approval: "ask"`, the platform's native tool-approval gate. Calling it -// suspends the run and creates a real `approval` row; `run` below only -// executes once a human approves. See `pain-point-collateral`'s -// `finalize-tool.ts` for the full suspend/resume account, which applies -// unchanged here. -// -// Persistence: `run` calls `createWorkflowArtifact` (`./artifact-client.ts`, -// duplicated from `@corbits/artifact-tools`' client rather than imported — -// see that file's header for why this installable-data package never -// imports another `@corbits/*` package) against the sanctioned -// workflow-artifacts HTTP surface. - -import { type } from "arktype"; -import { defineTool, type BaseEnv } from "@intx/agent"; -import { createWorkflowArtifact } from "./artifact-client"; -import { ATTIO_TASK_ARTIFACT_KINDS } from "./prompts"; - -export const ATTIO_TASK_AGENT_FINALIZE_TOOL_NAME = "attio_task_agent_finalize"; - -export const ATTIO_TASK_AGENT_FINALIZE_DESCRIPTION = - "Saves one finished draft for this CRM task, pending human approval, and persists it as a Library artifact."; - -/** - * The draft kinds this tool accepts, as an arktype union. Built from the - * one exported list so the prompt's menu and the parser's accepted set - * cannot drift — a kind the guidance never taught is rejected here rather - * than silently persisted under a label nothing recognizes. - */ -const DraftKind = type.enumerated(...ATTIO_TASK_ARTIFACT_KINDS); - -const FinalizeArgs = type({ - /** - * Which of the two shapes this call is: `"draft"` for a finished piece; - * `"status-note"` for a task that genuinely needed no drafting. This - * fixes the persisted artifact's `kind` — the model names the outcome, - * never the storage kind, so a run cannot mislabel a note as real work. - */ - outcome: "'draft'|'status-note'", - /** Short, human-facing title. Doubles as the approve-card headline. */ - title: "string > 0", - /** Which kind of piece this is — one of the kinds the prompt teaches. */ - draftKind: DraftKind, - /** The finished body, or the status note's honest explanation. */ - content: "string > 0", -}); - -export type FinalizeArgs = typeof FinalizeArgs.infer; - -export type ArtifactPayload = { - title: string; - kind: "text" | "status-note"; - content: string; -}; - -/** - * The payload `createWorkflowArtifact` persists. The draft kind leads the - * body rather than becoming the storage `kind`: the Library's kinds are - * `text` / `status-note`, and losing "this is a cold email" would leave a - * reader guessing what they are looking at. - */ -export function buildArtifactPayload(args: FinalizeArgs): ArtifactPayload { - if (args.outcome === "status-note") { - return { title: args.title, kind: "status-note", content: args.content }; - } - return { - title: args.title, - kind: "text", - content: `Kind: ${args.draftKind}\n\n${args.content}`, - }; -} - -/** - * The env this tool needs beyond `BaseEnv`: the sanctioned - * workflow-artifacts credential trio, populated for every workflow step - * (`apps/sidecar/src/workflow-substrate-factory/step-env.ts`). - */ -export interface WorkflowArtifactEnv extends BaseEnv { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly address: string; -} - -export const ATTIO_TASK_AGENT_FINALIZE_TOOL = defineTool({ - id: "@corbits/workflow-attio-task-agent/finalize", - requires: ["hubArtifactsUrl", "sidecarToken", "address"], - definitions: [ - { - name: ATTIO_TASK_AGENT_FINALIZE_TOOL_NAME, - approval: "ask", - }, - ], - factory: (env) => ({ - definitions: [ - { - name: ATTIO_TASK_AGENT_FINALIZE_TOOL_NAME, - description: ATTIO_TASK_AGENT_FINALIZE_DESCRIPTION, - inputSchema: { - type: "object", - properties: { - outcome: { type: "string", enum: ["draft", "status-note"] }, - title: { type: "string" }, - draftKind: { type: "string", enum: [...ATTIO_TASK_ARTIFACT_KINDS] }, - content: { type: "string" }, - }, - required: ["outcome", "title", "draftKind", "content"], - }, - }, - ], - run: async (call) => { - const parsed = FinalizeArgs(call.arguments); - if (parsed instanceof type.errors) { - return { - callId: call.id, - isError: true, - content: `Invalid arguments for ${ATTIO_TASK_AGENT_FINALIZE_TOOL_NAME}: ${parsed.summary}`, - }; - } - const artifact = buildArtifactPayload(parsed); - try { - const created = await createWorkflowArtifact( - { - hubArtifactsUrl: env.hubArtifactsUrl, - sidecarToken: env.sidecarToken, - runAddress: env.address, - }, - artifact, - ); - return { - callId: call.id, - isError: false, - content: JSON.stringify({ - id: created.id, - version: created.version, - title: artifact.title, - kind: artifact.kind, - persisted: true, - }), - }; - } catch (err) { - return { - callId: call.id, - isError: true, - content: `Failed to save "${artifact.title}" to the Library: ${ - err instanceof Error ? err.message : String(err) - }`, - }; - } - }, - }), -}); diff --git a/workflows/attio-task-agent/src/index.ts b/workflows/attio-task-agent/src/index.ts deleted file mode 100644 index b46b6b9bea..0000000000 --- a/workflows/attio-task-agent/src/index.ts +++ /dev/null @@ -1,203 +0,0 @@ -// The attio-task-agent workflow: works one CRM task end to end — -// grounds itself in the record and the surrounding context read-only, -// drafts what the task needs, and writes back to the CRM only once a -// human approves. Ported from the OG gtm-workbench's `attio-task-agent` -// (CL-6349). -// -// ## What the OG pipeline does, step by step -// listMembers -> memberSelectGate -> selectMember -> listTasks -> -// taskSelectGate -> selectTask -> fetchTask -> analyze (a read-only ReAct -// planner) -> clarifyGate -> clarify -> execute (writer) -> -// reviewArtifacts (judge) -> reviewGate -> review -> persist -> suggest -// -> syncApprovalGate -> approveSync -> syncGate -> writeNote -> -// writeComplete, with skipWriteBack as the decline leaf. Sixteen of -// those twenty-one steps were native `action` or `awaitSignal` -// primitives; four were reasoning steps. -// -// ## Adaptations this port makes -// -// (1) Folded to one reasoning step. Every OG `action` dispatches a -// `handler` string an `ActionInvoker` resolves, and this repo's -// production host leaves `invokeAction` undefined -// (`vendor/intx/workflow/src/runtime/run.ts` throws "this host does not -// support action primitives"). Every OG `awaitSignal` gate likewise needs a -// stepper UI to fulfil it, and a gate nobody fulfils hangs a run forever. -// Both collapse into the single reasoning step below: the planner / -// writer / reviewer / suggest phases become four named phases of one -// prompt (`./prompts.ts`), and the selection and clarification gates -// become the agent asking in the thread — the person is already there. -// -// (2) The CRM reaches this deployment through the Attio MCP preset -// (`packages/connections/src/mcp-presets.ts`, slug `attio`, OAuth) and -// `@corbits/mcp-tools`, rather than the OG's bespoke -// `@workbench/tools-attio` package. `@corbits/mcp-tools` is one of the -// few tool packages this repo publishes to its own registry -// (`packages/tool-registry-publish/src/registry.ts`), so a pin on it -// resolves at deploy time. Past calls (Granola) and the live web (Exa) -// come through the same bundle when their presets are connected — a -// connected server the agent can read, not a hard dependency. -// -// (3) The write-back gate is kept, not weakened. The OG gated it on an -// explicit `confirm` signal followed by fatal `attio_create_note` / -// `attio_update_task` actions. Here the same gate is `mcp_call`, which -// `@corbits/mcp-tools` declares `approval: "ask"` unconditionally: the -// run suspends and a human approves the exact call before anything -// reaches the CRM. Reads go through `mcp_read`, which refuses any tool -// the server does not mark read-only — so "never mutate while grounding" -// is enforced by the tool, not only by the prompt. -// -// (4) The OG's `gamma-presentation` draft kind is dropped: it existed to -// hand off to a Gamma tool package this deployment does not have, and a -// kind nothing can act on is a promise the run cannot keep. Every other -// kind is carried over (`./prompts.ts`). -// -// (5) Persistence is `attio_task_agent_finalize` (`approval: "ask"`, -// `./finalize-tool.ts`) rather than the OG's batch persist action — the -// only path from a workflow tool package to the Library engine here, and -// the shape this repo's approval and delivery pipelines already read. -// -// This package is installable data. It imports only published platform -// packages, and nothing imports it statically: a host publishes the -// serialized definition as a workflow asset and deploys it through the -// platform's deploy machinery. - -import type { AgentDefinition, InferencePreference } from "@intx/agent"; -import { defineWorkflow, step } from "@intx/workflow"; -import type { WorkflowDefinition } from "@intx/workflow"; -import type { ToolPackagePin } from "@intx/types/tool-packages"; - -import { ATTIO_TASK_AGENT_FINALIZE_TOOL_NAME } from "./finalize-tool"; -import { buildAttioTaskAgentSystemPrompt } from "./prompts"; - -export const ATTIO_TASK_AGENT_WORKFLOW_ID = "wf_attio_task_agent"; -export const ATTIO_TASK_AGENT_STEP_ID = "attio-task-agent-work"; - -/** The MCP preset slug the CRM is connected under — the same slug - * `packages/connections/src/mcp-presets.ts` registers Attio under, and - * the one the agent passes as `mcp_read` / `mcp_call`'s `server`. */ -export const ATTIO_MCP_SERVER_SLUG = "attio"; - -export const ATTIO_TASK_AGENT_SYSTEM_PROMPT = buildAttioTaskAgentSystemPrompt({ - attioServerSlug: ATTIO_MCP_SERVER_SLUG, - finalizeToolName: ATTIO_TASK_AGENT_FINALIZE_TOOL_NAME, -}); - -/** - * Tool packages this definition pins (CL-5999). `@corbits/mcp-tools` - * carries every connected MCP server — the CRM, and whatever meetings or - * web-search server the workbench also has — so one pin covers all three - * of the OG's separate tool packages. The finalize tool travels with the - * deploy of this package itself. - */ -export const ATTIO_TASK_AGENT_TOOL_PACKAGE_PINS: readonly ToolPackagePin[] = [ - { name: "@corbits/mcp-tools", version: "0.0.12" }, -]; - -/** - * Everything the definition needs that is per-deployment data. The - * trigger address names a specific deployment's inbox — for this - * workflow, the address a person messages to start work on a task. - */ -export interface AttioTaskAgentWorkflowInput { - /** The deployment's mail address; each inbound mail is one run. */ - readonly triggerAddress: string; - /** Provider/model preferences, in order; resolved at deploy time. */ - readonly inferencePreferences: readonly InferencePreference[]; - /** Per-turn timeout in milliseconds, enforced on the single step. */ - readonly turnTimeoutMs: number; -} - -/** - * Builds the attio-task-agent definition: exactly one mail-triggered - * reasoning step whose triggering mail names the task to work. Tools are - * never inlined on the definition — they arrive as pinned packages on the - * deploy, keeping the definition pure data. - */ -export function buildAttioTaskAgentWorkflow( - input: AttioTaskAgentWorkflowInput, -): WorkflowDefinition { - if (input.triggerAddress === "") { - throw new Error("buildAttioTaskAgentWorkflow requires a non-empty triggerAddress"); - } - if (!Number.isInteger(input.turnTimeoutMs) || input.turnTimeoutMs <= 0) { - throw new Error("buildAttioTaskAgentWorkflow requires turnTimeoutMs to be a positive integer"); - } - return defineWorkflow({ - id: ATTIO_TASK_AGENT_WORKFLOW_ID, - trigger: { type: "mail", to: input.triggerAddress }, - steps: { - [ATTIO_TASK_AGENT_STEP_ID]: step({ - agent: { - id: ATTIO_TASK_AGENT_STEP_ID, - description: - "Works one CRM task: grounds itself read-only, drafts what " + - "the task needs, and writes back only once a human approves", - systemPrompt: ATTIO_TASK_AGENT_SYSTEM_PROMPT, - toolFactories: [], - capabilities: [], - inference: { sources: input.inferencePreferences }, - toolPackagePins: ATTIO_TASK_AGENT_TOOL_PACKAGE_PINS, - } satisfies AgentDefinition, - timeout: input.turnTimeoutMs, - }), - }, - }); -} - -/** - * Serializes a definition to the JSON a workflow asset carries. The - * definition must survive the asset round-trip byte-faithfully, so - * anything JSON would silently drop or mangle is a loud error naming the - * offending path instead of a corrupted asset. - */ -export function serializeAttioTaskAgentWorkflow(definition: WorkflowDefinition): string { - assertJsonPortable(definition, "definition"); - return JSON.stringify(definition); -} - -function assertJsonPortable(value: unknown, path: string): void { - if (value === null) return; - switch (typeof value) { - case "string": - case "boolean": - return; - case "number": - if (!Number.isFinite(value)) { - throw new Error(`${path} is a non-finite number; JSON drops it`); - } - return; - case "object": - break; - default: - throw new Error( - `${path} is a ${typeof value}, which does not survive JSON ` + "serialization", - ); - } - if (Array.isArray(value)) { - value.forEach((element, index) => { - assertJsonPortable(element, `${path}[${index}]`); - }); - return; - } - const proto: unknown = Object.getPrototypeOf(value); - if (proto !== Object.prototype && proto !== null) { - throw new Error(`${path} is a non-plain object; JSON would flatten it lossily`); - } - for (const [key, entry] of Object.entries(value)) { - assertJsonPortable(entry, `${path}.${key}`); - } -} - -export { - ATTIO_TASK_ARTIFACT_KINDS, - ARTIFACT_KIND_GUIDANCE, - buildAttioTaskAgentSystemPrompt, -} from "./prompts"; -export type { AttioTaskArtifactKind } from "./prompts"; -export { - ATTIO_TASK_AGENT_FINALIZE_TOOL, - ATTIO_TASK_AGENT_FINALIZE_TOOL_NAME, - ATTIO_TASK_AGENT_FINALIZE_DESCRIPTION, - buildArtifactPayload, -} from "./finalize-tool"; -export type { ArtifactPayload, FinalizeArgs } from "./finalize-tool"; diff --git a/workflows/attio-task-agent/src/prompts.test.ts b/workflows/attio-task-agent/src/prompts.test.ts deleted file mode 100644 index c91578b735..0000000000 --- a/workflows/attio-task-agent/src/prompts.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { expect, test } from "bun:test"; - -import { - ARTIFACT_KIND_GUIDANCE, - ATTIO_TASK_ARTIFACT_KINDS, - buildAttioTaskAgentSystemPrompt, -} from "./prompts"; - -const PROMPT = buildAttioTaskAgentSystemPrompt({ - attioServerSlug: "attio", - finalizeToolName: "attio_task_agent_finalize", -}); - -test("every draft kind the prompt offers has its own quality bar", () => { - for (const kind of ATTIO_TASK_ARTIFACT_KINDS) { - expect(ARTIFACT_KIND_GUIDANCE[kind].length).toBeGreaterThan(0); - expect(PROMPT).toContain(`- ${kind}:`); - } - expect(Object.keys(ARTIFACT_KIND_GUIDANCE).sort()).toEqual([...ATTIO_TASK_ARTIFACT_KINDS].sort()); -}); - -test("the dropped Gamma hand-off is gone, not left as a kind nothing can act on", () => { - expect(ATTIO_TASK_ARTIFACT_KINDS).not.toContain("gamma-presentation" as never); - expect(PROMPT).not.toContain("gamma"); -}); - -test("grounding is read-only: the prompt routes reads through mcp_read and forbids CRM writes while gathering", () => { - expect(PROMPT).toContain("mcp_read"); - expect(PROMPT).toContain("Never write to the CRM while you are grounding"); -}); - -test("the CRM write-back is offered, never assumed, and goes through the approval-gated mcp_call", () => { - expect(PROMPT).toContain("mcp_call"); - expect(PROMPT).toContain("Only if they say yes"); - expect(PROMPT).toContain("nothing was written to the CRM"); -}); - -test("a decline reads as a decision, not an error", () => { - expect(PROMPT).toContain("Never present a decline as an error"); -}); - -test("the prompt threads in the finalize tool name it is given, so the two cannot drift", () => { - const renamed = buildAttioTaskAgentSystemPrompt({ - attioServerSlug: "attio", - finalizeToolName: "some_other_finalize", - }); - expect(renamed).toContain("some_other_finalize"); - expect(renamed).not.toContain("attio_task_agent_finalize"); -}); - -test("the prompt threads in the CRM server slug it is given", () => { - const renamed = buildAttioTaskAgentSystemPrompt({ - attioServerSlug: "crm-staging", - finalizeToolName: "attio_task_agent_finalize", - }); - expect(renamed).toContain('"crm-staging" server'); -}); - -test("a missing task is a question, never a guess", () => { - expect(PROMPT).toContain("do not guess"); -}); - -test("the prompt speaks the reader's language, never the system's internals", () => { - for (const jargon of ["awaitSignal", "ActionInvoker", "toolPackagePins"]) { - expect(PROMPT).not.toContain(jargon); - } -}); diff --git a/workflows/attio-task-agent/src/prompts.ts b/workflows/attio-task-agent/src/prompts.ts deleted file mode 100644 index 75bbe381d7..0000000000 --- a/workflows/attio-task-agent/src/prompts.ts +++ /dev/null @@ -1,89 +0,0 @@ -// The attio-task-agent's prompt material, kept out of `./index.ts` so the -// definition reads as the graph it is. Ported from the OG gtm-workbench -// workflow's `prompts.ts`, which split the same material across four -// agents (planner / executor / reviewer / suggest). This deployment runs -// one folded reasoning turn (see `./index.ts`'s header comment for why), -// so the four prompts become one prompt with four named phases — the -// per-phase instructions themselves are carried over close to verbatim. - -/** - * The kinds of thing this agent knows how to draft. The model picks from - * this list rather than inventing a label, so the guidance below always - * has an entry for whatever it produced, and `./finalize-tool.ts` can - * parse the kind it is handed instead of trusting free text. - */ -export const ATTIO_TASK_ARTIFACT_KINDS = [ - "cold-email", - "follow-up-email", - "twitter-post", - "linkedin-post", - "research-brief", - "task-explanation", - "blog", - "single-page-website", -] as const; - -export type AttioTaskArtifactKind = (typeof ATTIO_TASK_ARTIFACT_KINDS)[number]; - -/** One quality bar per kind — what "done" looks like for that kind. */ -export const ARTIFACT_KIND_GUIDANCE: Record = { - "cold-email": - "A first-touch email to the task's contact. Subject plus body. Open with a specific, researched reason for reaching out, not a generic hook. One clear ask. 90-130 words. Ready to send — no placeholders or [brackets]. Warm, direct, peer-to-peer; no hype.", - "follow-up-email": - "An email continuing a prior thread or meeting. Reference the specific prior context. Add one new piece of value — an insight, a resource, or a next step. Under 100 words. Ready to send, no placeholders.", - "twitter-post": - "One post inspired by the insight behind this task. Anonymized: no company names, personal names, or identifying details. Under 280 characters. One sharp idea; no hashtag spam.", - "linkedin-post": - "One post inspired by the insight behind this task. Anonymized: no company names, personal names, or identifying details — frame it as a universal lesson. 120-200 words, skimmable line breaks, one takeaway, a light prompt for discussion. No emoji as bullets.", - "research-brief": - "A brief on the company or person and the context of this task. Skimmable: who they are, why now, the relevant signals, and two or three angles for engagement. Ground every claim in what you actually gathered and cite the source inline. No speculation presented as fact.", - "task-explanation": - "This task restated with enough context that a teammate could pick it up cold: what the task is, why it matters, the relevant record and history, and the recommended approach. Concise and concrete.", - blog: "A long-form post derived from the task and the research. Strong headline, a hook, three to five sections with subheads, and a closing takeaway. 600-900 words. Authoritative but readable; ground claims in what you gathered.", - "single-page-website": - "The copy and section structure for a one-page site: hero (headline, subhead, call to action), problem, solution, proof, and a closing call to action. Give the copy for each section, labeled. Punchy and benefit-led.", -}; - -function kindGuidanceBlock(): string { - return ATTIO_TASK_ARTIFACT_KINDS.map((kind) => `- ${kind}: ${ARTIFACT_KIND_GUIDANCE[kind]}`).join( - "\n", - ); -} - -/** - * Builds the folded prompt. `finalizeToolName` is threaded in rather than - * imported so the prompt and the tool cannot drift apart silently — the - * definition passes the one exported constant. - */ -export function buildAttioTaskAgentSystemPrompt(opts: { - readonly attioServerSlug: string; - readonly finalizeToolName: string; -}): string { - return [ - "Treat Corbits, Corbits.dev, Interchange, and Faremeter as canonical Corbits names; spell them exactly. When source material contains a clear speech-to-text or spelling variant, use the canonical spelling in your output. Do not replace an ambiguous term unless surrounding context identifies it.", - - "You work one CRM task from start to finish. The message that started you names the task — by id, or by whose task list to look in. If it names neither, do not guess: list the open tasks you can see and ask which one.", - - "## 1. Ground yourself", - `Everything you read comes through connected servers. Call \`mcp_list_servers\` once to see what is connected, then \`mcp_list_tools\` for the ones you plan to use, then \`mcp_read\` for the reads themselves. The CRM is the "${opts.attioServerSlug}" server: read the task, the record it is attached to, and anything linked that helps. Past calls and notes are worth reading when a meetings server is connected, and the live web is worth searching when a search server is connected.`, - "Gather on your own initiative and stop when you have enough. Only come back to the person when a real blocker remains — something only they know. When you do, ask crisp questions and stop there; do not draft around a gap and hope.", - "Never write to the CRM while you are grounding yourself. Reads only.", - - "## 2. Decide what this task needs", - "Pick the pieces this task actually calls for — not a fixed menu, and not everything by default. Some tasks need one draft, some need several, some need none at all. At most four, and prefer fewer: a padded plan dilutes every piece in it.", - "The kinds you can draft, and what each has to clear:", - kindGuidanceBlock(), - - "## 3. Draft and check", - "Write each piece in full — finished and ready to use, with no placeholders or [brackets]. Then read each one back against what the task asked for and fix what does not hold up: off-topic, ungrounded, or not clearing the bar for its kind. Do not hand over a piece you would not send yourself.", - - "## 4. Save the work", - `Call \`${opts.finalizeToolName}\` once per finished piece, with outcome "draft", that piece's kind, a short title, and the full body. Each call requires a human's approval before it completes. If the task needed no drafts at all, call the same tool exactly once with outcome "status-note", a plain title, and content saying what the task was and why nothing needed drafting. Never end a run without at least one finalize call.`, - - "## 5. Offer the CRM write-back, never assume it", - `Attaching a note to the record or marking the task done changes the CRM, so it is the person's call, not yours. Say plainly what you would write and to which record, and wait. Only if they say yes, do it through \`mcp_call\` on the "${opts.attioServerSlug}" server — which asks them to approve the call itself as well. If they decline, or the approval is denied, say in one calm sentence that nothing was written to the CRM. Never present a decline as an error, and never apologize as if something broke.`, - - "## Closing out", - "End with a short summary: what you produced, whether anything was written back, and two to four concrete follow-ups if any are worth doing. Plain text, no code fence.", - ].join("\n\n"); -} diff --git a/workflows/attio-task-agent/test/boundary.test.ts b/workflows/attio-task-agent/test/boundary.test.ts deleted file mode 100644 index 8bd42a25f3..0000000000 --- a/workflows/attio-task-agent/test/boundary.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -// This package is installable data on the native workflow contract: -// its shipped sources import only published platform packages, so the -// source tree plus the package manifest deploys on any Interchange -// instance without a wrapper contract. The tool package this workflow -// expects at deploy time (@corbits/mcp-tools) is pinned by name, never -// imported here — see src/index.ts's header comment. - -import { readFile, readdir } from "node:fs/promises"; -import path from "node:path"; -import { expect, test } from "bun:test"; - -const ALLOWED_IMPORT_PREFIXES = ["@intx/", "arktype", "./", "../"]; - -async function listFiles(dir: string): Promise { - const entries = await readdir(dir, { withFileTypes: true }); - const files: string[] = []; - for (const entry of entries) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...(await listFiles(full))); - } else { - files.push(full); - } - } - return files; -} - -async function shippedFiles(): Promise { - const packageRoot = path.join(import.meta.dir, ".."); - return [ - ...(await listFiles(path.join(packageRoot, "src"))), - path.join(packageRoot, "package.json"), - ]; -} - -test("shipped sources import only published platform packages", async () => { - const importPattern = /from\s+"([^"]+)"/g; - const violations: string[] = []; - for (const file of await shippedFiles()) { - if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue; - const content = await readFile(file, "utf8"); - for (const match of content.matchAll(importPattern)) { - const specifier = match[1] ?? ""; - const allowed = ALLOWED_IMPORT_PREFIXES.some((prefix) => specifier.startsWith(prefix)); - if (!allowed) { - violations.push(`${path.basename(file)}: ${specifier}`); - } - } - } - expect(violations).toEqual([]); -}); diff --git a/workflows/attio-task-agent/test/definition.test.ts b/workflows/attio-task-agent/test/definition.test.ts deleted file mode 100644 index 1d6a6ac29c..0000000000 --- a/workflows/attio-task-agent/test/definition.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -// Tests for this package's own contract: the shape our factory commits -// to, its serialization guarantees, and its boundary. The platform's own -// normalization and validation are its business, not re-proven here. - -import { expect, test } from "bun:test"; -import type { StepPrimitive, WorkflowDefinition } from "@intx/workflow"; - -import { - ATTIO_MCP_SERVER_SLUG, - ATTIO_TASK_AGENT_FINALIZE_TOOL_NAME, - ATTIO_TASK_AGENT_STEP_ID, - ATTIO_TASK_AGENT_SYSTEM_PROMPT, - ATTIO_TASK_AGENT_TOOL_PACKAGE_PINS, - ATTIO_TASK_AGENT_WORKFLOW_ID, - buildAttioTaskAgentWorkflow, - serializeAttioTaskAgentWorkflow, -} from "../src/index"; - -const INPUT = { - triggerAddress: "ins_dep000000000000@example.test", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 900000, -} as const; - -function workStep(definition: WorkflowDefinition): StepPrimitive { - const primitive = definition.steps[ATTIO_TASK_AGENT_STEP_ID]; - if (primitive === undefined || primitive.kind !== "step") { - throw new Error(`definition has no step primitive named ${ATTIO_TASK_AGENT_STEP_ID}`); - } - return primitive; -} - -test("the definition folds the OG twenty-one-step pipeline into exactly one step", () => { - const definition = buildAttioTaskAgentWorkflow(INPUT); - expect(definition.stepOrder).toEqual([ATTIO_TASK_AGENT_STEP_ID]); - expect(Object.keys(definition.steps)).toEqual([ATTIO_TASK_AGENT_STEP_ID]); -}); - -test("the definition uses no action or awaitSignal primitive — this host can dispatch neither", () => { - const definition = buildAttioTaskAgentWorkflow(INPUT); - const kinds = Object.values(definition.steps).map((primitive) => primitive.kind); - expect(kinds).toEqual(["step"]); -}); - -test("the step carries an explicit per-turn timeout", () => { - const definition = buildAttioTaskAgentWorkflow(INPUT); - expect(workStep(definition).timeout).toBe(INPUT.turnTimeoutMs); -}); - -test("the workflow is triggered by mail to the given deployment address", () => { - const definition = buildAttioTaskAgentWorkflow(INPUT); - expect(definition.id).toBe(ATTIO_TASK_AGENT_WORKFLOW_ID); - expect(definition.triggers).toEqual([{ type: "mail", to: INPUT.triggerAddress }]); -}); - -test("the agent carries the fixed prompt and the caller's preferences, and inlines no tools", () => { - const agent = workStep(buildAttioTaskAgentWorkflow(INPUT)).agent; - expect(agent.systemPrompt).toBe(ATTIO_TASK_AGENT_SYSTEM_PROMPT); - expect(agent.inference).toEqual({ sources: INPUT.inferencePreferences }); - expect(agent.toolFactories).toEqual([]); -}); - -test("one MCP pin covers the CRM, past calls, and the web — the OG needed three tool packages", () => { - const agent = workStep(buildAttioTaskAgentWorkflow(INPUT)).agent; - expect(agent.toolPackagePins).toEqual(ATTIO_TASK_AGENT_TOOL_PACKAGE_PINS); - expect(ATTIO_TASK_AGENT_TOOL_PACKAGE_PINS).toEqual([ - { name: "@corbits/mcp-tools", version: "0.0.12" }, - ]); -}); - -test("the definition declares no static credential binding — mcp: is dynamic tenant data the host supplies", () => { - const definition = buildAttioTaskAgentWorkflow(INPUT); - expect(definition.credentialBindings ?? []).toEqual([]); -}); - -test("the prompt names the CRM server slug and the finalize tool the package actually exports", () => { - expect(ATTIO_MCP_SERVER_SLUG).toBe("attio"); - expect(ATTIO_TASK_AGENT_SYSTEM_PROMPT).toContain(`"${ATTIO_MCP_SERVER_SLUG}" server`); - expect(ATTIO_TASK_AGENT_SYSTEM_PROMPT).toContain(ATTIO_TASK_AGENT_FINALIZE_TOOL_NAME); -}); - -test("the definition survives the workflow-asset JSON round trip unchanged", () => { - const definition = buildAttioTaskAgentWorkflow(INPUT); - expect(JSON.parse(serializeAttioTaskAgentWorkflow(definition))).toEqual( - JSON.parse(JSON.stringify(definition)), - ); -}); - -test("serializing a definition JSON would mangle fails loudly, naming the path", () => { - const definition = buildAttioTaskAgentWorkflow(INPUT); - const poisoned = { - ...definition, - steps: { ...definition.steps, poison: () => undefined }, - } as unknown as WorkflowDefinition; - expect(() => serializeAttioTaskAgentWorkflow(poisoned)).toThrow( - /definition\.steps\.poison is a function/, - ); -}); - -test("an empty trigger address is rejected at build time", () => { - expect(() => buildAttioTaskAgentWorkflow({ ...INPUT, triggerAddress: "" })).toThrow( - /non-empty triggerAddress/, - ); -}); - -test("a non-positive turn timeout is rejected at build time", () => { - expect(() => buildAttioTaskAgentWorkflow({ ...INPUT, turnTimeoutMs: 0 })).toThrow( - /positive integer/, - ); -}); diff --git a/workflows/attio-task-agent/tsconfig.json b/workflows/attio-task-agent/tsconfig.json deleted file mode 100644 index a8c2b9966d..0000000000 --- a/workflows/attio-task-agent/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src", "test"], - "exclude": [] -} diff --git a/workflows/attio-task-agent/tsconfig.src.json b/workflows/attio-task-agent/tsconfig.src.json deleted file mode 100644 index fc8724305f..0000000000 --- a/workflows/attio-task-agent/tsconfig.src.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } -} diff --git a/workflows/code-review/LICENSE b/workflows/code-review/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/workflows/code-review/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/workflows/code-review/README.md b/workflows/code-review/README.md deleted file mode 100644 index 382a441c75..0000000000 --- a/workflows/code-review/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# Code review - -A pull request comes in, three reviewer lenses read its diff, and one -review is posted back on the pull request. - -## The loop - -1. **A pull request event arrives.** A GitHub `pull_request` webhook - registered through `@corbits/webhook-triggers` verifies the delivery, - renders its input template, and launches one run per event. The - template this definition expects is - `CODE_REVIEW_WEBHOOK_INPUT_TEMPLATE`: - - ``` - Review this pull request: {{pull_request.html_url}} - ``` - - The same run can be started by hand — the trigger is mail to the - deployment's address, so a message naming a pull-request URL is a - review request. - -2. **The diff is read once.** `github_pull_request_diff` (from - `@corbits/github-tools`) returns the title, description, head commit - sha, and every file's patch, plus the right-hand lines a comment can - be anchored to. - -3. **Three passes over the same diff.** Correctness (defects with - receipts), architecture (is the shape sound), and release risk (what - actually blocks shipping). The prompts live in - `@corbits/code-review`'s reviewer roster — the same three definitions - that install as agents through the agent-directory create path, so a - reviewer's wording exists in exactly one place. - -4. **One review, posted.** The passes are combined into a single review: - a finding two passes raise is one entry crediting both, blocking - first, and inline comments only on lines the diff can anchor. - `github_post_pull_request_review` posts it. - -## Approval - -Posting is not approval-gated. A review is a comment — it never -approves, requests changes, or merges — so it flows under the standing -grant on the GitHub connection. - -## Connections - -Needs the `github` connection. `CODE_REVIEW_CREDENTIAL_BINDINGS` binds -the pinned tool package's `github` handle to the tenant's credential; -without it the run says plainly that GitHub is not connected rather than -reviewing a change it could not read. - -## Large pull requests - -The diff read takes the first 100 changed files, and the review prompt -holds a total patch budget. When either cut applies, the prompt names -what was not shown and the review says so, rather than reporting a clean -pass over files nobody looked at. - -See [`workflows/README.md`](../README.md#status-note) for what -registration/automatable/seeded mean — this one is `automatable: false` -(it runs on a pull-request event, not a schedule) and not seeded, since -it needs a GitHub connection to be useful. - -## Deploy from the npm registry - -Once published, deploy this workflow definition to a tenant by name and -version pin: - -``` -POST /api/tenants/:id/workflows/deployments -{ - "source": { "kind": "registry", "registry": "npm" }, - "entry": "./src/index.ts", - "pin": "@corbits/code-review-workflow@0.0.1", - "sourceOfferingIds": ["", ...], - "defaultSourceOfferingId": "" -} -``` - -`entry` is the package's `interchange.workflow` module path; `pin` is -`"@corbits/code-review-workflow@0.0.1"` or a semver range on the same name. The hub -installs, probes, gates, and freezes the definition from the registry -tarball before creating the deployment. diff --git a/workflows/code-review/package.json b/workflows/code-review/package.json deleted file mode 100644 index e5a673ccb4..0000000000 --- a/workflows/code-review/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@corbits/code-review-workflow", - "version": "0.0.1", - "description": "Pull-request review workflow: three reviewer lenses over one diff, one review posted back", - "license": "LGPL-2.1-or-later", - "repository": { - "type": "git", - "url": "https://github.com/corbitsdev/workbench.git", - "directory": "workflows/code-review" - }, - "files": [ - "src", - "LICENSE", - "README.md" - ], - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "@corbits/code-review": "0.0.2" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0" - }, - "corbits": { - "workflow": { - "assetName": "code-review", - "displayName": "Code review", - "automatable": false - } - }, - "interchange": { - "workflow": "./src/index.ts" - } -} diff --git a/workflows/code-review/src/index.ts b/workflows/code-review/src/index.ts deleted file mode 100644 index 890f0e7ee2..0000000000 --- a/workflows/code-review/src/index.ts +++ /dev/null @@ -1,193 +0,0 @@ -// The code-review workflow: a pull request arrives, three reviewer -// lenses read the diff, and one review is posted back on the pull -// request. -// -// One step, one agent — the shape every definition in this catalog -// commits to, and the shape the webhook ingress can launch today -// (`@corbits/webhook-triggers` launches a folded, single-step definition -// through `@corbits/workflows`' `readFoldedBody`). A GitHub `pull_request` -// webhook registered against this definition renders its input template into the -// run's first message, so the trigger is a real PR event, not a poll. -// -// The three reviewer lenses are not restated here: their prompts come -// from `@corbits/code-review`, the same definitions installed as agents -// through the agent-directory create path, so a person editing a -// reviewer edits one prompt rather than two. Aggregation rules — dedupe -// what the lenses agree on, blocking first, anchor inline comments only -// to lines the diff reported — mirror that package's `aggregateReview`, -// which is the tested statement of the same contract. -// -// Posting is not approval-gated (owner ruling): a review is a comment, -// so it flows under the standing grant on the GitHub connection. The -// definition never approves, requests changes, or merges. -// -// Tool-package pins (CL-5999): `@intx/agent`'s `defineAgent` still does -// not accept a `toolPackagePins` field on its authoring-time config, so -// the agent below is built against `AgentDefinition`'s own type, which -// carries the field — matching every sibling definition in this catalog. -// -// This package is installable data. It imports only published platform -// packages, and nothing imports it statically: a host publishes the -// serialized definition as a workflow asset and deploys it through the -// platform's deploy machinery. - -import type { AgentDefinition, InferencePreference } from "@intx/agent"; -import { defineWorkflow, step } from "@intx/workflow"; -import type { WorkflowDefinition } from "@intx/workflow"; -import type { CredentialBinding } from "@intx/types"; -import type { ToolPackagePin } from "@intx/types/tool-packages"; -import { CODE_REVIEW_REVIEWERS } from "@corbits/code-review"; - -export const CODE_REVIEW_WORKFLOW_ID = "wf_code_review"; -export const CODE_REVIEW_STEP_ID = "code-review"; - -/** The tool packages this definition pins: GitHub reach, nothing else. */ -export const CODE_REVIEW_TOOL_PACKAGE_PINS: readonly ToolPackagePin[] = [ - { name: "@corbits/github-tools", version: "0.0.11" }, -]; - -/** Binds the pinned package's "github" handle to the tenant's connection. */ -export const CODE_REVIEW_CREDENTIAL_BINDINGS: readonly CredentialBinding[] = [ - { - package: "@corbits/github-tools", - handle: "github", - provider: "github", - locator: "tenant", - }, -]; - -/** - * The input template a GitHub `pull_request` webhook registers with, so - * the run's first message names the pull request the event is about. - */ -export const CODE_REVIEW_WEBHOOK_INPUT_TEMPLATE = - "Review this pull request: {{pull_request.html_url}}"; - -const REVIEWER_PASSES = CODE_REVIEW_REVIEWERS.map( - (reviewer, index) => - `### Pass ${String(index + 1)}: ${reviewer.displayName}\n` + reviewer.systemPrompt, -).join("\n\n"); - -export const CODE_REVIEW_SYSTEM_PROMPT = - "You review pull requests. A message names one pull request; you read " + - "it, review it under three separate lenses, and post one review back " + - "on it.\n" + - "\n" + - "## Reading the change\n" + - "Call `github_pull_request_diff` with the pull-request URL from the " + - "message. It gives you the title, the description, the head commit " + - "sha, and each file's patch with the right-hand lines a comment can " + - "be anchored to. If the message names no pull-request URL, or the " + - "call comes back not connected, say so plainly in one sentence and " + - "stop — never review a change you could not read.\n" + - "\n" + - "## The three passes\n" + - "Make each pass separately, in order, over the same diff. Do not let " + - "one pass answer for another: a pass that has nothing to raise says " + - "so.\n" + - "\n" + - REVIEWER_PASSES + - "\n" + - "\n" + - "## The one review\n" + - "Combine the three passes into a single review. A finding two passes " + - "raise is one entry crediting both. Order the entries blocking " + - "first, then worth fixing, then for later. Say what each pass looked " + - "at in one line, and name any pass that came up empty. Never invent " + - "a finding to fill a section, and never repeat the whole diff back.\n" + - "\n" + - "## Posting it\n" + - "Call `github_post_pr_review` exactly once, with the pull " + - "request's URL, the head commit sha the diff reported, the combined " + - "review as the body, and inline comments only for findings anchored " + - "to a line that diff reported for that file — every other finding " + - "belongs in the body. Then reply with a two-line summary: what you " + - "posted, and the counts by severity. You are posting a comment, not " + - "a verdict: never approve, request changes, or merge."; - -export interface CodeReviewWorkflowInput { - /** The deployment's mail address; each inbound message is one review. */ - readonly triggerAddress: string; - /** Provider/model preferences, in order; resolved at deploy time. */ - readonly inferencePreferences: readonly InferencePreference[]; - /** Per-turn timeout in milliseconds, enforced on the single step. */ - readonly turnTimeoutMs: number; -} - -/** - * Builds the code-review definition. One step, on purpose: it is the - * shape the webhook ingress can launch, and it keeps the three passes in - * one agent's context so the combined review is written from all three - * rather than stitched from summaries. - */ -export function buildCodeReviewWorkflow(input: CodeReviewWorkflowInput): WorkflowDefinition { - if (input.triggerAddress === "") { - throw new Error("buildCodeReviewWorkflow requires a non-empty triggerAddress"); - } - if (!Number.isInteger(input.turnTimeoutMs) || input.turnTimeoutMs <= 0) { - throw new Error("buildCodeReviewWorkflow requires turnTimeoutMs to be a positive integer"); - } - return defineWorkflow({ - id: CODE_REVIEW_WORKFLOW_ID, - trigger: { type: "mail", to: input.triggerAddress }, - steps: { - "code-review": step({ - agent: { - id: CODE_REVIEW_STEP_ID, - description: - "Reviews a pull request under three lenses and posts one " + "review back on it", - systemPrompt: CODE_REVIEW_SYSTEM_PROMPT, - toolFactories: [], - capabilities: [], - inference: { sources: input.inferencePreferences }, - toolPackagePins: CODE_REVIEW_TOOL_PACKAGE_PINS, - } satisfies AgentDefinition, - timeout: input.turnTimeoutMs, - triggers: "unbounded", - }), - }, - }); -} - -/** - * Serializes the definition to the JSON a workflow asset carries. A - * value JSON would drop or mangle is a loud error naming its path - * instead of a corrupted asset. - */ -export function serializeCodeReviewWorkflow(definition: WorkflowDefinition): string { - assertJsonPortable(definition, "definition"); - return JSON.stringify(definition); -} - -function assertJsonPortable(value: unknown, path: string): void { - if (value === null) return; - switch (typeof value) { - case "string": - case "boolean": - return; - case "number": - if (!Number.isFinite(value)) { - throw new Error(`${path} is a non-finite number; JSON drops it`); - } - return; - case "object": - break; - default: - throw new Error( - `${path} is a ${typeof value}, which does not survive JSON ` + "serialization", - ); - } - if (Array.isArray(value)) { - value.forEach((element, index) => { - assertJsonPortable(element, `${path}[${index}]`); - }); - return; - } - const proto: unknown = Object.getPrototypeOf(value); - if (proto !== Object.prototype && proto !== null) { - throw new Error(`${path} is a non-plain object; JSON would flatten it lossily`); - } - for (const [key, entry] of Object.entries(value)) { - assertJsonPortable(entry, `${path}.${key}`); - } -} diff --git a/workflows/code-review/test/definition.test.ts b/workflows/code-review/test/definition.test.ts deleted file mode 100644 index 33d444c544..0000000000 --- a/workflows/code-review/test/definition.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -// Tests this package's own contract: the shape the factory commits to, -// that the reviewer lenses are not restated here, and that the -// definition survives the asset round-trip. - -import { expect, test } from "bun:test"; -import type { StepPrimitive, WorkflowDefinition } from "@intx/workflow"; -import { CODE_REVIEW_REVIEWERS } from "@corbits/code-review"; - -import { - buildCodeReviewWorkflow, - CODE_REVIEW_STEP_ID, - CODE_REVIEW_SYSTEM_PROMPT, - CODE_REVIEW_TOOL_PACKAGE_PINS, - CODE_REVIEW_WEBHOOK_INPUT_TEMPLATE, - CODE_REVIEW_WORKFLOW_ID, - serializeCodeReviewWorkflow, -} from "../src/index"; - -const INPUT = { - triggerAddress: "ins_dep000000000000@example.test", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 600000, -} as const; - -function reviewStep(definition: WorkflowDefinition): StepPrimitive { - const primitive = definition.steps[CODE_REVIEW_STEP_ID]; - if (primitive === undefined || primitive.kind !== "step") { - throw new Error(`definition has no step named ${CODE_REVIEW_STEP_ID}`); - } - return primitive; -} - -test("the definition is one mail-triggered step the webhook ingress can launch", () => { - const definition = buildCodeReviewWorkflow(INPUT); - expect(definition.id).toBe(CODE_REVIEW_WORKFLOW_ID); - expect(definition.stepOrder).toEqual([CODE_REVIEW_STEP_ID]); - expect(definition.triggers).toEqual([{ type: "mail", to: INPUT.triggerAddress }]); -}); - -test("the step pins GitHub reach and sets an explicit turn timeout", () => { - const primitive = reviewStep(buildCodeReviewWorkflow(INPUT)); - expect(primitive.timeout).toBe(INPUT.turnTimeoutMs); - expect(primitive.agent.toolPackagePins).toEqual(CODE_REVIEW_TOOL_PACKAGE_PINS); - expect(primitive.agent.toolFactories).toEqual([]); -}); - -test("every reviewer lens reaches the prompt from the shared roster", () => { - for (const reviewer of CODE_REVIEW_REVIEWERS) { - expect(CODE_REVIEW_SYSTEM_PROMPT).toContain(reviewer.displayName); - expect(CODE_REVIEW_SYSTEM_PROMPT).toContain(reviewer.systemPrompt); - } -}); - -test("the prompt commits to reading the diff, posting once, and never merging", () => { - expect(CODE_REVIEW_SYSTEM_PROMPT).toContain("github_pull_request_diff"); - expect(CODE_REVIEW_SYSTEM_PROMPT).toContain("github_post_pr_review"); - expect(CODE_REVIEW_SYSTEM_PROMPT).toContain("never approve, request changes, or merge"); -}); - -test("the webhook input template names the pull request from the event", () => { - expect(CODE_REVIEW_WEBHOOK_INPUT_TEMPLATE).toContain("{{pull_request.html_url}}"); -}); - -test("the definition survives the workflow-asset round trip", () => { - const definition = buildCodeReviewWorkflow(INPUT); - const round = JSON.parse(serializeCodeReviewWorkflow(definition)) as WorkflowDefinition; - expect(round.stepOrder).toEqual(definition.stepOrder); - expect(round.id).toBe(definition.id); -}); - -test("a blank trigger address or a bad timeout is refused at build time", () => { - expect(() => buildCodeReviewWorkflow({ ...INPUT, triggerAddress: "" })).toThrow(/triggerAddress/); - expect(() => buildCodeReviewWorkflow({ ...INPUT, turnTimeoutMs: 0 })).toThrow(/turnTimeoutMs/); -}); diff --git a/workflows/code-review/tsconfig.json b/workflows/code-review/tsconfig.json deleted file mode 100644 index a8c2b9966d..0000000000 --- a/workflows/code-review/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src", "test"], - "exclude": [] -} diff --git a/workflows/code-review/tsconfig.src.json b/workflows/code-review/tsconfig.src.json deleted file mode 100644 index fc8724305f..0000000000 --- a/workflows/code-review/tsconfig.src.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } -} diff --git a/workflows/collateral-generation/LICENSE b/workflows/collateral-generation/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/workflows/collateral-generation/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/workflows/collateral-generation/README.md b/workflows/collateral-generation/README.md deleted file mode 100644 index 9c38ec19df..0000000000 --- a/workflows/collateral-generation/README.md +++ /dev/null @@ -1,145 +0,0 @@ -# @corbits/collateral-generation-workflow - -Drafts marketing collateral from one or more picked sources and content -types, with a swipe review on every draft and one human approval on the -final approved set (CL-5996, ported from `gtm-workbench`'s -`workflows/multi-source-collateral`, child of CL-5987). - -## What it does - -One step, one agent, matching every other definition in this catalog. The -system prompt commits it to: - -- **Source pick**: ask which source(s) to draw from — Granola call notes - (`granola_get_note`, `@corbits/granola-tools`), Linear issues - (`linear_list_recent_issues`, `@corbits/linear-tools`), and/or text - pasted directly into the conversation. Wait for an answer. -- **Content-type pick**: ask which of the seven content types to draft — - LinkedIn post/article, Twitter/X post/article, short/mid/long blog. A - run can draft more than one. Wait for an answer. -- **Gathering**: fetch every picked source. A source that errors is - reported plainly and skipped, never fabricated. If nothing loads and no - text was pasted either, the run calls `collateral_generation_finalize` - (the same tool the normal path uses, not a second one) with a single - `"status-note"` piece naming which sources were tried, which were - unreachable or not picked, and the concrete next step — so a Library - entry and chip still land instead of a bare, artifact-less reply. -- **Drafting**: one piece per picked content type, grounded only in what - was gathered, following that type's length/structure guidance. Public - collateral: customer- and company-identifying detail is stripped or - generalized. No buzzwords, no em dashes, no hollow superlatives. -- **Swipe review**: Good / Bad / Regenerate with optional feedback on - every draft. A rejected piece gets exactly one revision; a piece - rejected again after that is dropped, not retried. -- **Finalizing**: exactly one call to `collateral_generation_finalize` - (`./src/finalize-tool.ts`) with every approved piece, gated behind a - single human approval. - -**Teaching-artifact kind**: each piece's `kind` is `piece.contentType`, -one of the seven real content-type ids for a normal draft, or -`"status-note"` for the no-data teaching piece above. `"status-note"` is -the one teaching-artifact kind shared by every workflow in this catalog, -so the Library's kind badge always reads "Status note" for a no-data -run, regardless of which workflow made it. - -## Gate consolidation - -`gtm-workbench`'s original implementation suspended for a human **four** -times in sequence: source pick, content-type pick, a swipe review per -drafted piece, and a second review of anything regenerated. That shape -came from the OG's step-graph architecture (`awaitSignal` steps), which -had no other way to pause for input — it was not four independent -approval decisions. - -Three of those four are ordinary questions a chat-native agent asks in -the course of one conversation — what to draft from, what to draft, and -which drafts landed — not gates needing the platform's suspend/resume -approval machinery. This port keeps the same conversation shape but -**consolidates to one real approval gate**: finalizing the full set of -pieces the sender approved, all at once, in a single -`collateral_generation_finalize` call. The swipe review and its -one-revise-pass cap still happen — as regular conversational turns -before that one call, not as four separate suspended approvals. - -## Approval mechanics - -Identical to `@corbits/pain-point-collateral-workflow`'s -`finalize-tool.ts`: `collateral_generation_finalize` is declared -`approval: "ask"` (`@intx/agent`'s `ToolDeclaration`), the platform's -native tool-approval gate. Calling it suspends the run; a human approves -or rejects it from the inbox; on approval the tool actually runs, on -rejection it never runs and the model gets a synthetic "denied by -approver" error, which the system prompt turns into a calm terminal -reply. See that package's README for the full suspend/resume account, -which applies unchanged here. - -## Current limits (read before deploying) - -One real gap stands between this definition and a fully live deploy, -platform-level, not specific to this workflow: - -- **Tool-package pins resolve only once published** (CL-5999 closed the - pinning gap; publishing is still an operator step). This definition - pins `@corbits/granola-tools` and `@corbits/linear-tools` - (`COLLATERAL_GENERATION_TOOL_PACKAGE_PINS`); a pin resolves at deploy - time once an operator publishes the package to a registry the host's - `toolPackageRegistries` config reaches — npmjs, or a `package-registry` - asset for the `@corbits` scope (see `apps/hub/src/index.ts`'s - `CORBITS_TOOLS_REGISTRY` wiring). The finalize tool itself persists - for real (CL-6000): each approved piece writes through the - workflow-artifacts HTTP surface and lands in the Library with a - file-part chip in the channel. - -**Library persistence and listing are both real (CL-6000).** -`finalize-tool.ts`'s `run` persists each approved piece via -`./artifact-client.ts` against the sanctioned workflow-artifacts HTTP -surface, sequentially — a piece that fails to persist stops the loop and -reports how many of the set already persisted, rather than silently -losing them or claiming the whole batch failed. `@corbits/artifact-tools`' -`artifact_list_recent` calls the same surface's read side, so workbench -artifacts are a real source alongside the wired Granola and Linear ones. -Delivered pieces carry their persisted artifact's file-part chip -(`packages/chat/src/artifact-delivery.ts`). - -## Usage - -```ts -import { - buildCollateralGenerationWorkflow, - serializeCollateralGenerationWorkflow, -} from "@corbits/collateral-generation-workflow"; - -const definition = buildCollateralGenerationWorkflow({ - triggerAddress: "collateral-generation@tenant.example", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 300000, -}); - -const json = serializeCollateralGenerationWorkflow(definition); -``` - -See [`workflows/README.md`](../README.md#status-note) for what -registration/automatable/seeded mean — this one is `automatable: false`, -on-demand only, since its approval gate is a poor fit for unattended -scheduling. - -## Deploy from the npm registry - -Once published, deploy this workflow definition to a tenant by name and -version pin: - -``` -POST /api/tenants/:id/workflows/deployments -{ - "source": { "kind": "registry", "registry": "npm" }, - "entry": "./src/index.ts", - "pin": "@corbits/collateral-generation-workflow@0.0.1", - "sourceOfferingIds": ["", ...], - "defaultSourceOfferingId": "" -} -``` - -`entry` is the package's `interchange.workflow` module path; `pin` is -`"@corbits/collateral-generation-workflow@0.0.1"` or a semver range on the same name. The hub -installs, probes, gates, and freezes the definition from the registry -tarball before creating the deployment. diff --git a/workflows/collateral-generation/package.json b/workflows/collateral-generation/package.json deleted file mode 100644 index 8e37e04e34..0000000000 --- a/workflows/collateral-generation/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@corbits/collateral-generation-workflow", - "version": "0.0.1", - "description": "Drafts marketing collateral from one or more picked sources and content types, with a per-piece swipe review and one human approval on the final assembled set", - "license": "LGPL-2.1-or-later", - "repository": { - "type": "git", - "url": "https://github.com/corbitsdev/workbench.git", - "directory": "workflows/collateral-generation" - }, - "files": [ - "src", - "LICENSE", - "README.md" - ], - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "arktype": "catalog:" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0" - }, - "corbits": { - "workflow": { - "assetName": "collateral-generation", - "displayName": "Collateral generation", - "automatable": false - } - }, - "interchange": { - "workflow": "./src/index.ts" - } -} diff --git a/workflows/collateral-generation/src/artifact-client.test.ts b/workflows/collateral-generation/src/artifact-client.test.ts deleted file mode 100644 index 1c86414419..0000000000 --- a/workflows/collateral-generation/src/artifact-client.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { expect, test } from "bun:test"; - -import { createWorkflowArtifact } from "./artifact-client"; - -const CONFIG = { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - runAddress: "run_1@workflow", -}; - -test("posts to the workflow-artifacts endpoint and returns id/version", async () => { - const captured: { url: string; init: RequestInit | undefined } = { - url: "", - init: undefined, - }; - const fetchImpl = (async (input: URL | string, init?: RequestInit) => { - captured.url = String(input); - captured.init = init; - return new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - }); - }) as unknown as typeof fetch; - - const result = await createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ); - - expect(result).toEqual({ id: "art_1", version: 1 }); - expect(captured.url).toBe("https://hub.example.com/api/workflow-artifacts/artifacts"); - const headers = captured.init?.headers as Record; - expect(headers["authorization"]).toBe("Bearer sc-token"); - expect(headers["x-workflow-run-address"]).toBe("run_1@workflow"); -}); - -test("throws on a non-ok HTTP response", async () => { - const fetchImpl = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/500/); -}); - -test("throws when the response doesn't match the expected shape", async () => { - const fetchImpl = (async () => - new Response(JSON.stringify({ data: {} }), { - status: 201, - })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/did not match the expected shape/); -}); diff --git a/workflows/collateral-generation/src/artifact-client.ts b/workflows/collateral-generation/src/artifact-client.ts deleted file mode 100644 index b9d56ca425..0000000000 --- a/workflows/collateral-generation/src/artifact-client.ts +++ /dev/null @@ -1,62 +0,0 @@ -// A minimal client for the sanctioned workflow-artifacts HTTP surface -// (`@corbits/artifacts-hub`'s `createWorkflowArtifactRoutes`, CL-6000). -// Deliberately duplicated from `@corbits/artifact-tools`' `client.ts` -// rather than imported: this package ships as installable data on the -// native workflow contract (see `./index.ts`'s header comment and -// `test/boundary.test.ts`), so its shipped sources import only `@intx/*`, -// `arktype`, and relative specifiers — never another `@corbits/*` -// package. Authenticates with the sidecar's own bearer token plus the -// run's own mailbox address (both already reach a workflow-process -// child's tool env), never a database handle. -import { type } from "arktype"; - -export interface WorkflowArtifactClientConfig { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly runAddress: string; - /** Override for tests; defaults to the global `fetch`. */ - readonly fetchImpl?: typeof fetch; -} - -export type CreateWorkflowArtifactInput = { - readonly title: string; - readonly kind: string; - readonly content: string; -}; - -export type CreatedWorkflowArtifact = { - readonly id: string; - readonly version: number; -}; - -const CreatedWorkflowArtifactResponse = type({ - data: { id: "string", version: "number" }, -}); - -/** Persists one artifact. Throws on any transport, HTTP, or shape failure. */ -export async function createWorkflowArtifact( - config: WorkflowArtifactClientConfig, - input: CreateWorkflowArtifactInput, -): Promise { - const doFetch = config.fetchImpl ?? fetch; - const response = await doFetch(`${config.hubArtifactsUrl}/api/workflow-artifacts/artifacts`, { - method: "POST", - headers: { - authorization: `Bearer ${config.sidecarToken}`, - "x-workflow-run-address": config.runAddress, - "content-type": "application/json", - }, - body: JSON.stringify(input), - }); - if (!response.ok) { - throw new Error(`Workflow artifact create failed: ${response.status} ${response.statusText}`); - } - const body: unknown = await response.json(); - const parsed = CreatedWorkflowArtifactResponse(body); - if (parsed instanceof type.errors) { - throw new Error( - `Workflow artifact create response did not match the expected shape: ${parsed.summary}`, - ); - } - return parsed.data; -} diff --git a/workflows/collateral-generation/src/finalize-tool.test.ts b/workflows/collateral-generation/src/finalize-tool.test.ts deleted file mode 100644 index ecada42e02..0000000000 --- a/workflows/collateral-generation/src/finalize-tool.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { expect, test } from "bun:test"; - -import { - COLLATERAL_GENERATION_FINALIZE_TOOL, - COLLATERAL_GENERATION_FINALIZE_TOOL_NAME, - buildArtifactPayloads, - type WorkflowArtifactEnv, -} from "./finalize-tool"; - -test("the tool's definition marks itself approval-gated", () => { - expect(COLLATERAL_GENERATION_FINALIZE_TOOL.definitions).toEqual([ - { name: COLLATERAL_GENERATION_FINALIZE_TOOL_NAME, approval: "ask" }, - ]); -}); - -test("the tool is namespaced under this workflow package, not a shared one", () => { - expect(COLLATERAL_GENERATION_FINALIZE_TOOL.id).toBe( - "@corbits/workflow-collateral-generation/finalize", - ); -}); - -test("requires the sanctioned workflow-artifacts env keys", () => { - expect(COLLATERAL_GENERATION_FINALIZE_TOOL.requires).toEqual([ - "hubArtifactsUrl", - "sidecarToken", - "address", - ]); -}); - -test("buildArtifactPayloads maps each piece's content type to the artifact kind", () => { - const payloads = buildArtifactPayloads({ - pieces: [ - { - title: "Faster onboarding, in plain terms", - contentType: "linkedin-post", - content: "Our platform cuts onboarding time by...", - }, - { - title: "Why teams switch", - contentType: "blog-short", - content: "Teams switch when the old workflow costs more than...", - }, - ], - }); - expect(payloads).toEqual([ - { - title: "Faster onboarding, in plain terms", - kind: "linkedin-post", - content: "Our platform cuts onboarding time by...", - }, - { - title: "Why teams switch", - kind: "blog-short", - content: "Teams switch when the old workflow costs more than...", - }, - ]); -}); - -function testEnv(): WorkflowArtifactEnv { - return { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - address: "run_1@workflow", - } as unknown as WorkflowArtifactEnv; -} - -test("run persists every approved piece on real invocation (i.e. after approval re-dispatches the call)", async () => { - let call = 0; - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => { - call += 1; - return new Response(JSON.stringify({ data: { id: `art_${call}`, version: 1 } }), { - status: 201, - }); - }) as unknown as typeof fetch; - - try { - const bundle = COLLATERAL_GENERATION_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: COLLATERAL_GENERATION_FINALIZE_TOOL_NAME, - arguments: { - pieces: [ - { - title: "Faster onboarding, in plain terms", - contentType: "linkedin-post", - content: "Our platform cuts onboarding time by...", - }, - { - title: "Why teams switch", - contentType: "blog-short", - content: "Teams switch when the old workflow costs more than...", - }, - ], - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(String(result.content)) as { - artifacts: { id: string; title: string; persisted: boolean }[]; - }; - expect(parsed.artifacts).toHaveLength(2); - expect(parsed.artifacts.map((a) => a.id)).toEqual(["art_1", "art_2"]); - for (const artifact of parsed.artifacts) { - expect(artifact.persisted).toBe(true); - } - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run reports a partial failure honestly, naming how many pieces already persisted", async () => { - let call = 0; - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => { - call += 1; - if (call === 1) { - return new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { status: 201 }); - } - return new Response("nope", { status: 500 }); - }) as unknown as typeof fetch; - - try { - const bundle = COLLATERAL_GENERATION_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: COLLATERAL_GENERATION_FINALIZE_TOOL_NAME, - arguments: { - pieces: [ - { - title: "Faster onboarding, in plain terms", - contentType: "linkedin-post", - content: "Our platform cuts onboarding time by...", - }, - { - title: "Why teams switch", - contentType: "blog-short", - content: "Teams switch when the old workflow costs more than...", - }, - ], - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(true); - expect(result.content).toContain("persisting 1 of 2"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run persists a single teaching piece the same way as any other piece (the no-data path reuses this tool, not a second one)", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - })) as unknown as typeof fetch; - - try { - const bundle = COLLATERAL_GENERATION_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: COLLATERAL_GENERATION_FINALIZE_TOOL_NAME, - arguments: { - pieces: [ - { - title: "Nothing to draft from", - contentType: "status-note", - content: - "Tried Granola call notes, Linear issues, and pasted " + - "text; none were reachable or provided. Reply with a " + - "note id, confirm the source is connected, or paste " + - "text directly to try again.", - }, - ], - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(String(result.content)) as { - artifacts: { - id: string; - version: number; - title: string; - kind: string; - persisted: boolean; - }[]; - }; - expect(parsed.artifacts).toEqual([ - { - id: "art_1", - version: 1, - title: "Nothing to draft from", - kind: "status-note", - persisted: true, - }, - ]); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run rejects an empty pieces array without throwing", async () => { - const bundle = COLLATERAL_GENERATION_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: COLLATERAL_GENERATION_FINALIZE_TOOL_NAME, - arguments: { pieces: [] }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("at least one approved piece"); -}); - -test("run rejects malformed arguments without throwing", async () => { - const bundle = COLLATERAL_GENERATION_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: COLLATERAL_GENERATION_FINALIZE_TOOL_NAME, - arguments: {}, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); diff --git a/workflows/collateral-generation/src/finalize-tool.ts b/workflows/collateral-generation/src/finalize-tool.ts deleted file mode 100644 index dd1cf32c61..0000000000 --- a/workflows/collateral-generation/src/finalize-tool.ts +++ /dev/null @@ -1,191 +0,0 @@ -// The one piece of this workflow gated by human approval: finalizing the -// full set of pieces the human approved during swipe review. Kept inside -// the workflow package rather than a shared tool package — same -// "workflow-specific logic lives in the definition" convention -// `pain-point-collateral`'s `finalize-tool.ts` established — this is not -// a reusable integration on its own. -// -// Gate consolidation (CL-5996): the OG `gtm-workbench` implementation -// suspended for a human four times in sequence — source pick, content-type -// pick, a swipe review per drafted piece, and a second review of anything -// regenerated. Three of those are ordinary conversational turns (asking -// what to draft from, what to draft, and swiping good/bad/regenerate on -// each draft, with at most one revise pass), not approval-gate suspends — -// they only looked like gates because the OG's step-graph architecture had -// no other way to pause for input. This port asks the same questions in -// the same conversation instead (see `./index.ts`'s system prompt) and -// keeps exactly one real approval gate: finalizing the pieces the human -// actually approved, all at once, as one call to this tool. -// -// Approval mechanics: identical to `pain-point-collateral`'s -// `finalize-tool.ts` — `definitions` marks this tool's one definition -// `approval: "ask"`, the platform's native tool-approval gate. Calling it -// suspends the run and creates a real `approval` row; only executes once a -// human approves it. See that file's header comment for the full -// suspend/resume account, which applies unchanged here. -// -// Persistence (CL-6000): `run` below persists each piece via -// `createWorkflowArtifact` (`./artifact-client.ts`, duplicated from -// `@corbits/artifact-tools`' client rather than imported — see that -// file's header for why this installable-data package never imports -// another `@corbits/*` package) against the sanctioned workflow-artifacts -// HTTP surface, sequentially. A piece that fails to persist stops the -// loop and reports how many of the set already persisted rather than -// silently losing them or claiming the whole batch failed — an approval -// covers "finalize the whole set", so a partial failure is surfaced -// honestly rather than hidden. - -import { type } from "arktype"; -import { defineTool, type BaseEnv } from "@intx/agent"; -import { createWorkflowArtifact } from "./artifact-client"; - -export const COLLATERAL_GENERATION_FINALIZE_TOOL_NAME = "collateral_generation_finalize"; - -export const COLLATERAL_GENERATION_FINALIZE_DESCRIPTION = - "Finalizes the full set of human-approved collateral pieces from one run, pending a single human approval, and persists each as a Library artifact."; - -const CollateralPiece = type({ - /** Short, human-facing title. */ - title: "string > 0", - /** One of the content-type ids named in the system prompt, e.g. "linkedin-post". */ - contentType: "string > 0", - /** The drafted (and, if regenerated once, revised) piece body. */ - content: "string > 0", -}); - -const FinalizeArgs = type({ - pieces: CollateralPiece.array(), -}); - -export type FinalizeArgs = typeof FinalizeArgs.infer; -export type CollateralPiece = typeof CollateralPiece.infer; - -export type ArtifactPayload = { - title: string; - kind: string; - content: string; -}; - -/** - * The payload `createWorkflowArtifact` persists (`{ title, kind, - * content }`) for one piece — `kind` carries the content-type id, - * matching the OG's convention of using the content type as the - * artifact's kind. - */ -export function buildArtifactPayloads(args: FinalizeArgs): readonly ArtifactPayload[] { - return args.pieces.map((piece) => ({ - title: piece.title, - kind: piece.contentType, - content: piece.content, - })); -} - -/** - * The env this tool needs beyond `BaseEnv`: the sanctioned - * workflow-artifacts credential trio, populated for every workflow step - * (`apps/sidecar/src/workflow-substrate-factory/step-env.ts`) — the same - * three keys `@corbits/artifact-tools`' read-side bundle declares. - */ -export interface WorkflowArtifactEnv extends BaseEnv { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly address: string; -} - -/** - * `defineTool`'s env-DI factory shape. Needs the sanctioned - * workflow-artifacts credential trio beyond `BaseEnv`. - */ -export const COLLATERAL_GENERATION_FINALIZE_TOOL = defineTool({ - id: "@corbits/workflow-collateral-generation/finalize", - requires: ["hubArtifactsUrl", "sidecarToken", "address"], - definitions: [ - { - name: COLLATERAL_GENERATION_FINALIZE_TOOL_NAME, - approval: "ask", - }, - ], - factory: (env) => ({ - definitions: [ - { - name: COLLATERAL_GENERATION_FINALIZE_TOOL_NAME, - description: COLLATERAL_GENERATION_FINALIZE_DESCRIPTION, - inputSchema: { - type: "object", - properties: { - pieces: { - type: "array", - items: { - type: "object", - properties: { - title: { type: "string" }, - contentType: { type: "string" }, - content: { type: "string" }, - }, - required: ["title", "contentType", "content"], - }, - }, - }, - required: ["pieces"], - }, - }, - ], - run: async (call) => { - const parsed = FinalizeArgs(call.arguments); - if (parsed instanceof type.errors) { - return { - callId: call.id, - isError: true, - content: `Invalid arguments for ${COLLATERAL_GENERATION_FINALIZE_TOOL_NAME}: ${parsed.summary}`, - }; - } - if (parsed.pieces.length === 0) { - return { - callId: call.id, - isError: true, - content: `${COLLATERAL_GENERATION_FINALIZE_TOOL_NAME} requires at least one approved piece`, - }; - } - const artifacts = buildArtifactPayloads(parsed); - const persisted: { - id: string; - version: number; - title: string; - kind: string; - persisted: true; - }[] = []; - for (const artifact of artifacts) { - try { - const created = await createWorkflowArtifact( - { - hubArtifactsUrl: env.hubArtifactsUrl, - sidecarToken: env.sidecarToken, - runAddress: env.address, - }, - artifact, - ); - persisted.push({ - id: created.id, - version: created.version, - title: artifact.title, - kind: artifact.kind, - persisted: true, - }); - } catch (err) { - return { - callId: call.id, - isError: true, - content: `Failed to persist "${artifact.title}" as a Library artifact after persisting ${persisted.length} of ${artifacts.length} piece(s): ${ - err instanceof Error ? err.message : String(err) - }`, - }; - } - } - return { - callId: call.id, - isError: false, - content: JSON.stringify({ artifacts: persisted }), - }; - }, - }), -}); diff --git a/workflows/collateral-generation/src/index.ts b/workflows/collateral-generation/src/index.ts deleted file mode 100644 index 88962b9c41..0000000000 --- a/workflows/collateral-generation/src/index.ts +++ /dev/null @@ -1,351 +0,0 @@ -// The collateral-generation workflow (CL-5996, ported from -// `gtm-workbench`'s `workflows/multi-source-collateral`, child of -// CL-5987): drafts one piece of marketing collateral per content type -// the human picks, grounded in one or more sources the human picks, with -// a swipe review per piece and one human approval before anything is -// finalized. -// -// One step, one agent — matching the shape every other definition in -// this catalog commits to (`step`/`defineAgent`, mail-triggered, tools -// arrive as packages on the deploy, never inlined here). All -// workflow-specific logic — the exact source and content-type choices -// offered, how sources merge into one context, the per-content-type -// drafting guidance, the swipe-review and one-revise-pass rules, the -// exact wording of an honest "nothing to draft from" reply — lives in -// this definition's own system prompt and its one workflow-local tool -// (`./finalize-tool.ts`), not a separate tool package: none of it is a -// reusable integration on its own. The genuinely reusable pieces — -// fetching a Granola note by id and listing recently updated Linear -// issues — are `granola_get_note` (`@corbits/granola-tools`) and -// `linear_list_recent_issues` (`@corbits/linear-tools`), the same -// packages `pain-point-collateral` and `morning-brief` pin. -// -// Gate consolidation (see `finalize-tool.ts`'s header for the full -// account): the OG suspended for a human four times — source pick, -// content-type pick, per-piece swipe review, and a second review of -// anything regenerated. Only the last step needs the platform's real -// approval-suspend mechanism; the rest are ordinary conversational turns -// in this port, matching how every other definition in this catalog asks -// for input. This definition keeps exactly one approval gate: finalizing -// every piece the human approved, all at once. -// -// Tool-package pins (CL-5999): `@intx/agent`'s `defineAgent` still does -// not accept a `toolPackagePins` field on its authoring-time config — -// it is vendored, read-only source for this change — so the agent -// below is built directly against `AgentDefinition`'s own type, which -// already carries the field. `@corbits/granola-tools` and -// `@corbits/linear-tools` are pinned below; whether either pin -// *resolves* at deploy time still depends on an operator publishing it -// to a registry the host's tool-package resolver can reach (see -// `apps/hub/src/index.ts`'s `toolPackageRegistries` wiring). -// `@corbits/artifact-tools` (the read-side account in its own README) -// stays unpinned: its one tool still cannot reach the Library engine -// itself (CL-6000), so pinning it would resolve a package whose tool -// cannot do anything yet. See `finalize-tool.ts`'s header for the -// separate, still-open write-side gap. Until deploys actually resolve -// the pins below, this definition's system prompt still commits it to -// saying plainly when it has no way to reach a source or finalize a -// piece, rather than inventing source material or an approval that -// never happened. -// -// This package is installable data. It imports only published platform -// packages, and nothing imports it statically: a host publishes the -// serialized definition as a workflow asset and deploys it through the -// platform's deploy machinery; the execution host materializes it at -// runtime from the deploy alone. - -import type { AgentDefinition, InferencePreference } from "@intx/agent"; -import { defineWorkflow, step } from "@intx/workflow"; -import type { WorkflowDefinition } from "@intx/workflow"; -import type { ToolPackagePin } from "@intx/types/tool-packages"; -import type { CredentialBinding } from "@intx/types"; - -import { COLLATERAL_GENERATION_FINALIZE_TOOL_NAME } from "./finalize-tool"; - -export const COLLATERAL_GENERATION_WORKFLOW_ID = "wf_collateral_generation"; -export const COLLATERAL_GENERATION_STEP_ID = "collateral-generation"; - -/** - * Content types this workflow can draft, matching the OG's seven-type - * catalog. Named export so a future consumer (e.g. a picker UI wanting - * the same ids) reads the same list the prompt commits the model to, - * rather than re-deriving it from prose. - */ -export const COLLATERAL_CONTENT_TYPES = [ - { - id: "linkedin-post", - label: "LinkedIn post", - guidance: - "A hook in the first two lines, one clear insight grounded in the " + - "sources, and a close on a reflective question rather than a hard " + - "call to action. Around 120-220 words.", - }, - { - id: "linkedin-article", - label: "LinkedIn article", - guidance: - "A long-form piece: an opening narrative hook, two to four " + - "sections with headers developing the point, concrete (but " + - "generalized) examples, and a practical closing takeaway. Around " + - "600-1000 words.", - }, - { - id: "twitter-post", - label: "Twitter/X post", - guidance: - "One sharp idea, not a thread. Under 280 characters where " + - "possible, never over 500. No hashtag stacking, no emoji pile-on.", - }, - { - id: "twitter-article", - label: "Twitter/X article", - guidance: - "A strong title line followed by a scannable multi-section body. " + "Around 400-800 words.", - }, - { - id: "blog-short", - label: "Short blog post", - guidance: - "Hook, problem, insight, takeaway. Headers used sparingly, " + - "conversational and paste-ready. Around 400-600 words.", - }, - { - id: "blog-mid", - label: "Mid-length blog post", - guidance: - "A narrative arc across clear sections, with examples and " + - "practical advice. Around 800-1200 words.", - }, - { - id: "blog-long", - label: "Long-form blog post", - guidance: - "A deeper piece: context, problem, analysis, recommendations, " + - "across multiple sections. Around 1500-2200 words.", - }, -] as const; - -/** Sources this deployment can actually reach today, each backed by a - * real, pinnable tool package. Workbench artifacts reach the Library - * engine for real now (CL-6000) but still have no way to land on a - * deployed definition — `@corbits/artifact-tools` is pinnable the moment - * CL-5999's tool-pin gap closes — so it is named honestly as "not - * connected" here rather than silently omitted. */ -export const COLLATERAL_GENERATION_WIRED_SOURCES = ["Granola call notes", "Linear issues"] as const; -export const COLLATERAL_GENERATION_PENDING_SOURCES = ["workbench artifacts"] as const; - -/** - * Tool packages this definition pins (CL-5999), one per wired source in - * `COLLATERAL_GENERATION_WIRED_SOURCES`; see the header comment for why - * `@corbits/artifact-tools` stays unpinned. - */ -export const COLLATERAL_GENERATION_TOOL_PACKAGE_PINS: readonly ToolPackagePin[] = [ - { name: "@corbits/granola-tools", version: "0.0.5" }, - { name: "@corbits/linear-tools", version: "0.0.5" }, -]; - -/** - * Binds `@corbits/granola-tools`' and `@corbits/linear-tools`' declared - * handles to their tenant-owned credentials (CL-6028); see - * `workflows/granola-call/src/index.ts`'s sibling constant for the full - * rationale. - */ -export const COLLATERAL_GENERATION_CREDENTIAL_BINDINGS: readonly CredentialBinding[] = [ - { - package: "@corbits/granola-tools", - handle: "granola", - provider: "granola", - locator: "tenant", - }, - { - package: "@corbits/linear-tools", - handle: "linear", - provider: "linear", - locator: "tenant", - }, -]; - -const CONTENT_TYPE_LINES = COLLATERAL_CONTENT_TYPES.map( - (type) => `- "${type.id}" (${type.label}): ${type.guidance}`, -).join("\n"); - -export const COLLATERAL_GENERATION_SYSTEM_PROMPT = [ - "You draft marketing collateral from one or more sources the sender " + - "picks, in one or more content types the sender picks, with a " + - "swipe review on every draft and exactly one human approval before " + - "anything is finalized.", - - "Source pick: first, ask which source(s) to draw from — Granola call " + - "notes (fetched by note id with granola_get_note), Linear issues " + - "(from linear_list_recent_issues), and/or text pasted directly into " + - "the conversation. Workbench artifacts are not yet a reachable " + - "source for this workflow — say so plainly if asked, never pretend " + - "to read one. Wait for the sender's answer before doing anything " + - "else.", - - "Content-type pick: once you know the sources, ask which content " + - `type(s) to draft, offering exactly these choices:\n${CONTENT_TYPE_LINES}\n` + - "A run can draft more than one type. Wait for the sender's answer.", - - "Gathering: fetch every picked source with the matching tool. A tool " + - "result that comes back as an error means that source is not " + - "reachable right now — say so plainly and continue with whatever " + - "did load; never fail the whole run because one source is " + - "unavailable, and never fabricate content for a source you could " + - "not reach. If nothing you can reach yields any content and no " + - "pasted text was given either, you have nothing to draft from: " + - `call ${COLLATERAL_GENERATION_FINALIZE_TOOL_NAME} with a single ` + - 'teaching piece — title "Nothing to draft from", content type ' + - '"status-note", and a body naming exactly which sources you tried, ' + - "which were unreachable or not picked, and the concrete next step " + - "(paste text directly, supply a reachable note id or issue, or " + - "confirm the source is connected) — so a record of the attempt " + - "still lands in the Library instead of a bare reply with no " + - "artifact at all. Never invent source material.", - - "Drafting: draft one piece per picked content type, each grounded " + - "only in the gathered source material, following that type's " + - "guidance above. Ground every claim in the source material — never " + - "invent facts, quotes, or specifics the sources do not contain. " + - "These pieces are public: strip or generalize any customer- or " + - "company-identifying detail you find in the sources. Write plainly " + - "and specifically — no marketing buzzwords (synergy, leverage, " + - "unlock, streamline, game-changing, best-in-class), no em dashes, " + - "no hollow superlatives.", - - "Swipe review: present every draft to the sender and ask for Good, " + - "Bad, or Regenerate on each, with optional feedback. For any piece " + - "marked Bad or Regenerate, revise it once — using the feedback " + - "given, or otherwise making a materially different pass — and " + - "never revise the same piece a second time. If the sender still " + - "rejects a piece after that one revision, drop it from the final " + - "set rather than trying again.", - - `Finalizing: once every drafted piece has a final disposition — ` + - "approved, or dropped after its one revision — call " + - `${COLLATERAL_GENERATION_FINALIZE_TOOL_NAME} exactly once with the ` + - "full set of approved pieces (title, content type, and body for " + - "each). This call requires a human's approval before it completes. " + - "Never call it more than once per run, with an empty set, or with " + - "a piece the sender did not approve.", - - "If the call succeeds, present the finalized pieces as your reply — " + - "each title and body, clearly formatted — with no commentary about " + - "the approval mechanism itself. If the call is denied, reply with " + - "one calm, plain sentence that the pieces were not approved and no " + - "action was taken; never present a denial as an error, and never " + - "apologize as if something broke.", -].join("\n\n"); - -/** - * Everything the definition needs that is per-deployment data. The - * trigger address names a specific deployment's inbox — for this - * workflow, the address a human messages to start a run — so a - * definition built here is per-deployment by construction. - */ -export interface CollateralGenerationWorkflowInput { - /** The deployment's mail address; each inbound mail is one run. */ - readonly triggerAddress: string; - /** Provider/model preferences, in order; resolved at deploy time. */ - readonly inferencePreferences: readonly InferencePreference[]; - /** Per-turn timeout in milliseconds, enforced on the single step. */ - readonly turnTimeoutMs: number; -} - -/** - * Builds the collateral-generation definition. Exactly one step, - * matching the shape every other definition in this repo commits to. - * - * The step always sets an explicit `timeout` — the singular `agent:` - * shorthand sets none, and a wedged inference call (or one parked on an - * unresolved approval past a sane bound) would otherwise hang a run - * forever. Tools are never inlined on the definition: they arrive as - * packages on the deploy, keeping the definition pure data. - */ -export function buildCollateralGenerationWorkflow( - input: CollateralGenerationWorkflowInput, -): WorkflowDefinition { - if (input.triggerAddress === "") { - throw new Error("buildCollateralGenerationWorkflow requires a non-empty triggerAddress"); - } - if (!Number.isInteger(input.turnTimeoutMs) || input.turnTimeoutMs <= 0) { - throw new Error( - "buildCollateralGenerationWorkflow requires turnTimeoutMs to be a positive integer", - ); - } - return defineWorkflow({ - id: COLLATERAL_GENERATION_WORKFLOW_ID, - trigger: { type: "mail", to: input.triggerAddress }, - credentialBindings: COLLATERAL_GENERATION_CREDENTIAL_BINDINGS, - steps: { - [COLLATERAL_GENERATION_STEP_ID]: step({ - agent: { - id: COLLATERAL_GENERATION_STEP_ID, - description: - "Drafts marketing collateral from picked sources and " + - "content types, with a swipe review per piece and one " + - "human approval on the final approved set", - systemPrompt: COLLATERAL_GENERATION_SYSTEM_PROMPT, - toolFactories: [], - capabilities: [], - inference: { sources: input.inferencePreferences }, - toolPackagePins: COLLATERAL_GENERATION_TOOL_PACKAGE_PINS, - } satisfies AgentDefinition, - timeout: input.turnTimeoutMs, - }), - }, - }); -} - -/** - * Serializes a definition to the JSON a workflow asset carries. The - * definition must survive the asset round-trip byte-faithfully, so - * anything JSON would silently drop or mangle — functions, undefined, - * symbols, bigints, non-finite numbers, class instances — is a loud - * error naming the offending path instead of a corrupted asset. - */ -export function serializeCollateralGenerationWorkflow(definition: WorkflowDefinition): string { - assertJsonPortable(definition, "definition"); - return JSON.stringify(definition); -} - -function assertJsonPortable(value: unknown, path: string): void { - if (value === null) return; - switch (typeof value) { - case "string": - case "boolean": - return; - case "number": - if (!Number.isFinite(value)) { - throw new Error(`${path} is a non-finite number; JSON drops it`); - } - return; - case "object": - break; - default: - throw new Error( - `${path} is a ${typeof value}, which does not survive JSON ` + "serialization", - ); - } - if (Array.isArray(value)) { - value.forEach((element, index) => { - assertJsonPortable(element, `${path}[${index}]`); - }); - return; - } - const proto: unknown = Object.getPrototypeOf(value); - if (proto !== Object.prototype && proto !== null) { - throw new Error(`${path} is a non-plain object; JSON would flatten it lossily`); - } - for (const [key, entry] of Object.entries(value)) { - assertJsonPortable(entry, `${path}.${key}`); - } -} - -export { - COLLATERAL_GENERATION_FINALIZE_TOOL, - COLLATERAL_GENERATION_FINALIZE_TOOL_NAME, - COLLATERAL_GENERATION_FINALIZE_DESCRIPTION, - buildArtifactPayloads, -} from "./finalize-tool"; -export type { ArtifactPayload, CollateralPiece, FinalizeArgs } from "./finalize-tool"; diff --git a/workflows/collateral-generation/test/boundary.test.ts b/workflows/collateral-generation/test/boundary.test.ts deleted file mode 100644 index 147a2f795d..0000000000 --- a/workflows/collateral-generation/test/boundary.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -// This package is installable data on the native workflow contract: -// its shipped sources import only published platform packages, so the -// source tree plus the package manifest deploys on any Interchange -// instance without a wrapper contract. Tool packages this workflow -// expects at deploy time (@corbits/granola-tools, @corbits/linear-tools, -// @corbits/artifact-tools) are pinned by the deployer, never imported -// here — see src/index.ts's header comment. - -import { readFile, readdir } from "node:fs/promises"; -import path from "node:path"; -import { expect, test } from "bun:test"; - -const ALLOWED_IMPORT_PREFIXES = ["@intx/", "arktype", "./", "../"]; - -async function listFiles(dir: string): Promise { - const entries = await readdir(dir, { withFileTypes: true }); - const files: string[] = []; - for (const entry of entries) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...(await listFiles(full))); - } else { - files.push(full); - } - } - return files; -} - -async function shippedFiles(): Promise { - const packageRoot = path.join(import.meta.dir, ".."); - return [ - ...(await listFiles(path.join(packageRoot, "src"))), - path.join(packageRoot, "package.json"), - ]; -} - -test("shipped sources import only published platform packages", async () => { - const importPattern = /from\s+"([^"]+)"/g; - const violations: string[] = []; - for (const file of await shippedFiles()) { - if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue; - const content = await readFile(file, "utf8"); - for (const match of content.matchAll(importPattern)) { - const specifier = match[1] ?? ""; - const allowed = ALLOWED_IMPORT_PREFIXES.some((prefix) => specifier.startsWith(prefix)); - if (!allowed) { - violations.push(`${path.basename(file)}: ${specifier}`); - } - } - } - expect(violations).toEqual([]); -}); diff --git a/workflows/collateral-generation/test/definition.test.ts b/workflows/collateral-generation/test/definition.test.ts deleted file mode 100644 index ee7923468a..0000000000 --- a/workflows/collateral-generation/test/definition.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -// Tests for this package's own contract: the shape our factory -// commits to, its serialization guarantees, and its boundary. The -// platform's own normalization and validation are its business, not -// re-proven here. - -import { expect, test } from "bun:test"; -import type { StepPrimitive, WorkflowDefinition } from "@intx/workflow"; - -import { - COLLATERAL_CONTENT_TYPES, - COLLATERAL_GENERATION_CREDENTIAL_BINDINGS, - COLLATERAL_GENERATION_FINALIZE_TOOL_NAME, - COLLATERAL_GENERATION_PENDING_SOURCES, - COLLATERAL_GENERATION_STEP_ID, - COLLATERAL_GENERATION_SYSTEM_PROMPT, - COLLATERAL_GENERATION_TOOL_PACKAGE_PINS, - COLLATERAL_GENERATION_WIRED_SOURCES, - COLLATERAL_GENERATION_WORKFLOW_ID, - buildCollateralGenerationWorkflow, - serializeCollateralGenerationWorkflow, -} from "../src/index"; - -const INPUT = { - triggerAddress: "ins_dep000000000000@example.test", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 300000, -} as const; - -function collateralStep(definition: WorkflowDefinition): StepPrimitive { - const primitive = definition.steps[COLLATERAL_GENERATION_STEP_ID]; - if (primitive === undefined || primitive.kind !== "step") { - throw new Error(`definition has no step primitive named ${COLLATERAL_GENERATION_STEP_ID}`); - } - return primitive; -} - -test("the definition has exactly one step", () => { - const definition = buildCollateralGenerationWorkflow(INPUT); - expect(definition.stepOrder).toEqual([COLLATERAL_GENERATION_STEP_ID]); - expect(Object.keys(definition.steps)).toEqual([COLLATERAL_GENERATION_STEP_ID]); -}); - -test("the step carries an explicit per-turn timeout", () => { - const definition = buildCollateralGenerationWorkflow(INPUT); - expect(collateralStep(definition).timeout).toBe(INPUT.turnTimeoutMs); -}); - -test("the workflow is triggered by mail to the given deployment address", () => { - const definition = buildCollateralGenerationWorkflow(INPUT); - expect(definition.id).toBe(COLLATERAL_GENERATION_WORKFLOW_ID); - expect(definition.triggers).toEqual([{ type: "mail", to: INPUT.triggerAddress }]); -}); - -test("the agent carries the fixed prompt, the preferences, and inlines no tools", () => { - const agent = collateralStep(buildCollateralGenerationWorkflow(INPUT)).agent; - expect(agent.systemPrompt).toBe(COLLATERAL_GENERATION_SYSTEM_PROMPT); - expect(agent.inference.sources).toEqual([...INPUT.inferencePreferences]); - // Tools arrive as packages on the deploy, never inlined here: an - // inline factory is a function-valued field the asset cannot carry. - expect(agent.toolFactories).toEqual([]); -}); - -test("the agent pins @corbits/granola-tools and @corbits/linear-tools by name and version", () => { - const agent = collateralStep(buildCollateralGenerationWorkflow(INPUT)).agent; - expect(agent.toolPackagePins).toEqual([...COLLATERAL_GENERATION_TOOL_PACKAGE_PINS]); - expect(COLLATERAL_GENERATION_TOOL_PACKAGE_PINS).toEqual([ - { name: "@corbits/granola-tools", version: "0.0.5" }, - { name: "@corbits/linear-tools", version: "0.0.5" }, - ]); -}); - -test("the system prompt names the exact approval-gated finalize tool", () => { - expect(COLLATERAL_GENERATION_SYSTEM_PROMPT).toContain(COLLATERAL_GENERATION_FINALIZE_TOOL_NAME); -}); - -test("the system prompt offers every content type by its exact id", () => { - for (const type of COLLATERAL_CONTENT_TYPES) { - expect(COLLATERAL_GENERATION_SYSTEM_PROMPT).toContain(type.id); - } - expect(COLLATERAL_CONTENT_TYPES).toHaveLength(7); -}); - -test("the system prompt names the wired sources' real tools, and is honest about the pending one", () => { - expect(COLLATERAL_GENERATION_SYSTEM_PROMPT).toContain("granola_get_note"); - expect(COLLATERAL_GENERATION_SYSTEM_PROMPT).toContain("linear_list_recent_issues"); - expect(COLLATERAL_GENERATION_WIRED_SOURCES).toEqual(["Granola call notes", "Linear issues"]); - expect(COLLATERAL_GENERATION_PENDING_SOURCES).toEqual(["workbench artifacts"]); - expect(COLLATERAL_GENERATION_SYSTEM_PROMPT).toMatch( - /workbench artifacts are not yet a reachable source/i, - ); -}); - -test("the system prompt commits to an honest 'nothing to draft from' failure, not fabricated source material", () => { - expect(COLLATERAL_GENERATION_SYSTEM_PROMPT).toMatch(/nothing to draft from/i); - expect(COLLATERAL_GENERATION_SYSTEM_PROMPT).toMatch(/never invent/i); -}); - -test("the system prompt commits to a teaching artifact, not silence, when nothing is reachable", () => { - expect(COLLATERAL_GENERATION_SYSTEM_PROMPT).toContain(COLLATERAL_GENERATION_FINALIZE_TOOL_NAME); - expect(COLLATERAL_GENERATION_SYSTEM_PROMPT).toMatch(/status-note/i); -}); - -test("the system prompt caps a rejected piece at one revise pass", () => { - expect(COLLATERAL_GENERATION_SYSTEM_PROMPT).toMatch(/never revise the same piece a second time/i); -}); - -test("the system prompt collapses review into one approval, not a second gate", () => { - expect(COLLATERAL_GENERATION_SYSTEM_PROMPT).toMatch( - /exactly once with the full set of approved pieces/i, - ); -}); - -test("the system prompt commits to a calm terminal reply on denial, not an error", () => { - expect(COLLATERAL_GENERATION_SYSTEM_PROMPT).toMatch(/not approved/i); - expect(COLLATERAL_GENERATION_SYSTEM_PROMPT).toMatch(/never present a denial as an error/i); -}); - -test("the definition binds @corbits/granola-tools' and @corbits/linear-tools' declared handles to their tenant-owned credentials", () => { - const definition = buildCollateralGenerationWorkflow(INPUT); - expect(definition.credentialBindings).toEqual([...COLLATERAL_GENERATION_CREDENTIAL_BINDINGS]); - expect(COLLATERAL_GENERATION_CREDENTIAL_BINDINGS).toEqual([ - { - package: "@corbits/granola-tools", - handle: "granola", - provider: "granola", - locator: "tenant", - }, - { - package: "@corbits/linear-tools", - handle: "linear", - provider: "linear", - locator: "tenant", - }, - ]); -}); - -test("the definition survives the workflow-asset JSON round-trip", () => { - const definition = buildCollateralGenerationWorkflow(INPUT); - const revived: unknown = JSON.parse(serializeCollateralGenerationWorkflow(definition)); - expect(revived).toEqual(definition); -}); - -test("an empty trigger address is rejected", () => { - expect(() => buildCollateralGenerationWorkflow({ ...INPUT, triggerAddress: "" })).toThrow( - /triggerAddress/, - ); -}); - -test("a non-positive or fractional turn timeout is rejected", () => { - expect(() => buildCollateralGenerationWorkflow({ ...INPUT, turnTimeoutMs: 0 })).toThrow( - /turnTimeoutMs/, - ); - expect(() => buildCollateralGenerationWorkflow({ ...INPUT, turnTimeoutMs: 0.5 })).toThrow( - /turnTimeoutMs/, - ); -}); diff --git a/workflows/collateral-generation/tsconfig.json b/workflows/collateral-generation/tsconfig.json deleted file mode 100644 index a8c2b9966d..0000000000 --- a/workflows/collateral-generation/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src", "test"], - "exclude": [] -} diff --git a/workflows/collateral-generation/tsconfig.src.json b/workflows/collateral-generation/tsconfig.src.json deleted file mode 100644 index fc8724305f..0000000000 --- a/workflows/collateral-generation/tsconfig.src.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } -} diff --git a/workflows/credential-expiry/LICENSE b/workflows/credential-expiry/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/workflows/credential-expiry/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/workflows/credential-expiry/README.md b/workflows/credential-expiry/README.md deleted file mode 100644 index a6017a099e..0000000000 --- a/workflows/credential-expiry/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# @corbits/credential-expiry-workflow - -A mail-triggered workflow that checks this tenant's credentials on each -trigger and mails a reconnect notice for any credential that is `active` -but past its own `expiresAt`. `@corbits/cron` delivers a tick mail once -a day. Replaces the hub-owned periodic loop -`apps/hub/src/credential-expiry-sweep.ts` (CL-8181): the same decision -runs as a deployed Routine instead of inside the hub process. - -## What it does - -One step, one agent: it calls `credential_expiry_check` (this package's -own read-only tool) exactly once. That tool reads the stock -`GET /api/tenants/:tenantId/credentials` (paired with `/providers` for -display names) with the run's own bearer, and -`findDueCredentialExpiries` (`./src/decide.ts`) decides which rows are -`active` and already past their own `expiresAt`. The step's reply is -the reconnect notice itself — one sentence when nothing is due, or a -short notice naming each due credential — delivered as mail by the host -the same way `@corbits/heartbeat-workflow` and -`@corbits/workbench-digest-workflow` deliver theirs. No separate -mail-send tool exists in this repo yet. - -## Scope note - -The hub sweep this replaces also refreshed `mcp:` OAuth tokens in -place before ever mailing anyone (CL-6207). That native-refresh path -mutates a stored credential's secret directly, which no stock tenant -route exposes to a workflow run, so it stays a hub concern and is not -carried over here — this workflow only reads and reports. - -## Usage - -```ts -import { - buildCredentialExpiryWorkflow, - serializeCredentialExpiryWorkflow, -} from "@corbits/credential-expiry-workflow"; - -const definition = buildCredentialExpiryWorkflow({ - triggerAddress: "ins_dep000000000000@example.test", - inferencePreferences: [{ provider: "anthropic", model: "claude-haiku" }], - turnTimeoutMs: 60_000, -}); - -const json = serializeCredentialExpiryWorkflow(definition); -``` - -See [`workflows/README.md`](../README.md#status-note) for what -registration/automatable/seeded mean. - -## Deploy from the npm registry - -Once published, deploy this workflow definition to a tenant by name and -version pin: - -``` -POST /api/tenants/:id/workflows/deployments -{ - "source": { "kind": "registry", "registry": "npm" }, - "entry": "./src/index.ts", - "pin": "@corbits/credential-expiry-workflow@0.0.1", - "sourceOfferingIds": ["", ...], - "defaultSourceOfferingId": "" -} -``` - -`entry` is the package's `interchange.workflow` module path; `pin` is -`"@corbits/credential-expiry-workflow@0.0.1"` or a semver range on the -same name. The hub installs, probes, gates, and freezes the definition -from the registry tarball before creating the deployment. diff --git a/workflows/credential-expiry/package.json b/workflows/credential-expiry/package.json deleted file mode 100644 index dc2cbbf47e..0000000000 --- a/workflows/credential-expiry/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@corbits/credential-expiry-workflow", - "version": "0.0.1", - "description": "Scheduled workflow that checks this tenant's credentials and mails a reconnect notice for any active credential past its own expiresAt, replacing the hub-owned credential-expiry sweep", - "license": "LGPL-2.1-or-later", - "repository": { - "type": "git", - "url": "https://github.com/corbitsdev/workbench.git", - "directory": "workflows/credential-expiry" - }, - "files": [ - "src", - "LICENSE", - "README.md" - ], - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "arktype": "catalog:" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/workflow": "0.3.0" - }, - "corbits": { - "workflow": { - "assetName": "credential-expiry", - "displayName": "Credential expiry check", - "automatable": true - } - }, - "interchange": { - "workflow": "./src/index.ts" - } -} diff --git a/workflows/credential-expiry/src/client.ts b/workflows/credential-expiry/src/client.ts deleted file mode 100644 index 0d669f7bb4..0000000000 --- a/workflows/credential-expiry/src/client.ts +++ /dev/null @@ -1,105 +0,0 @@ -// A minimal client for the one stock Interchange surface this tool -// reads: `GET /api/tenants/:tenantId/credentials` and `/providers`, with -// the workflow run's own bearer (sidecar token plus run address), the -// same pairing `@corbits/connections-tools`' `listConnectedProviders` -// uses (`tools/connections/src/client.ts`) — this package -// cannot depend on that one directly (a workflow ships as its own npm -// tarball; `@corbits/connections-tools` is not part of that surface), -// so the fetch/paginate shape is duplicated rather than imported. -import { type } from "arktype"; - -import type { ExpiringCredential } from "./decide"; - -export interface CredentialExpiryClientConfig { - /** The hub's plain HTTP origin. */ - readonly hubConnectionsUrl: string; - readonly tenantId: string; - readonly sidecarToken: string; - readonly address: string; - /** Override for tests; defaults to the global `fetch`. */ - readonly fetchImpl?: typeof fetch; -} - -const ProvidersPage = type({ - data: type({ id: "string", name: "string" }).array(), - "nextCursor?": "string | null", -}); - -const CredentialsPage = type({ - data: type({ - id: "string", - providerId: "string", - name: "string", - status: "'active'|'expired'|'revoked'|'error'", - "expiresAt?": "string | null", - }).array(), - "nextCursor?": "string | null", -}); - -const PAGE_LIMIT = 100; - -function authHeaders(config: CredentialExpiryClientConfig): Record { - return { - authorization: `Bearer ${config.sidecarToken}`, - "x-workflow-run-address": config.address, - }; -} - -function tenantBase(config: CredentialExpiryClientConfig): string { - return `${config.hubConnectionsUrl}/api/tenants/${encodeURIComponent(config.tenantId)}`; -} - -/** Walks every page: a truncated first page would read as "nothing is - * expiring," a wrong answer rather than a slow one. */ -async function readAllPages( - config: CredentialExpiryClientConfig, - path: string, - what: string, - parse: ( - body: unknown, - ) => { data: readonly T[]; nextCursor?: string | null | undefined } | type.errors, -): Promise { - const doFetch = config.fetchImpl ?? fetch; - const items: T[] = []; - let cursor: string | undefined; - for (;;) { - const params = new URLSearchParams({ limit: String(PAGE_LIMIT) }); - if (cursor !== undefined) params.set("cursor", cursor); - const response = await doFetch(`${tenantBase(config)}${path}?${params.toString()}`, { - headers: authHeaders(config), - }); - if (!response.ok) { - throw new Error(`${what} failed: ${response.status} ${response.statusText}`); - } - const parsed = parse(await response.json()); - if (parsed instanceof type.errors) { - throw new Error(`${what} came back in an unexpected shape: ${parsed.summary}`); - } - items.push(...parsed.data); - const next = parsed.nextCursor; - if (next === undefined || next === null) return items; - cursor = next; - } -} - -/** - * Every credential this tenant holds, paired with its owning provider's - * display name. Throws on any transport, HTTP, or shape failure — - * never fabricates a result, matching `listConnectedProviders`. - */ -export async function fetchTenantCredentials( - config: CredentialExpiryClientConfig, -): Promise { - const [providers, credentials] = await Promise.all([ - readAllPages(config, "/providers", "Listing providers", (body) => ProvidersPage(body)), - readAllPages(config, "/credentials", "Listing credentials", (body) => CredentialsPage(body)), - ]); - const providerNameById = new Map(providers.map((provider) => [provider.id, provider.name])); - return credentials.map((row) => ({ - credentialId: row.id, - name: row.name, - providerLabel: providerNameById.get(row.providerId) ?? row.providerId, - status: row.status, - expiresAt: row.expiresAt, - })); -} diff --git a/workflows/credential-expiry/src/decide.test.ts b/workflows/credential-expiry/src/decide.test.ts deleted file mode 100644 index cff4fd42bb..0000000000 --- a/workflows/credential-expiry/src/decide.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Moved from packages/notify/test/credential-expiry.test.ts (CL-8181): -// the pure decision logic now lives in this workflow package instead of -// the hub-owned `@corbits/notify` (which needs `@intx/db`/`postgres` and -// can never be a dependency of installable workflow data), and its -// candidate shape dropped `tenantId`/`providerId`/`recipients` — fields -// only the DB-backed sweep needed — for the stock credential row this -// workflow's own tool fetches instead. -import { describe, expect, test } from "bun:test"; - -import { findDueCredentialExpiries } from "./decide"; -import type { ExpiringCredential } from "./decide"; - -const now = new Date("2026-08-13T12:00:00.000Z"); - -function credential(overrides: Partial = {}): ExpiringCredential { - return { - credentialId: "cred_1", - name: "Hugging Face", - providerLabel: "huggingface", - status: "active", - expiresAt: "2026-08-13T11:00:00.000Z", - ...overrides, - }; -} - -describe("findDueCredentialExpiries", () => { - test("an active credential past its expiry is due", () => { - const due = findDueCredentialExpiries([credential()], now); - expect(due).toHaveLength(1); - expect(due[0]?.credential).toEqual(credential()); - }); - - test("a credential expiring exactly at now is due", () => { - const due = findDueCredentialExpiries([credential({ expiresAt: now.toISOString() })], now); - expect(due).toHaveLength(1); - }); - - test("a credential not yet expired is not due", () => { - const due = findDueCredentialExpiries( - [credential({ expiresAt: "2026-08-13T13:00:00.000Z" })], - now, - ); - expect(due).toHaveLength(0); - }); - - test("a durable credential with no expiry is never due", () => { - const due = findDueCredentialExpiries([credential({ expiresAt: undefined })], now); - expect(due).toHaveLength(0); - }); - - test("a null expiresAt (the stock route's shape for 'none') is never due", () => { - const due = findDueCredentialExpiries([credential({ expiresAt: null })], now); - expect(due).toHaveLength(0); - }); - - test("an already-expired credential is not re-decided", () => { - const due = findDueCredentialExpiries([credential({ status: "expired" })], now); - expect(due).toHaveLength(0); - }); - - test("a revoked or errored credential is not swept either", () => { - const due = findDueCredentialExpiries( - [credential({ status: "revoked" }), credential({ status: "error" })], - now, - ); - expect(due).toHaveLength(0); - }); - - test("an unparseable expiresAt is skipped rather than crashing", () => { - const due = findDueCredentialExpiries([credential({ expiresAt: "not-a-date" })], now); - expect(due).toHaveLength(0); - }); - - test("preserves candidate order across a mixed batch", () => { - const due = findDueCredentialExpiries( - [ - credential({ credentialId: "cred_a" }), - credential({ credentialId: "cred_b", expiresAt: undefined }), - credential({ credentialId: "cred_c" }), - ], - now, - ); - expect(due.map((d) => d.credential.credentialId)).toEqual(["cred_a", "cred_c"]); - }); -}); diff --git a/workflows/credential-expiry/src/decide.ts b/workflows/credential-expiry/src/decide.ts deleted file mode 100644 index 798d42fa0c..0000000000 --- a/workflows/credential-expiry/src/decide.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Pure decision layer for the reconnect-notice sweep (CL-8181): given the -// stock-shaped credential rows this workflow's own tool fetches from -// `GET /api/tenants/:tenantId/credentials`, decide which are `active` -// and already past their own `expiresAt`. Ported from the hub's -// `credential-expiry-sweep.ts` (CL-5988), which decided the same thing -// over DB rows and one specific provider's `metadata.expiresAt`; this -// workflow reads the credential's real `expiresAt` column through the -// stock route instead, so the decision generalizes across every -// provider rather than a curated `SWEPT_PROVIDER_LABELS` list. -// -// Touches no network and sends no mail — the caller (`./tool.ts`) is -// responsible for fetching candidates and turning a due credential into -// a reconnect notice. - -export type CredentialStatus = "active" | "expired" | "revoked" | "error"; - -export type ExpiringCredential = { - readonly credentialId: string; - readonly name: string; - readonly providerLabel: string; - readonly status: CredentialStatus; - /** ISO instant from the credential's own `expiresAt` column; - * `undefined`/`null` for a durable credential with no expiry. */ - readonly expiresAt: string | null | undefined; -}; - -export type DueCredentialExpiry = { - readonly credential: ExpiringCredential; -}; - -/** - * Which of `candidates` are `active` and past their own `expiresAt` as - * of `now`. An already `expired`/`revoked`/`error` row is never - * re-decided here, and an unparseable or absent `expiresAt` is skipped - * rather than guessed at. Order is preserved from `candidates`. - */ -export function findDueCredentialExpiries( - candidates: readonly ExpiringCredential[], - now: Date, -): readonly DueCredentialExpiry[] { - const due: DueCredentialExpiry[] = []; - for (const candidate of candidates) { - if (candidate.status !== "active") continue; - if (candidate.expiresAt === null || candidate.expiresAt === undefined) { - continue; - } - const expiresAt = new Date(candidate.expiresAt); - if (Number.isNaN(expiresAt.getTime())) continue; - if (expiresAt.getTime() > now.getTime()) continue; - due.push({ credential: candidate }); - } - return due; -} diff --git a/workflows/credential-expiry/src/index.ts b/workflows/credential-expiry/src/index.ts deleted file mode 100644 index be9ad7a6ad..0000000000 --- a/workflows/credential-expiry/src/index.ts +++ /dev/null @@ -1,162 +0,0 @@ -// The credential-expiry workflow (CL-8181): on each trigger, check this -// tenant's credentials and mail a reconnect notice for each one that is -// active but past its own `expiresAt`. Replaces the hub-owned periodic -// loop `apps/hub/src/credential-expiry-sweep.ts` — that loop's own -// decision logic now lives in `./decide.ts` (moved rather than -// reimplemented; it needs no DB and no mail infrastructure to stay -// correct), and this definition is deployed like any other Routine -// instead of running inside the hub process for every tenant at once. -// It is mail-triggered, not schedule-triggered: `@corbits/cron` -// delivers a tick mail on the daily cadence, so this definition never -// declares `type: "schedule"` itself (CL-8183). -// -// Scope narrows on the port: the old sweep also refreshed -// `mcp:` OAuth tokens in place before ever mailing anyone -// (CL-6207) — that native-refresh path stays a hub concern (it mutates -// a stored credential's secret directly, which no stock tenant route -// exposes to a workflow run) and is not carried over here. This -// workflow only reads and reports, exactly as CL-8181 scopes it. -// -// Zero DB, one HTTP read: `credential_expiry_check` (`./tool.ts`) reads -// the stock `GET /api/tenants/:tenantId/credentials` (paired with -// `/providers` for display names) with the run's own bearer, then -// `findDueCredentialExpiries` decides which are due. The step's reply -// is the reconnect notice itself, delivered as mail by the host the -// same way `@corbits/heartbeat-workflow` and -// `@corbits/workbench-digest-workflow` deliver theirs — no separate -// mail-send tool exists in this repo yet. - -import { defineAgent } from "@intx/agent"; -import type { AgentDefinition, InferencePreference } from "@intx/agent"; -import { defineWorkflow, step } from "@intx/workflow"; -import type { WorkflowDefinition } from "@intx/workflow"; - -import { CREDENTIAL_EXPIRY_CHECK_TOOL_NAME } from "./tool"; - -export const CREDENTIAL_EXPIRY_WORKFLOW_ID = "wf_credential_expiry"; -export const CREDENTIAL_EXPIRY_STEP_ID = "credential-expiry-check"; - -export const CREDENTIAL_EXPIRY_SYSTEM_PROMPT = [ - "You are a scheduled credential-expiry check for this workbench's " + - "tenant. Nobody is watching this run happen — write for the tenant " + - "owner reading it later, cold.", - `Call \`${CREDENTIAL_EXPIRY_CHECK_TOOL_NAME}\` exactly once. It returns a \`due\` list of credentials that are active but past their own expiry.`, - "If `due` is empty, reply with exactly one plain sentence saying every " + - "connected credential is current — no other text.", - "If `due` is non-empty, reply with a short reconnect notice: one " + - "opening sentence, then one bullet per due credential naming its " + - "`name` (or `providerLabel` if `name` is empty) and the `expiresAt` " + - "instant it passed, and close with one sentence asking the reader to " + - "reconnect it from the workbench's Plugins settings. Never invent a " + - "credential the tool did not return, and never omit one it did.", -].join("\n\n"); - -/** - * Everything the definition needs that is per-deployment data. The - * trigger address names a specific deployment's inbox, so a definition - * built here is per-deployment by construction. - */ -export interface CredentialExpiryWorkflowInput { - /** The deployment's mail address; each inbound mail is one run. */ - readonly triggerAddress: string; - /** Provider/model preferences, in order; resolved at deploy time. */ - readonly inferencePreferences: readonly InferencePreference[]; - /** Per-turn timeout in milliseconds, enforced on the single step. */ - readonly turnTimeoutMs: number; -} - -/** - * Builds the credential-expiry definition. Exactly one step, matching - * the shape every other definition in this repo commits to. Tools are - * never inlined on the serialized definition — `toolFactories` stays - * empty here, same as every other package in `workflows/*`, since a - * `ToolFactory` is a function and cannot survive the workflow asset's - * JSON round trip; the deploy resolves this package's own exported - * `CREDENTIAL_EXPIRY_CHECK_TOOL` the same way it resolves any other - * pinned tool package. - */ -export function buildCredentialExpiryWorkflow( - input: CredentialExpiryWorkflowInput, -): WorkflowDefinition { - if (input.triggerAddress === "") { - throw new Error("buildCredentialExpiryWorkflow requires a non-empty triggerAddress"); - } - if (!Number.isInteger(input.turnTimeoutMs) || input.turnTimeoutMs <= 0) { - throw new Error( - "buildCredentialExpiryWorkflow requires turnTimeoutMs to be a positive integer", - ); - } - return defineWorkflow({ - id: CREDENTIAL_EXPIRY_WORKFLOW_ID, - trigger: { type: "mail", to: input.triggerAddress }, - steps: { - [CREDENTIAL_EXPIRY_STEP_ID]: step({ - agent: defineAgent({ - id: CREDENTIAL_EXPIRY_STEP_ID, - description: - "Checks this tenant's credentials for ones past their own " + - "expiry and mails a reconnect notice for each", - systemPrompt: CREDENTIAL_EXPIRY_SYSTEM_PROMPT, - tools: [], - capabilities: [], - inference: { sources: input.inferencePreferences }, - }) satisfies AgentDefinition, - timeout: input.turnTimeoutMs, - }), - }, - }); -} - -/** - * Serializes a definition to the JSON a workflow asset carries. The - * definition must survive the asset round-trip byte-faithfully, so - * anything JSON would silently drop or mangle — functions, undefined, - * symbols, bigints, non-finite numbers, class instances — is a loud - * error naming the offending path instead of a corrupted asset. - */ -export function serializeCredentialExpiryWorkflow(definition: WorkflowDefinition): string { - assertJsonPortable(definition, "definition"); - return JSON.stringify(definition); -} - -function assertJsonPortable(value: unknown, path: string): void { - if (value === null) return; - switch (typeof value) { - case "string": - case "boolean": - return; - case "number": - if (!Number.isFinite(value)) { - throw new Error(`${path} is a non-finite number; JSON drops it`); - } - return; - case "object": - break; - default: - throw new Error( - `${path} is a ${typeof value}, which does not survive JSON ` + "serialization", - ); - } - if (Array.isArray(value)) { - value.forEach((element, index) => { - assertJsonPortable(element, `${path}[${index}]`); - }); - return; - } - const proto: unknown = Object.getPrototypeOf(value); - if (proto !== Object.prototype && proto !== null) { - throw new Error(`${path} is a non-plain object; JSON would flatten it lossily`); - } - for (const [key, entry] of Object.entries(value)) { - assertJsonPortable(entry, `${path}.${key}`); - } -} - -export { - CREDENTIAL_EXPIRY_CHECK_TOOL, - CREDENTIAL_EXPIRY_CHECK_TOOL_NAME, - CREDENTIAL_EXPIRY_CHECK_DESCRIPTION, -} from "./tool"; -export type { WorkflowCredentialExpiryEnv } from "./tool"; -export { findDueCredentialExpiries } from "./decide"; -export type { DueCredentialExpiry, ExpiringCredential } from "./decide"; diff --git a/workflows/credential-expiry/src/tool.ts b/workflows/credential-expiry/src/tool.ts deleted file mode 100644 index 9ae5554a36..0000000000 --- a/workflows/credential-expiry/src/tool.ts +++ /dev/null @@ -1,83 +0,0 @@ -// The one tool this workflow's step calls: read this tenant's -// credentials through the stock route and report which ones are -// `active` and past their own `expiresAt`. Kept inside the workflow -// package rather than a shared tool package — same "workflow-specific -// logic lives in the definition" convention `exa-topic-watch`'s -// `finalize-tool.ts` established — this read-only reconnect check is -// not a reusable integration on its own. -// -// Read-only: this tool sends no mail and mutates nothing, so it -// declares no `approval` key, matching `list_connections` -// (`@corbits/connections-tools`). The reconnect notice itself is this -// step's own reply text, delivered as mail by the host the same way -// `@corbits/heartbeat-workflow` and `@corbits/workbench-digest-workflow` -// deliver their replies — no separate mail-send tool exists here. -import { defineTool, type BaseEnv } from "@intx/agent"; - -import { fetchTenantCredentials } from "./client"; -import { findDueCredentialExpiries } from "./decide"; - -export const CREDENTIAL_EXPIRY_CHECK_TOOL_NAME = "credential_expiry_check"; - -export const CREDENTIAL_EXPIRY_CHECK_DESCRIPTION = - "Reads this tenant's credentials and reports which ones are active but past their own expiresAt, so a reconnect notice can be drafted for each."; - -/** The env this tool needs beyond `BaseEnv`: the run's own hub-reach - * credential, mirroring `@corbits/connections-tools`' - * `WorkflowConnectionEnv`. */ -export interface WorkflowCredentialExpiryEnv extends BaseEnv { - readonly hubConnectionsUrl: string; - readonly tenantId: string; - readonly sidecarToken: string; - readonly address: string; -} - -export const CREDENTIAL_EXPIRY_CHECK_TOOL = defineTool({ - id: "@corbits/workflow-credential-expiry/check", - requires: ["hubConnectionsUrl", "tenantId", "sidecarToken", "address"], - definitions: [{ name: CREDENTIAL_EXPIRY_CHECK_TOOL_NAME }], - factory: (env) => ({ - definitions: [ - { - name: CREDENTIAL_EXPIRY_CHECK_TOOL_NAME, - description: CREDENTIAL_EXPIRY_CHECK_DESCRIPTION, - inputSchema: { type: "object", properties: {}, required: [] }, - }, - ], - run: async (call) => { - try { - const candidates = await fetchTenantCredentials({ - hubConnectionsUrl: env.hubConnectionsUrl, - tenantId: env.tenantId, - sidecarToken: env.sidecarToken, - address: env.address, - }); - const due = findDueCredentialExpiries(candidates, new Date()); - return { - callId: call.id, - isError: false, - content: JSON.stringify({ - due: due.map(({ credential }) => ({ - credentialId: credential.credentialId, - name: credential.name, - providerLabel: credential.providerLabel, - expiresAt: credential.expiresAt, - })), - }), - }; - } catch (err) { - // report-error-ignore: this failure is surfaced to the calling - // agent as an error tool result, which it reports plainly in - // its reply rather than silently retrying — nothing here is - // swallowed. - return { - callId: call.id, - isError: true, - content: `Failed to check credential expiry: ${ - err instanceof Error ? err.message : String(err) - }`, - }; - } - }, - }), -}); diff --git a/workflows/credential-expiry/tsconfig.json b/workflows/credential-expiry/tsconfig.json deleted file mode 100644 index e125e90614..0000000000 --- a/workflows/credential-expiry/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src"], - "exclude": [] -} diff --git a/workflows/credential-expiry/tsconfig.src.json b/workflows/credential-expiry/tsconfig.src.json deleted file mode 100644 index fc8724305f..0000000000 --- a/workflows/credential-expiry/tsconfig.src.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } -} diff --git a/workflows/exa-topic-watch/LICENSE b/workflows/exa-topic-watch/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/workflows/exa-topic-watch/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/workflows/exa-topic-watch/README.md b/workflows/exa-topic-watch/README.md deleted file mode 100644 index 209c7aaa21..0000000000 --- a/workflows/exa-topic-watch/README.md +++ /dev/null @@ -1,78 +0,0 @@ -# exa-topic-watch - -A scheduled web topic watch. Each run reads one topic from the message -that started it, searches the live web, and publishes a short digest of -what moved — held for a human's approval before anything is saved. - -Ported from the OG gtm-workbench's `exa-topic-watch` (CL-6349). - -## The run - -One reasoning turn: - -1. Read the topic off the triggering mail. -2. Discover and call the web-search tools on the connected Exa MCP server - (`mcp_list_tools`, then `mcp_read` — read-only, no approval). -3. Write the digest: **What moved**, **Takeaways**, **Worth a closer look**. -4. Call `exa_topic_watch_finalize` once. That call is `approval: "ask"`: - the run suspends until a human approves, then the digest is persisted - as a Library artifact. - -A quiet week still finalizes, with `outcome: "status-note"` — so every -fire leaves a visible trace instead of silence. - -## What this port changed, and why - -- **Six steps folded into one.** Five of the OG's steps were native - `action` primitives. This repo's production host leaves `invokeAction` - undefined, so an `action` step would deploy as something nothing can - dispatch. Folding also deleted the OG's `exa_topic_watch_prepare_search` - rename tool outright: an agent names a search tool's arguments itself. -- **Exa through the MCP preset**, not a bespoke `@workbench/tools-exa` - package. `@corbits/mcp-tools` is one of the few tool packages this repo - publishes to its own registry, so a pin on it resolves at deploy time. - The `mcp:exa` credential handle is dynamic tenant data, so the host - supplies its binding (`apps/hub/src/mcp-credential-bindings.ts`) — this - definition declares none. -- **Publishing is approval-gated.** The OG persisted through an ungated - `write_artifact` action. Every external side effect here sits behind - human approval (`AGENTS.md`). -- **The trigger is the topic.** The OG parked on `awaitSignal("intake")`. - Workbench collects the topic on the routine before launch and delivers - it as the first-turn mail; a gate nobody fulfils would hang the run. - -## Known gaps - -- `toolPackagePins` resolution is proven for single-step definitions only. - This definition is single-step by design, so it stays inside what the - deploy path has actually run. -- The digest has no memory of the previous run. "What moved since last - time" is the model reading publication dates, not a stored watermark. - -## Registration - -See [`workflows/README.md`](../README.md#status-note) for what -registration/automatable/seeded mean — this one is `automatable: true` -(it is built to run on a schedule) and not seeded by default: it needs an -Exa connection not every tenant has. - -## Deploy from the npm registry - -Once published, deploy this workflow definition to a tenant by name and -version pin: - -``` -POST /api/tenants/:id/workflows/deployments -{ - "source": { "kind": "registry", "registry": "npm" }, - "entry": "./src/index.ts", - "pin": "@corbits/exa-topic-watch-workflow@0.0.1", - "sourceOfferingIds": ["", ...], - "defaultSourceOfferingId": "" -} -``` - -`entry` is the package's `interchange.workflow` module path; `pin` is -`"@corbits/exa-topic-watch-workflow@0.0.1"` or a semver range on the same name. The hub -installs, probes, gates, and freezes the definition from the registry -tarball before creating the deployment. diff --git a/workflows/exa-topic-watch/package.json b/workflows/exa-topic-watch/package.json deleted file mode 100644 index 131b2121a6..0000000000 --- a/workflows/exa-topic-watch/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@corbits/exa-topic-watch-workflow", - "version": "0.0.1", - "description": "Scheduled web topic watch: searches the live web for one topic each run and publishes a short digest, held for approval", - "license": "LGPL-2.1-or-later", - "repository": { - "type": "git", - "url": "https://github.com/corbitsdev/workbench.git", - "directory": "workflows/exa-topic-watch" - }, - "files": [ - "src", - "LICENSE", - "README.md" - ], - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "arktype": "catalog:" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0" - }, - "corbits": { - "workflow": { - "assetName": "exa-topic-watch", - "displayName": "Web topic watch", - "automatable": true - } - }, - "interchange": { - "workflow": "./src/index.ts" - } -} diff --git a/workflows/exa-topic-watch/src/artifact-client.test.ts b/workflows/exa-topic-watch/src/artifact-client.test.ts deleted file mode 100644 index 1c86414419..0000000000 --- a/workflows/exa-topic-watch/src/artifact-client.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { expect, test } from "bun:test"; - -import { createWorkflowArtifact } from "./artifact-client"; - -const CONFIG = { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - runAddress: "run_1@workflow", -}; - -test("posts to the workflow-artifacts endpoint and returns id/version", async () => { - const captured: { url: string; init: RequestInit | undefined } = { - url: "", - init: undefined, - }; - const fetchImpl = (async (input: URL | string, init?: RequestInit) => { - captured.url = String(input); - captured.init = init; - return new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - }); - }) as unknown as typeof fetch; - - const result = await createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ); - - expect(result).toEqual({ id: "art_1", version: 1 }); - expect(captured.url).toBe("https://hub.example.com/api/workflow-artifacts/artifacts"); - const headers = captured.init?.headers as Record; - expect(headers["authorization"]).toBe("Bearer sc-token"); - expect(headers["x-workflow-run-address"]).toBe("run_1@workflow"); -}); - -test("throws on a non-ok HTTP response", async () => { - const fetchImpl = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/500/); -}); - -test("throws when the response doesn't match the expected shape", async () => { - const fetchImpl = (async () => - new Response(JSON.stringify({ data: {} }), { - status: 201, - })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/did not match the expected shape/); -}); diff --git a/workflows/exa-topic-watch/src/artifact-client.ts b/workflows/exa-topic-watch/src/artifact-client.ts deleted file mode 100644 index b9d56ca425..0000000000 --- a/workflows/exa-topic-watch/src/artifact-client.ts +++ /dev/null @@ -1,62 +0,0 @@ -// A minimal client for the sanctioned workflow-artifacts HTTP surface -// (`@corbits/artifacts-hub`'s `createWorkflowArtifactRoutes`, CL-6000). -// Deliberately duplicated from `@corbits/artifact-tools`' `client.ts` -// rather than imported: this package ships as installable data on the -// native workflow contract (see `./index.ts`'s header comment and -// `test/boundary.test.ts`), so its shipped sources import only `@intx/*`, -// `arktype`, and relative specifiers — never another `@corbits/*` -// package. Authenticates with the sidecar's own bearer token plus the -// run's own mailbox address (both already reach a workflow-process -// child's tool env), never a database handle. -import { type } from "arktype"; - -export interface WorkflowArtifactClientConfig { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly runAddress: string; - /** Override for tests; defaults to the global `fetch`. */ - readonly fetchImpl?: typeof fetch; -} - -export type CreateWorkflowArtifactInput = { - readonly title: string; - readonly kind: string; - readonly content: string; -}; - -export type CreatedWorkflowArtifact = { - readonly id: string; - readonly version: number; -}; - -const CreatedWorkflowArtifactResponse = type({ - data: { id: "string", version: "number" }, -}); - -/** Persists one artifact. Throws on any transport, HTTP, or shape failure. */ -export async function createWorkflowArtifact( - config: WorkflowArtifactClientConfig, - input: CreateWorkflowArtifactInput, -): Promise { - const doFetch = config.fetchImpl ?? fetch; - const response = await doFetch(`${config.hubArtifactsUrl}/api/workflow-artifacts/artifacts`, { - method: "POST", - headers: { - authorization: `Bearer ${config.sidecarToken}`, - "x-workflow-run-address": config.runAddress, - "content-type": "application/json", - }, - body: JSON.stringify(input), - }); - if (!response.ok) { - throw new Error(`Workflow artifact create failed: ${response.status} ${response.statusText}`); - } - const body: unknown = await response.json(); - const parsed = CreatedWorkflowArtifactResponse(body); - if (parsed instanceof type.errors) { - throw new Error( - `Workflow artifact create response did not match the expected shape: ${parsed.summary}`, - ); - } - return parsed.data; -} diff --git a/workflows/exa-topic-watch/src/finalize-tool.test.ts b/workflows/exa-topic-watch/src/finalize-tool.test.ts deleted file mode 100644 index 4a2f3c22e7..0000000000 --- a/workflows/exa-topic-watch/src/finalize-tool.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { expect, test } from "bun:test"; - -import { - EXA_TOPIC_WATCH_FINALIZE_TOOL, - EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME, - buildArtifactPayload, - type WorkflowArtifactEnv, -} from "./finalize-tool"; - -test("the tool's definition marks itself approval-gated", () => { - expect(EXA_TOPIC_WATCH_FINALIZE_TOOL.definitions).toEqual([ - { name: EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME, approval: "ask" }, - ]); -}); - -test("the tool is namespaced under this workflow package, not a shared one", () => { - expect(EXA_TOPIC_WATCH_FINALIZE_TOOL.id).toBe("@corbits/workflow-exa-topic-watch/finalize"); -}); - -test("requires the sanctioned workflow-artifacts env keys", () => { - expect(EXA_TOPIC_WATCH_FINALIZE_TOOL.requires).toEqual([ - "hubArtifactsUrl", - "sidecarToken", - "address", - ]); -}); - -test("buildArtifactPayload marks a real digest as a text artifact", () => { - const payload = buildArtifactPayload({ - outcome: "digest", - title: "Web topic watch: agentic coding tools", - content: "## What moved\n\nSomething happened", - }); - expect(payload).toEqual({ - title: "Web topic watch: agentic coding tools", - kind: "text", - content: "## What moved\n\nSomething happened", - }); -}); - -test("buildArtifactPayload marks a quiet-run note as status-note, not text", () => { - const payload = buildArtifactPayload({ - outcome: "status-note", - title: "Web topic watch: agentic coding tools — quiet week", - content: "Nothing new on this topic since the last run.", - }); - expect(payload).toEqual({ - title: "Web topic watch: agentic coding tools — quiet week", - kind: "status-note", - content: "Nothing new on this topic since the last run.", - }); -}); - -function testEnv(): WorkflowArtifactEnv { - return { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - address: "run_1@workflow", - } as unknown as WorkflowArtifactEnv; -} - -test("run persists the artifact on real invocation (i.e. after approval re-dispatches the call) and reports it persisted", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - })) as unknown as typeof fetch; - - try { - const bundle = EXA_TOPIC_WATCH_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME, - arguments: { - outcome: "digest", - title: "Web topic watch: agentic coding tools", - content: "## What moved\n\nSomething happened", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(String(result.content)) as { - id: string; - version: number; - title: string; - kind: string; - persisted: boolean; - }; - expect(parsed).toEqual({ - id: "art_1", - version: 1, - title: "Web topic watch: agentic coding tools", - kind: "text", - persisted: true, - }); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run persists a quiet-run note with a status-note kind, distinct from a real digest", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response(JSON.stringify({ data: { id: "art_2", version: 1 } }), { - status: 201, - })) as unknown as typeof fetch; - - try { - const bundle = EXA_TOPIC_WATCH_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_2", - name: EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME, - arguments: { - outcome: "status-note", - title: "Web topic watch: agentic coding tools — quiet week", - content: "Searched the web for this topic and found nothing new " + "since the last run.", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(String(result.content)) as { - persisted: boolean; - kind: string; - }; - expect(parsed.persisted).toBe(true); - expect(parsed.kind).toBe("status-note"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run returns an honest error result when persistence fails, never fabricating persisted: true", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - try { - const bundle = EXA_TOPIC_WATCH_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME, - arguments: { - outcome: "digest", - title: "Web topic watch: agentic coding tools", - content: "## What moved\n\nSomething happened", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(true); - expect(result.content).toContain("Failed to publish"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run rejects malformed arguments without throwing", async () => { - const bundle = EXA_TOPIC_WATCH_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME, - arguments: {}, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); - -test("run rejects an outcome value outside the two structural kinds", async () => { - const bundle = EXA_TOPIC_WATCH_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME, - arguments: { - outcome: "summary", - title: "Web topic watch: agentic coding tools", - content: "## What moved\n\nSomething happened", - }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); diff --git a/workflows/exa-topic-watch/src/finalize-tool.ts b/workflows/exa-topic-watch/src/finalize-tool.ts deleted file mode 100644 index 47dfc28db8..0000000000 --- a/workflows/exa-topic-watch/src/finalize-tool.ts +++ /dev/null @@ -1,149 +0,0 @@ -// The one piece of this workflow gated by human approval: publishing the -// digest. Kept inside the workflow package rather than a shared tool -// package — same "workflow-specific logic lives in the definition" -// convention `pain-point-collateral`'s `finalize-tool.ts` established — -// this is not a reusable integration on its own. -// -// Approval mechanics: identical to `last-30-days-research`'s -// `finalize-tool.ts` — `definitions` marks this tool's one definition -// `approval: "ask"`, the platform's native tool-approval gate. Calling it -// suspends the run and creates a real `approval` row; only executes once -// a human approves it. See `pain-point-collateral`'s `finalize-tool.ts` -// for the full suspend/resume account, which applies unchanged here. -// -// Persistence: `run` below calls `createWorkflowArtifact` -// (`./artifact-client.ts`, duplicated from `@corbits/artifact-tools`' -// client rather than imported — see that file's header for why this -// installable-data package never imports another `@corbits/*` package) -// against the sanctioned workflow-artifacts HTTP surface. -// -// This watch runs unattended on a schedule, so a quiet week is the -// common case, not an error. `outcome: "status-note"` is that path: the -// run still finalizes, with an honest "nothing new on this topic since -// the last run" payload, so every fire leaves a visible trace instead of -// silence. - -import { type } from "arktype"; -import { defineTool, type BaseEnv } from "@intx/agent"; -import { createWorkflowArtifact } from "./artifact-client"; - -export const EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME = "exa_topic_watch_finalize"; - -export const EXA_TOPIC_WATCH_FINALIZE_DESCRIPTION = - "Publishes this run's topic digest, pending human approval, and persists it as a Library artifact."; - -const FinalizeArgs = type({ - /** - * Which of the two shapes this call is: `"digest"` when the watch - * found something worth reading; `"status-note"` for the quiet-run - * path. This is what fixes the persisted artifact's `kind` — the model - * names the outcome, but never supplies `kind` directly, so a run can - * never mislabel a quiet run as a real digest or vice versa. - */ - outcome: "'digest'|'status-note'", - /** Short, human-facing title. Doubles as the inbox/approve-card headline. */ - title: "string > 0", - /** The digest's markdown body, or the quiet-run note's honest explanation. */ - content: "string > 0", -}); - -export type FinalizeArgs = typeof FinalizeArgs.infer; - -export type ArtifactPayload = { - title: string; - kind: "text" | "status-note"; - content: string; -}; - -/** - * The payload `createWorkflowArtifact` persists (`{ title, kind, - * content }`). Built here so the persist call is a straight pass-through - * of this object, not a payload assembled at the call site. `kind` comes - * from `args.outcome`, never from free-text the model could drift on. - */ -export function buildArtifactPayload(args: FinalizeArgs): ArtifactPayload { - return { - title: args.title, - kind: args.outcome === "status-note" ? "status-note" : "text", - content: args.content, - }; -} - -/** - * The env this tool needs beyond `BaseEnv`: the sanctioned - * workflow-artifacts credential trio, populated for every workflow step - * (`apps/sidecar/src/workflow-substrate-factory/step-env.ts`). - */ -export interface WorkflowArtifactEnv extends BaseEnv { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly address: string; -} - -export const EXA_TOPIC_WATCH_FINALIZE_TOOL = defineTool({ - id: "@corbits/workflow-exa-topic-watch/finalize", - requires: ["hubArtifactsUrl", "sidecarToken", "address"], - definitions: [ - { - name: EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME, - approval: "ask", - }, - ], - factory: (env) => ({ - definitions: [ - { - name: EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME, - description: EXA_TOPIC_WATCH_FINALIZE_DESCRIPTION, - inputSchema: { - type: "object", - properties: { - outcome: { type: "string", enum: ["digest", "status-note"] }, - title: { type: "string" }, - content: { type: "string" }, - }, - required: ["outcome", "title", "content"], - }, - }, - ], - run: async (call) => { - const parsed = FinalizeArgs(call.arguments); - if (parsed instanceof type.errors) { - return { - callId: call.id, - isError: true, - content: `Invalid arguments for ${EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME}: ${parsed.summary}`, - }; - } - const artifact = buildArtifactPayload(parsed); - try { - const created = await createWorkflowArtifact( - { - hubArtifactsUrl: env.hubArtifactsUrl, - sidecarToken: env.sidecarToken, - runAddress: env.address, - }, - artifact, - ); - return { - callId: call.id, - isError: false, - content: JSON.stringify({ - id: created.id, - version: created.version, - title: artifact.title, - kind: artifact.kind, - persisted: true, - }), - }; - } catch (err) { - return { - callId: call.id, - isError: true, - content: `Failed to publish "${artifact.title}" to the Library: ${ - err instanceof Error ? err.message : String(err) - }`, - }; - } - }, - }), -}); diff --git a/workflows/exa-topic-watch/src/index.ts b/workflows/exa-topic-watch/src/index.ts deleted file mode 100644 index c3e32c0b7d..0000000000 --- a/workflows/exa-topic-watch/src/index.ts +++ /dev/null @@ -1,210 +0,0 @@ -// The exa-topic-watch workflow: on a schedule, search the live web for -// one topic and publish a short digest of what moved. Ported from the OG -// gtm-workbench's `exa-topic-watch` workflow (CL-6349). -// -// ## What the OG pipeline does, step by step -// intake (awaitSignal, a topic) -> prepare-search (action: a -// workflow-owned tool renaming `topic` to exa_search's `query` argument) -// -> fetch (action: `exa_search`) -> digest (reasoning: write the -// markdown digest) -> document (action: pair topic + reply into -// `{ title, body }`) -> persist (action: `write_artifact`). -// -// ## Adaptations this port makes -// -// (1) Folded to one step. Five of the OG's six steps were native -// `action` primitives dispatching a `handler` string an `ActionInvoker` -// resolves. This repo's production host leaves `invokeAction` undefined -// (`vendor/intx/workflow/src/runtime/run.ts` throws "this host does not -// support action primitives"), so an `action` step here would deploy as -// something nothing can dispatch. Every OG action collapses into the -// single reasoning step below, which calls the same capabilities as -// ordinary agent tools inside one turn — the shape every proven workflow -// package in this repo already uses. Folding also removes the OG's -// `prepare-search` rename tool outright: an agent naming a search -// tool's own arguments has nothing left to rename. -// -// (2) Exa reaches this deployment through the Exa MCP preset -// (`packages/connections/src/mcp-presets.ts`, slug `exa`, keyless) and -// `@corbits/mcp-tools`' `mcp_read`, rather than the OG's bespoke -// `@workbench/tools-exa` package. `@corbits/mcp-tools` is one of the few -// tool packages this repo actually publishes to its own registry -// (`packages/tool-registry-publish/src/registry.ts`), so a pin on it -// resolves at deploy time; a pin on a native Exa bundle would not. -// The MCP credential handle (`mcp:exa`) is dynamic tenant data, so the -// host supplies its binding per tenant -// (`apps/hub/src/mcp-credential-bindings.ts`) — this definition declares -// no static credential binding of its own. -// -// (3) Persistence is `exa_topic_watch_finalize` (`approval: "ask"`, -// `./finalize-tool.ts`) rather than the OG's ungated `write_artifact` -// action. Every external side effect in this repo sits behind human -// approval (`AGENTS.md`), and this is the only path from a workflow tool -// package to the Library engine here. -// -// (4) Intake: the OG parks on `awaitSignal("intake")`. Workbench collects -// the topic on the routine *before* launch and delivers it as the -// first-turn mail (`renderRoutineInput` -> `trigger.payload`), so a gate -// nobody fulfills would hang the run forever. The triggering mail *is* -// the topic. -// -// This package is installable data. It imports only published platform -// packages, and nothing imports it statically: a host publishes the -// serialized definition as a workflow asset and deploys it through the -// platform's deploy machinery. - -import type { AgentDefinition, InferencePreference } from "@intx/agent"; -import { defineWorkflow, step } from "@intx/workflow"; -import type { WorkflowDefinition } from "@intx/workflow"; -import type { ToolPackagePin } from "@intx/types/tool-packages"; - -import { EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME } from "./finalize-tool"; - -export const EXA_TOPIC_WATCH_WORKFLOW_ID = "wf_exa_topic_watch"; -export const EXA_TOPIC_WATCH_STEP_ID = "exa-topic-watch-digest"; - -/** The MCP preset slug this watch searches through — the same slug - * `packages/connections/src/mcp-presets.ts` registers Exa under, and the - * one the agent passes as `mcp_read`'s `server` argument. */ -export const EXA_MCP_SERVER_SLUG = "exa"; - -/** The digest's fixed section structure — this deployment's contract, and - * what the system prompt below instructs the model to fill in. */ -export const EXA_TOPIC_WATCH_SECTIONS = ["What moved", "Takeaways", "Worth a closer look"] as const; - -const CORBITS_VOCABULARY = - "Treat Corbits, Corbits.dev, Interchange, and Faremeter as canonical " + - "Corbits names; spell them exactly. When source material contains a " + - "clear speech-to-text or spelling variant, use the canonical spelling " + - "in your output. Do not replace an ambiguous term unless surrounding " + - "context identifies it."; - -export const EXA_TOPIC_WATCH_SYSTEM_PROMPT = [ - CORBITS_VOCABULARY, - "You are a scheduled web topic watch. Each run, you read one topic " + - "from the message that started you and report what moved on it since " + - "the last run. Nobody is watching this run happen — write for someone " + - "reading it later, cold.", - "The message carries a `topic`. It is the whole brief: never invent a " + - "topic, and never widen one. If it is missing or empty, skip searching " + - "entirely and finalize a status note saying what a topic looks like.", - `Searching: web search reaches you through the "${EXA_MCP_SERVER_SLUG}" server. Call \`mcp_list_tools\` once for that server to learn its search tool's exact name and arguments, then call \`mcp_read\` at most three times, each with a differently-angled query for the same topic. If the server is not connected, or a call comes back as an error, that is an honest "the web is not reachable right now" — say so plainly, and never invent results to cover for it.`, - `Write the digest as markdown with exactly these sections, in order: ${EXA_TOPIC_WATCH_SECTIONS.join(", ")}. "What moved" is one line. "Takeaways" is three to seven bullets, each linking its source. "Worth a closer look" is at most three items, each with one sentence on why. No preamble.`, - "Every claim carries the link it came from. A claim you cannot link " + "does not go in.", - `Finalizing: call \`${EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME}\` exactly once with outcome "digest", a short title (e.g. "Web topic watch: "), and the full markdown digest as content. This call requires a human's approval before it completes. A quiet run is still a run: when nothing new surfaced, or the web was unreachable, call the same tool once with outcome "status-note", a plain title (e.g. "Web topic watch: — quiet week"), and content that honestly says what you searched for and what you found or could not reach. Never end a run without finalizing.`, - "If the finalize call succeeds, present the digest as your reply " + - "exactly as written, with no commentary about the approval mechanism " + - "itself. If the call is denied, reply with one calm, plain sentence " + - "that the digest was not published and nothing was saved; never " + - "present a denial as an error, and never apologize as if something " + - "broke.", -].join("\n\n"); - -/** - * Tool packages this definition pins (CL-5999). `@corbits/mcp-tools` - * carries every connected MCP server, Exa among them; the finalize tool - * travels with the deploy of this package itself. - */ -export const EXA_TOPIC_WATCH_TOOL_PACKAGE_PINS: readonly ToolPackagePin[] = [ - { name: "@corbits/mcp-tools", version: "0.0.12" }, -]; - -/** - * Everything the definition needs that is per-deployment data. The - * trigger address names a specific deployment's inbox — for this - * workflow, the address a Routine's scheduled trigger mail lands on. - */ -export interface ExaTopicWatchWorkflowInput { - /** The deployment's mail address; each inbound mail is one run. */ - readonly triggerAddress: string; - /** Provider/model preferences, in order; resolved at deploy time. */ - readonly inferencePreferences: readonly InferencePreference[]; - /** Per-turn timeout in milliseconds, enforced on the single step. */ - readonly turnTimeoutMs: number; -} - -/** - * Builds the exa-topic-watch definition: exactly one mail-triggered - * reasoning step whose triggering mail carries the watch topic. Tools are - * never inlined on the definition — they arrive as pinned packages on the - * deploy, keeping the definition pure data. - */ -export function buildExaTopicWatchWorkflow(input: ExaTopicWatchWorkflowInput): WorkflowDefinition { - if (input.triggerAddress === "") { - throw new Error("buildExaTopicWatchWorkflow requires a non-empty triggerAddress"); - } - if (!Number.isInteger(input.turnTimeoutMs) || input.turnTimeoutMs <= 0) { - throw new Error("buildExaTopicWatchWorkflow requires turnTimeoutMs to be a positive integer"); - } - return defineWorkflow({ - id: EXA_TOPIC_WATCH_WORKFLOW_ID, - trigger: { type: "mail", to: input.triggerAddress }, - steps: { - [EXA_TOPIC_WATCH_STEP_ID]: step({ - agent: { - id: EXA_TOPIC_WATCH_STEP_ID, - description: - "Searches the live web for one topic and publishes a short " + - "digest of what moved, once a human approves it", - systemPrompt: EXA_TOPIC_WATCH_SYSTEM_PROMPT, - toolFactories: [], - capabilities: [], - inference: { sources: input.inferencePreferences }, - toolPackagePins: EXA_TOPIC_WATCH_TOOL_PACKAGE_PINS, - } satisfies AgentDefinition, - timeout: input.turnTimeoutMs, - }), - }, - }); -} - -/** - * Serializes a definition to the JSON a workflow asset carries. The - * definition must survive the asset round-trip byte-faithfully, so - * anything JSON would silently drop or mangle is a loud error naming the - * offending path instead of a corrupted asset. - */ -export function serializeExaTopicWatchWorkflow(definition: WorkflowDefinition): string { - assertJsonPortable(definition, "definition"); - return JSON.stringify(definition); -} - -function assertJsonPortable(value: unknown, path: string): void { - if (value === null) return; - switch (typeof value) { - case "string": - case "boolean": - return; - case "number": - if (!Number.isFinite(value)) { - throw new Error(`${path} is a non-finite number; JSON drops it`); - } - return; - case "object": - break; - default: - throw new Error( - `${path} is a ${typeof value}, which does not survive JSON ` + "serialization", - ); - } - if (Array.isArray(value)) { - value.forEach((element, index) => { - assertJsonPortable(element, `${path}[${index}]`); - }); - return; - } - const proto: unknown = Object.getPrototypeOf(value); - if (proto !== Object.prototype && proto !== null) { - throw new Error(`${path} is a non-plain object; JSON would flatten it lossily`); - } - for (const [key, entry] of Object.entries(value)) { - assertJsonPortable(entry, `${path}.${key}`); - } -} - -export { - EXA_TOPIC_WATCH_FINALIZE_TOOL, - EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME, - EXA_TOPIC_WATCH_FINALIZE_DESCRIPTION, - buildArtifactPayload, -} from "./finalize-tool"; -export type { ArtifactPayload, FinalizeArgs } from "./finalize-tool"; diff --git a/workflows/exa-topic-watch/test/boundary.test.ts b/workflows/exa-topic-watch/test/boundary.test.ts deleted file mode 100644 index 8bd42a25f3..0000000000 --- a/workflows/exa-topic-watch/test/boundary.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -// This package is installable data on the native workflow contract: -// its shipped sources import only published platform packages, so the -// source tree plus the package manifest deploys on any Interchange -// instance without a wrapper contract. The tool package this workflow -// expects at deploy time (@corbits/mcp-tools) is pinned by name, never -// imported here — see src/index.ts's header comment. - -import { readFile, readdir } from "node:fs/promises"; -import path from "node:path"; -import { expect, test } from "bun:test"; - -const ALLOWED_IMPORT_PREFIXES = ["@intx/", "arktype", "./", "../"]; - -async function listFiles(dir: string): Promise { - const entries = await readdir(dir, { withFileTypes: true }); - const files: string[] = []; - for (const entry of entries) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...(await listFiles(full))); - } else { - files.push(full); - } - } - return files; -} - -async function shippedFiles(): Promise { - const packageRoot = path.join(import.meta.dir, ".."); - return [ - ...(await listFiles(path.join(packageRoot, "src"))), - path.join(packageRoot, "package.json"), - ]; -} - -test("shipped sources import only published platform packages", async () => { - const importPattern = /from\s+"([^"]+)"/g; - const violations: string[] = []; - for (const file of await shippedFiles()) { - if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue; - const content = await readFile(file, "utf8"); - for (const match of content.matchAll(importPattern)) { - const specifier = match[1] ?? ""; - const allowed = ALLOWED_IMPORT_PREFIXES.some((prefix) => specifier.startsWith(prefix)); - if (!allowed) { - violations.push(`${path.basename(file)}: ${specifier}`); - } - } - } - expect(violations).toEqual([]); -}); diff --git a/workflows/exa-topic-watch/test/definition.test.ts b/workflows/exa-topic-watch/test/definition.test.ts deleted file mode 100644 index 8e4202f6fa..0000000000 --- a/workflows/exa-topic-watch/test/definition.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -// Tests for this package's own contract: the shape our factory commits -// to, its serialization guarantees, and its boundary. The platform's own -// normalization and validation are its business, not re-proven here. - -import { expect, test } from "bun:test"; -import type { StepPrimitive, WorkflowDefinition } from "@intx/workflow"; - -import { - EXA_MCP_SERVER_SLUG, - EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME, - EXA_TOPIC_WATCH_SECTIONS, - EXA_TOPIC_WATCH_STEP_ID, - EXA_TOPIC_WATCH_SYSTEM_PROMPT, - EXA_TOPIC_WATCH_TOOL_PACKAGE_PINS, - EXA_TOPIC_WATCH_WORKFLOW_ID, - buildExaTopicWatchWorkflow, - serializeExaTopicWatchWorkflow, -} from "../src/index"; - -const INPUT = { - triggerAddress: "ins_dep000000000000@example.test", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 600000, -} as const; - -function digestStep(definition: WorkflowDefinition): StepPrimitive { - const primitive = definition.steps[EXA_TOPIC_WATCH_STEP_ID]; - if (primitive === undefined || primitive.kind !== "step") { - throw new Error(`definition has no step primitive named ${EXA_TOPIC_WATCH_STEP_ID}`); - } - return primitive; -} - -test("the definition folds the OG six-step pipeline into exactly one step", () => { - const definition = buildExaTopicWatchWorkflow(INPUT); - expect(definition.stepOrder).toEqual([EXA_TOPIC_WATCH_STEP_ID]); - expect(Object.keys(definition.steps)).toEqual([EXA_TOPIC_WATCH_STEP_ID]); -}); - -test("the definition uses no action primitive, which this host cannot dispatch", () => { - const definition = buildExaTopicWatchWorkflow(INPUT); - const kinds = Object.values(definition.steps).map((primitive) => primitive.kind); - expect(kinds).toEqual(["step"]); -}); - -test("the step carries an explicit per-turn timeout", () => { - const definition = buildExaTopicWatchWorkflow(INPUT); - expect(digestStep(definition).timeout).toBe(INPUT.turnTimeoutMs); -}); - -test("the workflow is triggered by mail to the given deployment address", () => { - const definition = buildExaTopicWatchWorkflow(INPUT); - expect(definition.id).toBe(EXA_TOPIC_WATCH_WORKFLOW_ID); - expect(definition.triggers).toEqual([{ type: "mail", to: INPUT.triggerAddress }]); -}); - -test("the agent carries the fixed prompt and the caller's preferences, and inlines no tools", () => { - const agent = digestStep(buildExaTopicWatchWorkflow(INPUT)).agent; - expect(agent.systemPrompt).toBe(EXA_TOPIC_WATCH_SYSTEM_PROMPT); - expect(agent.inference).toEqual({ sources: INPUT.inferencePreferences }); - expect(agent.toolFactories).toEqual([]); -}); - -test("the step pins the MCP tools bundle, the one package a deploy here can resolve", () => { - const agent = digestStep(buildExaTopicWatchWorkflow(INPUT)).agent; - expect(agent.toolPackagePins).toEqual(EXA_TOPIC_WATCH_TOOL_PACKAGE_PINS); - expect(EXA_TOPIC_WATCH_TOOL_PACKAGE_PINS).toEqual([ - { name: "@corbits/mcp-tools", version: "0.0.12" }, - ]); -}); - -test("the definition declares no static credential binding — mcp: is dynamic tenant data the host supplies", () => { - const definition = buildExaTopicWatchWorkflow(INPUT); - expect(definition.credentialBindings ?? []).toEqual([]); -}); - -test("the prompt names the Exa MCP server slug the agent has to pass as `server`", () => { - expect(EXA_MCP_SERVER_SLUG).toBe("exa"); - expect(EXA_TOPIC_WATCH_SYSTEM_PROMPT).toContain(`"${EXA_MCP_SERVER_SLUG}"`); - expect(EXA_TOPIC_WATCH_SYSTEM_PROMPT).toContain("mcp_read"); -}); - -test("the prompt reaches the web read-only — never mcp_call, which would gate every search on an approval", () => { - expect(EXA_TOPIC_WATCH_SYSTEM_PROMPT).not.toContain("mcp_call"); -}); - -test("the prompt names every digest section and both finalize outcomes", () => { - for (const section of EXA_TOPIC_WATCH_SECTIONS) { - expect(EXA_TOPIC_WATCH_SYSTEM_PROMPT).toContain(section); - } - expect(EXA_TOPIC_WATCH_SYSTEM_PROMPT).toContain(EXA_TOPIC_WATCH_FINALIZE_TOOL_NAME); - expect(EXA_TOPIC_WATCH_SYSTEM_PROMPT).toContain('outcome "digest"'); - expect(EXA_TOPIC_WATCH_SYSTEM_PROMPT).toContain('outcome "status-note"'); -}); - -test("a quiet run still finalizes rather than ending silently", () => { - expect(EXA_TOPIC_WATCH_SYSTEM_PROMPT).toContain("Never end a run without finalizing"); -}); - -test("the definition survives the workflow-asset JSON round trip unchanged", () => { - const definition = buildExaTopicWatchWorkflow(INPUT); - expect(JSON.parse(serializeExaTopicWatchWorkflow(definition))).toEqual( - JSON.parse(JSON.stringify(definition)), - ); -}); - -test("serializing a definition JSON would mangle fails loudly, naming the path", () => { - const definition = buildExaTopicWatchWorkflow(INPUT); - const poisoned = { - ...definition, - steps: { ...definition.steps, poison: () => undefined }, - } as unknown as WorkflowDefinition; - expect(() => serializeExaTopicWatchWorkflow(poisoned)).toThrow( - /definition\.steps\.poison is a function/, - ); -}); - -test("an empty trigger address is rejected at build time", () => { - expect(() => buildExaTopicWatchWorkflow({ ...INPUT, triggerAddress: "" })).toThrow( - /non-empty triggerAddress/, - ); -}); - -test("a non-positive turn timeout is rejected at build time", () => { - expect(() => buildExaTopicWatchWorkflow({ ...INPUT, turnTimeoutMs: 0 })).toThrow( - /positive integer/, - ); -}); diff --git a/workflows/exa-topic-watch/tsconfig.json b/workflows/exa-topic-watch/tsconfig.json deleted file mode 100644 index a8c2b9966d..0000000000 --- a/workflows/exa-topic-watch/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src", "test"], - "exclude": [] -} diff --git a/workflows/exa-topic-watch/tsconfig.src.json b/workflows/exa-topic-watch/tsconfig.src.json deleted file mode 100644 index fc8724305f..0000000000 --- a/workflows/exa-topic-watch/tsconfig.src.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } -} diff --git a/workflows/granola-call/LICENSE b/workflows/granola-call/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/workflows/granola-call/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/workflows/granola-call/README.md b/workflows/granola-call/README.md deleted file mode 100644 index 57f0ef338c..0000000000 --- a/workflows/granola-call/README.md +++ /dev/null @@ -1,124 +0,0 @@ -# @corbits/granola-call-workflow - -The parent half of the Granola call-notes pipeline (CL-5998, ported from -`gtm-workbench`'s `workflows/granola-call`). Meant to run on a recurring -Routine schedule — hourly or daily both work — it polls Granola for -recent calls and starts one `process-granola-call` run per call that has -no published call-notes artifact yet. - -## What it does - -One step, one agent. The system prompt commits it to: - -- looking at up to `DEFAULT_CALL_LIMIT` (10) recent calls per run unless - told otherwise, -- never reprocessing a call that already has a published call-notes - artifact (idempotent — a quiet run starts nothing), -- skipping a call whose transcript is missing or unreadable rather than - failing the whole run, -- when a run starts no `process-granola-call` children — no way to - reach Granola at all, or Granola connected but nothing new to - process — calling `granola_call_report_status` - (`./src/finalize-tool.ts`) once to persist a real, chip-visible - status artifact: why nothing started, how many calls were actually - examined, and what to check next (starting with the `granola` - connector's connection status, - `packages/connections/src/registry.ts`). Never inventing call counts - or notes. That tool is not approval-gated — a status report has - nothing for a human to confirm, only what actually happened — and - persists via `createWorkflowArtifact` (`./src/artifact-client.ts`, - CL-6000), the same sanctioned workflow-artifacts surface - `process-granola-call`'s finalize tool uses. - -**Teaching-artifact kind**: `buildStatusArtifactPayload` always persists -with `kind: "status-note"` — this tool only ever produces a status -report, never a real deliverable, so there is no competing shape to -confuse it with, but the value still matches the one teaching-artifact -kind shared by every workflow in this catalog, so the Library's kind -badge always reads "Status note" here too, not a bare "Text" that would -look identical to a real deliverable elsewhere. - -## Sensible defaults - -- **Schedule:** attach an hourly or daily Routine. A tighter schedule - than hourly buys little (new calls accumulate between polls anyway) - and a daily poll is enough for most workspaces. -- **Call limit:** 10 per run, matching the source repo's default and - bounding a poll's cost even on a backlog. - -## Current limits (read before deploying) - -This port keeps the shape of every other workflow in this catalog -(`step`/`defineAgent`, mail-triggered, tools arrive as packages on the -deploy) rather than inventing new primitives. Two real gaps stand -between this definition and the pipeline's actual behavior, both -platform-level, not specific to Granola: - -1. **No spawn-child mechanism.** Nothing in this host lets a run start - other runs programmatically yet. `@intx/workflow`'s `action` - primitive exists but no shipped host wires the `invokeAction` - callback it needs (see `@corbits/heartbeat-workflow`'s README for the - same finding). Fanning out into per-call `process-granola-call` runs - needs that wired first. -2. **`@corbits/granola-tools` is not published anywhere this host's tool - loader (`@intx/tool-packaging`) can resolve it from by default.** This - definition now pins it (`GRANOLA_CALL_TOOL_PACKAGE_PINS`, CL-5999), - but a pin only resolves once an operator seeds it into a registry the - host's `toolPackageRegistries` config reaches — either npmjs, or a - `package-registry` asset for the `@corbits` scope (see - `apps/hub/src/index.ts`'s `CORBITS_TOOLS_REGISTRY` wiring). - -Until both land, a deployment of this definition is an honest -placeholder: until an operator publishes the pin, every run persists a -status artifact saying Granola is not connected rather than fabricating -a call list. That is deliberate — see the "no fallbacks" and "parse at -every trust boundary" rules in `AGENTS.md`; a workflow that quietly -returns fake data on a broken dependency is worse than one that says so. - -Persisting that honest status as a real Library artifact (CL-6029) does -not depend on either gap above: it is this workflow's own finalize step, -independent of spawning children or reaching Granola. Only the -pipeline's actual fan-out — starting real `process-granola-call` -children — waits on gap #1. - -## Usage - -```ts -import { - buildGranolaCallWorkflow, - serializeGranolaCallWorkflow, -} from "@corbits/granola-call-workflow"; - -const definition = buildGranolaCallWorkflow({ - triggerAddress: "granola-call@tenant.example", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 120_000, -}); - -const json = serializeGranolaCallWorkflow(definition); -``` - -See [`workflows/README.md`](../README.md#status-note) for what -registration/automatable/seeded mean — this one is `automatable: true` -but not seeded, since Granola isn't connected for every tenant. - -## Deploy from the npm registry - -Once published, deploy this workflow definition to a tenant by name and -version pin: - -``` -POST /api/tenants/:id/workflows/deployments -{ - "source": { "kind": "registry", "registry": "npm" }, - "entry": "./src/index.ts", - "pin": "@corbits/granola-call-workflow@0.0.1", - "sourceOfferingIds": ["", ...], - "defaultSourceOfferingId": "" -} -``` - -`entry` is the package's `interchange.workflow` module path; `pin` is -`"@corbits/granola-call-workflow@0.0.1"` or a semver range on the same name. The hub -installs, probes, gates, and freezes the definition from the registry -tarball before creating the deployment. diff --git a/workflows/granola-call/package.json b/workflows/granola-call/package.json deleted file mode 100644 index 85c237ede5..0000000000 --- a/workflows/granola-call/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@corbits/granola-call-workflow", - "version": "0.0.1", - "description": "Polls Granola for new calls and starts one call-notes run per call without a published call-notes artifact yet", - "license": "LGPL-2.1-or-later", - "repository": { - "type": "git", - "url": "https://github.com/corbitsdev/workbench.git", - "directory": "workflows/granola-call" - }, - "files": [ - "src", - "LICENSE", - "README.md" - ], - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "arktype": "catalog:" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0" - }, - "corbits": { - "workflow": { - "assetName": "granola-call", - "displayName": "Granola call notes", - "automatable": true - } - }, - "interchange": { - "workflow": "./src/index.ts" - } -} diff --git a/workflows/granola-call/src/artifact-client.test.ts b/workflows/granola-call/src/artifact-client.test.ts deleted file mode 100644 index 7f88d66fd2..0000000000 --- a/workflows/granola-call/src/artifact-client.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { expect, test } from "bun:test"; - -import { createWorkflowArtifact } from "./artifact-client"; - -const CONFIG = { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - runAddress: "run_1@workflow", -}; - -test("posts to the workflow-artifacts endpoint and returns id/version", async () => { - const captured: { url: string; init: RequestInit | undefined } = { - url: "", - init: undefined, - }; - const fetchImpl = (async (input: URL | string, init?: RequestInit) => { - captured.url = String(input); - captured.init = init; - return new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - }); - }) as unknown as typeof fetch; - - const result = await createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Status", kind: "text", content: "hello" }, - ); - - expect(result).toEqual({ id: "art_1", version: 1 }); - expect(captured.url).toBe("https://hub.example.com/api/workflow-artifacts/artifacts"); - const headers = captured.init?.headers as Record; - expect(headers["authorization"]).toBe("Bearer sc-token"); - expect(headers["x-workflow-run-address"]).toBe("run_1@workflow"); -}); - -test("throws on a non-ok HTTP response", async () => { - const fetchImpl = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Status", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/500/); -}); - -test("throws when the response doesn't match the expected shape", async () => { - const fetchImpl = (async () => - new Response(JSON.stringify({ data: {} }), { - status: 201, - })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Status", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/did not match the expected shape/); -}); diff --git a/workflows/granola-call/src/artifact-client.ts b/workflows/granola-call/src/artifact-client.ts deleted file mode 100644 index b9d56ca425..0000000000 --- a/workflows/granola-call/src/artifact-client.ts +++ /dev/null @@ -1,62 +0,0 @@ -// A minimal client for the sanctioned workflow-artifacts HTTP surface -// (`@corbits/artifacts-hub`'s `createWorkflowArtifactRoutes`, CL-6000). -// Deliberately duplicated from `@corbits/artifact-tools`' `client.ts` -// rather than imported: this package ships as installable data on the -// native workflow contract (see `./index.ts`'s header comment and -// `test/boundary.test.ts`), so its shipped sources import only `@intx/*`, -// `arktype`, and relative specifiers — never another `@corbits/*` -// package. Authenticates with the sidecar's own bearer token plus the -// run's own mailbox address (both already reach a workflow-process -// child's tool env), never a database handle. -import { type } from "arktype"; - -export interface WorkflowArtifactClientConfig { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly runAddress: string; - /** Override for tests; defaults to the global `fetch`. */ - readonly fetchImpl?: typeof fetch; -} - -export type CreateWorkflowArtifactInput = { - readonly title: string; - readonly kind: string; - readonly content: string; -}; - -export type CreatedWorkflowArtifact = { - readonly id: string; - readonly version: number; -}; - -const CreatedWorkflowArtifactResponse = type({ - data: { id: "string", version: "number" }, -}); - -/** Persists one artifact. Throws on any transport, HTTP, or shape failure. */ -export async function createWorkflowArtifact( - config: WorkflowArtifactClientConfig, - input: CreateWorkflowArtifactInput, -): Promise { - const doFetch = config.fetchImpl ?? fetch; - const response = await doFetch(`${config.hubArtifactsUrl}/api/workflow-artifacts/artifacts`, { - method: "POST", - headers: { - authorization: `Bearer ${config.sidecarToken}`, - "x-workflow-run-address": config.runAddress, - "content-type": "application/json", - }, - body: JSON.stringify(input), - }); - if (!response.ok) { - throw new Error(`Workflow artifact create failed: ${response.status} ${response.statusText}`); - } - const body: unknown = await response.json(); - const parsed = CreatedWorkflowArtifactResponse(body); - if (parsed instanceof type.errors) { - throw new Error( - `Workflow artifact create response did not match the expected shape: ${parsed.summary}`, - ); - } - return parsed.data; -} diff --git a/workflows/granola-call/src/finalize-tool.test.ts b/workflows/granola-call/src/finalize-tool.test.ts deleted file mode 100644 index de42ea3369..0000000000 --- a/workflows/granola-call/src/finalize-tool.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { expect, test } from "bun:test"; - -import { - GRANOLA_CALL_REPORT_STATUS_TOOL, - GRANOLA_CALL_REPORT_STATUS_TOOL_NAME, - buildStatusArtifactPayload, - type WorkflowArtifactEnv, -} from "./finalize-tool"; - -test("the tool carries no approval mark: a status report has nothing for a human to confirm", () => { - expect(GRANOLA_CALL_REPORT_STATUS_TOOL.definitions).toEqual([ - { name: GRANOLA_CALL_REPORT_STATUS_TOOL_NAME }, - ]); -}); - -test("the tool is namespaced under this workflow package, not a shared one", () => { - expect(GRANOLA_CALL_REPORT_STATUS_TOOL.id).toBe("@corbits/workflow-granola-call/report-status"); -}); - -test("requires the sanctioned workflow-artifacts env keys", () => { - expect(GRANOLA_CALL_REPORT_STATUS_TOOL.requires).toEqual([ - "hubArtifactsUrl", - "sidecarToken", - "address", - ]); -}); - -test("buildStatusArtifactPayload names the reason, examined count, and next steps", () => { - const payload = buildStatusArtifactPayload({ - reason: "No Granola connection is configured for this workspace.", - callsExamined: 0, - nextSteps: "Connect Granola in Settings > Connections, then re-run this Routine.", - }); - expect(payload.kind).toBe("status-note"); - expect(payload.content).toContain("No Granola connection is configured for this workspace."); - expect(payload.content).toContain("0"); - expect(payload.content).toContain( - "Connect Granola in Settings > Connections, then re-run this Routine.", - ); - expect(payload.content).toContain("granola connector"); -}); - -function testEnv(): WorkflowArtifactEnv { - return { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - address: "run_1@workflow", - } as unknown as WorkflowArtifactEnv; -} - -test("run persists the status artifact and reports it persisted", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - })) as unknown as typeof fetch; - - try { - const bundle = GRANOLA_CALL_REPORT_STATUS_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: GRANOLA_CALL_REPORT_STATUS_TOOL_NAME, - arguments: { - reason: "No Granola connection is configured for this workspace.", - callsExamined: 0, - nextSteps: "Connect Granola in Settings > Connections.", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(String(result.content)) as { - id: string; - version: number; - kind: string; - persisted: boolean; - }; - expect(parsed.id).toBe("art_1"); - expect(parsed.version).toBe(1); - expect(parsed.persisted).toBe(true); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run returns an honest error result when persistence fails, never fabricating persisted: true", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - try { - const bundle = GRANOLA_CALL_REPORT_STATUS_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: GRANOLA_CALL_REPORT_STATUS_TOOL_NAME, - arguments: { - reason: "No Granola connection is configured for this workspace.", - callsExamined: 0, - nextSteps: "Connect Granola in Settings > Connections.", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(true); - expect(result.content).toContain("Failed to persist"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run rejects malformed arguments without throwing", async () => { - const bundle = GRANOLA_CALL_REPORT_STATUS_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: GRANOLA_CALL_REPORT_STATUS_TOOL_NAME, - arguments: {}, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); - -test("run rejects a negative callsExamined", async () => { - const bundle = GRANOLA_CALL_REPORT_STATUS_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: GRANOLA_CALL_REPORT_STATUS_TOOL_NAME, - arguments: { - reason: "No Granola connection is configured for this workspace.", - callsExamined: -1, - nextSteps: "Connect Granola in Settings > Connections.", - }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); diff --git a/workflows/granola-call/src/finalize-tool.ts b/workflows/granola-call/src/finalize-tool.ts deleted file mode 100644 index 14fbb9f093..0000000000 --- a/workflows/granola-call/src/finalize-tool.ts +++ /dev/null @@ -1,163 +0,0 @@ -// The one piece of this workflow that reaches the Library engine: an -// honest status report for a run that started no `process-granola-call` -// children — either because it had no way to reach Granola at all, or -// because it found nothing new to process. Kept inside the workflow -// package rather than a shared tool package — this behavior is specific -// to this workflow, not a reusable integration — following the same -// "workflow-specific logic lives in the definition" convention -// `pain-point-collateral`'s and `reddit-opportunity-scanner`'s -// finalize-tool.ts files establish. -// -// Not approval-gated: a status report has nothing a human needs to -// confirm or reject, only an honest account of what this run actually -// found — the same reasoning `reddit-opportunity-scanner`'s -// `reddit_opportunity_scanner_report_no_results` tool documents for its -// own no-data path (see that package's `finalize-tool.ts` header). -// -// Persistence (CL-6000): `run` below calls `createWorkflowArtifact` -// (`./artifact-client.ts`) against the sanctioned workflow-artifacts HTTP -// surface — authenticated with the sidecar's own bearer token and this -// run's own mailbox address, both already present on `env`, never a -// database handle. A successful call returns the persisted artifact's -// id/version so the delivery pipeline can reference it with a file part -// (`packages/chat/src/artifact-delivery.ts`); a failed call surfaces as -// an honest `isError: true` result rather than a fabricated success. -// -// This tool exists independently of the spawn-mechanism gap documented -// in `./index.ts`'s header comment: reporting an honest status is not -// blocked on `action`/`invokeAction` landing, only on the model having -// something true to say — so a quiet or disconnected run can still leave -// the sender a real, chip-visible record instead of a bare prose line. - -import { type } from "arktype"; -import { defineTool, type BaseEnv } from "@intx/agent"; -import { createWorkflowArtifact } from "./artifact-client"; - -export const GRANOLA_CALL_REPORT_STATUS_TOOL_NAME = "granola_call_report_status"; - -export const GRANOLA_CALL_REPORT_STATUS_DESCRIPTION = - "Persists one honest status artifact for a run that started no " + - "process-granola-call children: why (no Granola connection, or " + - "nothing new to process), how many calls were examined, and what to " + - "check next. Not approval-gated — nothing was selected, so there is " + - "nothing for a human to confirm."; - -const StatusReportArgs = type({ - /** Why this run started nothing, grounded in what actually happened. */ - reason: "string > 0", - /** How many recent calls this run actually looked at (0 if unreachable). */ - callsExamined: "number >= 0", - /** Plain-language advice for what the sender should do next. */ - nextSteps: "string > 0", -}); - -export type StatusReportArgs = typeof StatusReportArgs.infer; - -export type ArtifactPayload = { - title: string; - kind: "status-note"; - content: string; -}; - -/** - * The payload `createWorkflowArtifact` persists (`{ title, kind, - * content }`). Every line is drawn from `args` — nothing here is - * invented sample data. `kind` is always `"status-note"` — this tool - * only ever produces a teaching/status report, never a real deliverable, - * so there is no competing shape to distinguish it from, but the value - * still matches the same convention every other workflow's teaching - * payload uses, so the Library's kind badge reads consistently across - * the whole catalog. - */ -export function buildStatusArtifactPayload(args: StatusReportArgs): ArtifactPayload { - return { - title: "Granola call notes: nothing new to process", - kind: "status-note", - content: - "This run started no process-granola-call children.\n\n" + - `Why\n${args.reason}\n\n` + - `Calls examined: ${args.callsExamined}\n\n` + - `Next steps\n${args.nextSteps}\n\n` + - "Check the granola connector's status for this workspace under " + - "Settings > Connections.", - }; -} - -/** - * The env this tool needs beyond `BaseEnv`: the sanctioned - * workflow-artifacts credential trio, populated for every workflow step - * (`apps/sidecar/src/workflow-substrate-factory/step-env.ts`) — the same - * three keys `@corbits/artifact-tools`' read-side bundle declares. - */ -export interface WorkflowArtifactEnv extends BaseEnv { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly address: string; -} - -/** - * `defineTool`'s env-DI factory shape. Needs the sanctioned - * workflow-artifacts credential trio beyond `BaseEnv`. - */ -export const GRANOLA_CALL_REPORT_STATUS_TOOL = defineTool({ - id: "@corbits/workflow-granola-call/report-status", - requires: ["hubArtifactsUrl", "sidecarToken", "address"], - definitions: [{ name: GRANOLA_CALL_REPORT_STATUS_TOOL_NAME }], - factory: (env) => ({ - definitions: [ - { - name: GRANOLA_CALL_REPORT_STATUS_TOOL_NAME, - description: GRANOLA_CALL_REPORT_STATUS_DESCRIPTION, - inputSchema: { - type: "object", - properties: { - reason: { type: "string" }, - callsExamined: { type: "number" }, - nextSteps: { type: "string" }, - }, - required: ["reason", "callsExamined", "nextSteps"], - }, - }, - ], - run: async (call) => { - const parsed = StatusReportArgs(call.arguments); - if (parsed instanceof type.errors) { - return { - callId: call.id, - isError: true, - content: `Invalid arguments for ${GRANOLA_CALL_REPORT_STATUS_TOOL_NAME}: ${parsed.summary}`, - }; - } - const artifact = buildStatusArtifactPayload(parsed); - try { - const created = await createWorkflowArtifact( - { - hubArtifactsUrl: env.hubArtifactsUrl, - sidecarToken: env.sidecarToken, - runAddress: env.address, - }, - artifact, - ); - return { - callId: call.id, - isError: false, - content: JSON.stringify({ - id: created.id, - version: created.version, - title: artifact.title, - kind: artifact.kind, - persisted: true, - }), - }; - } catch (err) { - return { - callId: call.id, - isError: true, - content: `Failed to persist "${artifact.title}" as a Library artifact: ${ - err instanceof Error ? err.message : String(err) - }`, - }; - } - }, - }), -}); diff --git a/workflows/granola-call/src/index.ts b/workflows/granola-call/src/index.ts deleted file mode 100644 index d0885abd74..0000000000 --- a/workflows/granola-call/src/index.ts +++ /dev/null @@ -1,206 +0,0 @@ -// The granola-call workflow: the parent half of the Granola call-notes -// pipeline (CL-5998). Meant to be attached to a Routine on a recurring -// schedule (hourly or daily both work — see the README), it discovers -// recent Granola calls and starts one process-granola-call run per call -// that has no published call-notes artifact yet. Idempotent by design: a -// quiet run (nothing new since the last poll) starts nothing. -// -// This package is installable data. It imports only published platform -// packages, and nothing imports it statically: a host publishes the -// serialized definition as a workflow asset and deploys it through the -// platform's deploy machinery; the execution host materializes it at -// runtime from the deploy alone. -// -// Infrastructure gap (see README "Current limits"): fanning a parent run -// out into per-call child runs is Granola-specific machinery nowhere near -// ready in this host today — no shipped Interchange host resolves the -// `action` primitive `@intx/workflow` already declares (confirmed by -// `@corbits/heartbeat-workflow`'s own README). That gap is unrelated to -// tool access: CL-5999 closed the tool-pin gap this file used to -// document here (`@intx/agent`'s `defineAgent` still does not accept a -// `toolPackagePins` field — it is vendored, read-only source — so the -// agent below is built directly against `AgentDefinition`'s own type, -// which already carries the field). `@corbits/granola-tools` is pinned -// below; whether it *resolves* at deploy time still depends on an -// operator publishing it to a registry the host's tool-package resolver -// can reach (npm, or an Interchange package-registry asset — see -// `apps/hub/src/index.ts`'s `toolPackageRegistries` wiring). Until a -// deploy actually resolves the pin, this definition's system prompt -// still commits it to saying plainly that Granola is not connected -// rather than inventing call data, the same "no fallbacks" standard the -// rest of this repo holds. -// -// Status reporting (CL-6029): the spawn gap above blocks the pipeline's -// real work, not an honest account of a run that did nothing. -// `granola_call_report_status` (`./finalize-tool.ts`) persists a real, -// chip-visible Library artifact — what was examined, why nothing -// started, what to check next — every time a run starts no children, -// instead of a bare "Granola is not connected" line with nothing else. -// It is not approval-gated: a status report has nothing for a human to -// confirm, only what actually happened. - -import type { AgentDefinition, InferencePreference } from "@intx/agent"; -import { defineWorkflow, step } from "@intx/workflow"; -import type { WorkflowDefinition } from "@intx/workflow"; -import type { ToolPackagePin } from "@intx/types/tool-packages"; -import type { CredentialBinding } from "@intx/types"; - -import { GRANOLA_CALL_REPORT_STATUS_TOOL_NAME } from "./finalize-tool"; - -export const GRANOLA_CALL_WORKFLOW_ID = "wf_granola_call"; -export const GRANOLA_CALL_STEP_ID = "granola-call"; - -/** Recent calls examined per run when nothing else says otherwise. */ -export const DEFAULT_CALL_LIMIT = 10; - -export const GRANOLA_CALL_SYSTEM_PROMPT = - "You run the Granola call-notes pipeline for this workspace, on a " + - "recurring schedule. Each run: list recent Granola calls, and for " + - "every call that has no published call-notes artifact yet, start one " + - `process-granola-call run for it. Look at no more than ${DEFAULT_CALL_LIMIT} ` + - "calls per run unless the trigger says otherwise. Never reprocess a " + - "call that already has a published call-notes artifact — that check " + - "is what keeps this idempotent. If a call's transcript is missing or " + - "unreadable, skip only that call and continue with the rest of the " + - "batch; never let one bad call stop the run. " + - "If a run starts no process-granola-call children — no way to reach " + - "Granola at all, or Granola is connected but there is nothing new to " + - "process — never invent call counts, call names, or notes. Instead " + - `call \`${GRANOLA_CALL_REPORT_STATUS_TOOL_NAME}\` exactly once with a ` + - "plain-language reason grounded in what actually happened, how many " + - "calls you actually examined (0 if you had no way to reach Granola at " + - "all), and next steps a human can check, starting with the granola " + - "connector's connection status for this workspace. This call requires " + - "no approval; after it completes, give a one-sentence plain-text " + - "summary of the same finding as your reply."; - -/** Tool packages this definition pins (CL-5999); see the header comment. */ -export const GRANOLA_CALL_TOOL_PACKAGE_PINS: readonly ToolPackagePin[] = [ - { name: "@corbits/granola-tools", version: "0.0.5" }, -]; - -/** - * Binds `@corbits/granola-tools`' declared "granola" handle to a - * tenant-owned Granola credential (CL-6028). The launch resolves this - * against `buildCredentialDelivery`, materializing a consumer-scoped - * `credential:{id}` / `use` grant for the pinned package — see - * `tools/granola/src/tool.ts`'s header comment for the runtime - * gap that still separates a binding resolved at launch and a credential - * actually reachable by the tool at call time. - */ -export const GRANOLA_CALL_CREDENTIAL_BINDINGS: readonly CredentialBinding[] = [ - { - package: "@corbits/granola-tools", - handle: "granola", - provider: "granola", - locator: "tenant", - }, -]; - -/** - * Everything the definition needs that is per-deployment data. The - * trigger address names a specific deployment's inbox — for this - * workflow, the address a Routine's scheduled trigger mail lands on — so - * a definition built here is per-deployment by construction. - */ -export interface GranolaCallWorkflowInput { - /** The deployment's mail address; each inbound mail is one run. */ - readonly triggerAddress: string; - /** Provider/model preferences, in order; resolved at deploy time. */ - readonly inferencePreferences: readonly InferencePreference[]; - /** Per-turn timeout in milliseconds, enforced on the single step. */ - readonly turnTimeoutMs: number; -} - -/** - * Builds the granola-call definition. Exactly one step, matching the - * shape every other definition in this repo commits to: tools and any - * future spawn machinery arrive as packages on the deploy, never inlined - * here, keeping the definition pure data. - * - * The step always sets an explicit `timeout` — the singular `agent:` - * shorthand sets none, and a wedged inference call would then hang a run - * forever. - */ -export function buildGranolaCallWorkflow(input: GranolaCallWorkflowInput): WorkflowDefinition { - if (input.triggerAddress === "") { - throw new Error("buildGranolaCallWorkflow requires a non-empty triggerAddress"); - } - if (!Number.isInteger(input.turnTimeoutMs) || input.turnTimeoutMs <= 0) { - throw new Error("buildGranolaCallWorkflow requires turnTimeoutMs to be a positive integer"); - } - return defineWorkflow({ - id: GRANOLA_CALL_WORKFLOW_ID, - trigger: { type: "mail", to: input.triggerAddress }, - credentialBindings: GRANOLA_CALL_CREDENTIAL_BINDINGS, - steps: { - [GRANOLA_CALL_STEP_ID]: step({ - agent: { - id: GRANOLA_CALL_STEP_ID, - description: - "Polls Granola for new calls and starts one call-notes run " + - "per call that has not been processed yet", - systemPrompt: GRANOLA_CALL_SYSTEM_PROMPT, - toolFactories: [], - capabilities: [], - inference: { sources: input.inferencePreferences }, - toolPackagePins: GRANOLA_CALL_TOOL_PACKAGE_PINS, - } satisfies AgentDefinition, - timeout: input.turnTimeoutMs, - }), - }, - }); -} - -/** - * Serializes a definition to the JSON a workflow asset carries. The - * definition must survive the asset round-trip byte-faithfully, so - * anything JSON would silently drop or mangle — functions, undefined, - * symbols, bigints, non-finite numbers, class instances — is a loud - * error naming the offending path instead of a corrupted asset. - */ -export function serializeGranolaCallWorkflow(definition: WorkflowDefinition): string { - assertJsonPortable(definition, "definition"); - return JSON.stringify(definition); -} - -function assertJsonPortable(value: unknown, path: string): void { - if (value === null) return; - switch (typeof value) { - case "string": - case "boolean": - return; - case "number": - if (!Number.isFinite(value)) { - throw new Error(`${path} is a non-finite number; JSON drops it`); - } - return; - case "object": - break; - default: - throw new Error( - `${path} is a ${typeof value}, which does not survive JSON ` + "serialization", - ); - } - if (Array.isArray(value)) { - value.forEach((element, index) => { - assertJsonPortable(element, `${path}[${index}]`); - }); - return; - } - const proto: unknown = Object.getPrototypeOf(value); - if (proto !== Object.prototype && proto !== null) { - throw new Error(`${path} is a non-plain object; JSON would flatten it lossily`); - } - for (const [key, entry] of Object.entries(value)) { - assertJsonPortable(entry, `${path}.${key}`); - } -} - -export { - GRANOLA_CALL_REPORT_STATUS_TOOL, - GRANOLA_CALL_REPORT_STATUS_TOOL_NAME, - GRANOLA_CALL_REPORT_STATUS_DESCRIPTION, - buildStatusArtifactPayload, -} from "./finalize-tool"; -export type { ArtifactPayload, StatusReportArgs } from "./finalize-tool"; diff --git a/workflows/granola-call/test/boundary.test.ts b/workflows/granola-call/test/boundary.test.ts deleted file mode 100644 index 4f55feb147..0000000000 --- a/workflows/granola-call/test/boundary.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -// This package is installable data on the native workflow contract: -// its shipped sources import only published platform packages, so the -// source tree plus the package manifest deploys on any Interchange -// instance without a wrapper contract. - -import { readFile, readdir } from "node:fs/promises"; -import path from "node:path"; -import { expect, test } from "bun:test"; - -const ALLOWED_IMPORT_PREFIXES = ["@intx/", "arktype", "./", "../"]; - -async function listFiles(dir: string): Promise { - const entries = await readdir(dir, { withFileTypes: true }); - const files: string[] = []; - for (const entry of entries) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...(await listFiles(full))); - } else { - files.push(full); - } - } - return files; -} - -async function shippedFiles(): Promise { - const packageRoot = path.join(import.meta.dir, ".."); - return [ - ...(await listFiles(path.join(packageRoot, "src"))), - path.join(packageRoot, "package.json"), - ]; -} - -test("shipped sources import only published platform packages", async () => { - const importPattern = /from\s+"([^"]+)"/g; - const violations: string[] = []; - for (const file of await shippedFiles()) { - if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue; - const content = await readFile(file, "utf8"); - for (const match of content.matchAll(importPattern)) { - const specifier = match[1] ?? ""; - const allowed = ALLOWED_IMPORT_PREFIXES.some((prefix) => specifier.startsWith(prefix)); - if (!allowed) { - violations.push(`${path.basename(file)}: ${specifier}`); - } - } - } - expect(violations).toEqual([]); -}); diff --git a/workflows/granola-call/test/definition.test.ts b/workflows/granola-call/test/definition.test.ts deleted file mode 100644 index acb42f3cf3..0000000000 --- a/workflows/granola-call/test/definition.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -// Tests for this package's own contract: the shape our factory -// commits to, its serialization guarantees, and its boundary. The -// platform's own normalization and validation are its business, not -// re-proven here. - -import { expect, test } from "bun:test"; -import type { StepPrimitive, WorkflowDefinition } from "@intx/workflow"; - -import { - DEFAULT_CALL_LIMIT, - GRANOLA_CALL_CREDENTIAL_BINDINGS, - GRANOLA_CALL_REPORT_STATUS_TOOL_NAME, - GRANOLA_CALL_STEP_ID, - GRANOLA_CALL_SYSTEM_PROMPT, - GRANOLA_CALL_TOOL_PACKAGE_PINS, - GRANOLA_CALL_WORKFLOW_ID, - buildGranolaCallWorkflow, - serializeGranolaCallWorkflow, -} from "../src/index"; - -const INPUT = { - triggerAddress: "ins_dep000000000000@example.test", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 600000, -} as const; - -function granolaCallStep(definition: WorkflowDefinition): StepPrimitive { - const primitive = definition.steps[GRANOLA_CALL_STEP_ID]; - if (primitive === undefined || primitive.kind !== "step") { - throw new Error(`definition has no step primitive named ${GRANOLA_CALL_STEP_ID}`); - } - return primitive; -} - -test("the definition has exactly one step", () => { - const definition = buildGranolaCallWorkflow(INPUT); - expect(definition.stepOrder).toEqual([GRANOLA_CALL_STEP_ID]); - expect(Object.keys(definition.steps)).toEqual([GRANOLA_CALL_STEP_ID]); -}); - -test("the step carries an explicit per-turn timeout", () => { - const definition = buildGranolaCallWorkflow(INPUT); - expect(granolaCallStep(definition).timeout).toBe(INPUT.turnTimeoutMs); -}); - -test("the workflow is triggered by mail to the given deployment address", () => { - const definition = buildGranolaCallWorkflow(INPUT); - expect(definition.id).toBe(GRANOLA_CALL_WORKFLOW_ID); - expect(definition.triggers).toEqual([{ type: "mail", to: INPUT.triggerAddress }]); -}); - -test("the agent instructs the pipeline, carries the preferences, and inlines no tools", () => { - const agent = granolaCallStep(buildGranolaCallWorkflow(INPUT)).agent; - expect(agent.systemPrompt).toBe(GRANOLA_CALL_SYSTEM_PROMPT); - expect(agent.inference.sources).toEqual([...INPUT.inferencePreferences]); - // Tools arrive as packages on the deploy, never inlined here: an - // inline factory is a function-valued field the asset cannot carry. - expect(agent.toolFactories).toEqual([]); -}); - -test("the agent pins @corbits/granola-tools by name and version", () => { - const agent = granolaCallStep(buildGranolaCallWorkflow(INPUT)).agent; - expect(agent.toolPackagePins).toEqual([...GRANOLA_CALL_TOOL_PACKAGE_PINS]); - expect(GRANOLA_CALL_TOOL_PACKAGE_PINS).toEqual([ - { name: "@corbits/granola-tools", version: "0.0.5" }, - ]); -}); - -test("the system prompt commits to the default call limit and an honest no-connection message", () => { - expect(GRANOLA_CALL_SYSTEM_PROMPT).toContain(String(DEFAULT_CALL_LIMIT)); - expect(GRANOLA_CALL_SYSTEM_PROMPT.toLowerCase()).toContain("never invent call counts"); -}); - -test("the system prompt names the status-report tool for a run that starts no children", () => { - expect(GRANOLA_CALL_SYSTEM_PROMPT).toContain(GRANOLA_CALL_REPORT_STATUS_TOOL_NAME); - expect(GRANOLA_CALL_SYSTEM_PROMPT.toLowerCase()).toContain("granola connector"); -}); - -test("the system prompt commits to skip-and-continue on a bad call, never blocking the batch", () => { - expect(GRANOLA_CALL_SYSTEM_PROMPT.toLowerCase()).toContain("skip only that call"); -}); - -test("the definition binds @corbits/granola-tools' declared handle to a tenant-owned granola credential", () => { - const definition = buildGranolaCallWorkflow(INPUT); - expect(definition.credentialBindings).toEqual([...GRANOLA_CALL_CREDENTIAL_BINDINGS]); - expect(GRANOLA_CALL_CREDENTIAL_BINDINGS).toEqual([ - { - package: "@corbits/granola-tools", - handle: "granola", - provider: "granola", - locator: "tenant", - }, - ]); -}); - -test("the definition survives the workflow-asset JSON round-trip", () => { - const definition = buildGranolaCallWorkflow(INPUT); - const revived: unknown = JSON.parse(serializeGranolaCallWorkflow(definition)); - expect(revived).toEqual(definition); -}); - -test("serialization fails loud on a function-valued field, naming its path", () => { - const poisoned = { - id: GRANOLA_CALL_WORKFLOW_ID, - triggers: [{ type: "manual" }], - stepOrder: [GRANOLA_CALL_STEP_ID], - steps: { - [GRANOLA_CALL_STEP_ID]: { - kind: "step", - id: GRANOLA_CALL_STEP_ID, - drainBehavior: "cancel", - agent: { - id: GRANOLA_CALL_STEP_ID, - systemPrompt: GRANOLA_CALL_SYSTEM_PROMPT, - toolFactories: [() => []], - capabilities: [], - inference: { sources: [] }, - }, - }, - }, - } as unknown as WorkflowDefinition; - expect(() => serializeGranolaCallWorkflow(poisoned)).toThrow( - /steps\.granola-call\.agent\.toolFactories\[0\]/, - ); -}); - -test("an empty trigger address is rejected", () => { - expect(() => buildGranolaCallWorkflow({ ...INPUT, triggerAddress: "" })).toThrow( - /triggerAddress/, - ); -}); - -test("a non-positive or fractional turn timeout is rejected", () => { - expect(() => buildGranolaCallWorkflow({ ...INPUT, turnTimeoutMs: 0 })).toThrow(/turnTimeoutMs/); - expect(() => buildGranolaCallWorkflow({ ...INPUT, turnTimeoutMs: 0.5 })).toThrow(/turnTimeoutMs/); -}); diff --git a/workflows/granola-call/tsconfig.json b/workflows/granola-call/tsconfig.json deleted file mode 100644 index a8c2b9966d..0000000000 --- a/workflows/granola-call/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src", "test"], - "exclude": [] -} diff --git a/workflows/granola-call/tsconfig.src.json b/workflows/granola-call/tsconfig.src.json deleted file mode 100644 index fc8724305f..0000000000 --- a/workflows/granola-call/tsconfig.src.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } -} diff --git a/workflows/heartbeat/LICENSE b/workflows/heartbeat/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/workflows/heartbeat/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/workflows/heartbeat/README.md b/workflows/heartbeat/README.md deleted file mode 100644 index cd81fadf50..0000000000 --- a/workflows/heartbeat/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# @corbits/heartbeat-workflow - -The smallest workflow definition in the catalog: a single mail-triggered -step that completes immediately on every trigger. It exists to give -Interchange's scheduling and mail-trigger paths a target that can run -continuously — every few seconds, if desired — without anyone worrying -about cost. - -## What it does - -One step, one agent, a fixed system prompt that forbids drafting a real -reply. Each inbound mail to the deployment's trigger address is one run; -the run's own lifecycle (when it started, when it completed) is the -"timestamp result" — nothing about the reply text carries information. - -## Cost profile: zero - -`@intx/workflow` has no deterministic, agent-free step primitive that -the shipped hub/sidecar host can execute today — the DSL's `action` -primitive exists, but no production host wires the `invokeAction` -callback it needs, so an `action` step throws at runtime. Reaching the -DSL's only invokable primitive that performs work at all — `step` — means -going through an agent, so this definition is deployed with its -`inferencePreferences` pinned to the hub's `noop-inference` endpoint -(`packages/chat/src/noop-inference.ts`), the same trick channel-host -anchors use to avoid burning a real model call on every message (see -that file's header comment for the pin shape). - -Under that pin, every run resolves against a constant, locally served -SSE response — no request ever reaches a real provider, so triggering -this workflow as often as scheduling allows costs nothing. - -## Usage - -```ts -import { buildHeartbeatWorkflow, serializeHeartbeatWorkflow } from "@corbits/heartbeat-workflow"; - -const definition = buildHeartbeatWorkflow({ - triggerAddress: "heartbeat@tenant.example", - inferencePreferences: [{ provider: "anthropic", model: "noop" }], - turnTimeoutMs: 60_000, -}); - -const json = serializeHeartbeatWorkflow(definition); -``` - -See [`workflows/README.md`](../README.md#status-note) for what -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). - -## Deploy from the npm registry - -Once published, deploy this workflow definition to a tenant by name and -version pin: - -``` -POST /api/tenants/:id/workflows/deployments -{ - "source": { "kind": "registry", "registry": "npm" }, - "entry": "./src/index.ts", - "pin": "@corbits/heartbeat-workflow@0.0.1", - "sourceOfferingIds": ["", ...], - "defaultSourceOfferingId": "" -} -``` - -`entry` is the package's `interchange.workflow` module path; `pin` is -`"@corbits/heartbeat-workflow@0.0.1"` or a semver range on the same name. The hub -installs, probes, gates, and freezes the definition from the registry -tarball before creating the deployment. diff --git a/workflows/heartbeat/package.json b/workflows/heartbeat/package.json deleted file mode 100644 index 8a4db7527a..0000000000 --- a/workflows/heartbeat/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "@corbits/heartbeat-workflow", - "version": "0.0.1", - "description": "Minimal mail-triggered workflow that completes immediately, exercising Interchange's scheduling path at zero inference cost", - "license": "LGPL-2.1-or-later", - "repository": { - "type": "git", - "url": "https://github.com/corbitsdev/workbench.git", - "directory": "workflows/heartbeat" - }, - "files": [ - "src", - "LICENSE", - "README.md" - ], - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/workflow": "0.3.0" - }, - "corbits": { - "workflow": { - "assetName": "heartbeat", - "displayName": "Heartbeat", - "automatable": true - } - }, - "interchange": { - "workflow": "./src/index.ts" - } -} diff --git a/workflows/heartbeat/src/index.ts b/workflows/heartbeat/src/index.ts deleted file mode 100644 index fc0175dcda..0000000000 --- a/workflows/heartbeat/src/index.ts +++ /dev/null @@ -1,140 +0,0 @@ -// The heartbeat workflow: the smallest possible true consumer of the -// native workflow contract, meant to run on a tight, continuous -// schedule so that the platform's scheduling and mail-trigger paths -// stay exercised in the background. It is a single-step, mail-triggered -// definition whose agent replies with a fixed, zero-content -// acknowledgement — the definition carries no per-run computation at -// all, so the run's own recorded lifecycle (its trigger time and -// completion) is the "timestamp result" this workflow exists to -// produce, not anything the agent says. -// -// No agent-free step primitive runs on the deployed host today (the -// `action` primitive exists in `@intx/workflow`, but no shipped host -// wires an `invokeAction` callback to resolve it — see -// VENDORED.md-adjacent research notes in this package's README), so -// this step is unavoidably an agent step. CL-8160 stopped pinning a -// deployer-supplied synthetic noop `ModelSource` here: a scheduled step -// that needs no model must not call one, but with no non-agent step -// shape available the least-bad option is to deploy against the -// tenant's own real, resolved catalog offering like every other -// workflow (the same `resolveRealSourceOfferingIds` pattern the deleted -// hub-side seed step used) rather than inventing a -// fake provider to route around a step that will still make a real -// call. This definition still accepts `inferencePreferences` as plain -// deploy-time data; it is the deployer's choice, never this -// definition's, what those preferences resolve against. -// -// This package is installable data. It imports only published platform -// packages, and nothing imports it statically: a host publishes the -// serialized definition as a workflow asset and deploys it through the -// platform's deploy machinery; the execution host materializes it at -// runtime from the deploy alone. - -import { defineAgent } from "@intx/agent"; -import type { InferencePreference } from "@intx/agent"; -import { defineWorkflow, step } from "@intx/workflow"; -import type { WorkflowDefinition } from "@intx/workflow"; - -export const HEARTBEAT_WORKFLOW_ID = "wf_heartbeat"; -export const HEARTBEAT_STEP_ID = "heartbeat"; - -export const HEARTBEAT_SYSTEM_PROMPT = - "You are a heartbeat check. You exist only to let a run complete; " + - "never draft a reply of your own."; - -/** - * Everything the definition needs that is per-deployment data. The - * trigger address names a specific deployment's inbox, so a definition - * built here is per-deployment by construction. - */ -export interface HeartbeatWorkflowInput { - /** The deployment's mail address; each inbound mail is one run. */ - readonly triggerAddress: string; - /** Provider/model preferences, in order; resolved at deploy time. */ - readonly inferencePreferences: readonly InferencePreference[]; - /** Per-turn timeout in milliseconds, enforced on the single step. */ - readonly turnTimeoutMs: number; -} - -/** - * Builds the heartbeat definition. Exactly one step, matching the - * shape every other definition in this repo commits to; a second step - * would give a heartbeat run more to fail on for no benefit. - * - * The step always sets an explicit `timeout` — the singular `agent:` - * shorthand sets none, and a wedged inference call would then hang a - * run forever. Tools are never inlined on the definition: they arrive - * as packages on the deploy, keeping the definition pure data. - */ -export function buildHeartbeatWorkflow(input: HeartbeatWorkflowInput): WorkflowDefinition { - if (input.triggerAddress === "") { - throw new Error("buildHeartbeatWorkflow requires a non-empty triggerAddress"); - } - if (!Number.isInteger(input.turnTimeoutMs) || input.turnTimeoutMs <= 0) { - throw new Error("buildHeartbeatWorkflow requires turnTimeoutMs to be a positive integer"); - } - return defineWorkflow({ - id: HEARTBEAT_WORKFLOW_ID, - trigger: { type: "mail", to: input.triggerAddress }, - steps: { - heartbeat: step({ - agent: defineAgent({ - id: HEARTBEAT_STEP_ID, - description: - "Completes immediately on every trigger, proving the " + - "scheduling and mail-trigger paths are alive", - systemPrompt: HEARTBEAT_SYSTEM_PROMPT, - tools: [], - capabilities: [], - inference: { sources: input.inferencePreferences }, - }), - timeout: input.turnTimeoutMs, - }), - }, - }); -} - -/** - * Serializes a definition to the JSON a workflow asset carries. The - * definition must survive the asset round-trip byte-faithfully, so - * anything JSON would silently drop or mangle — functions, undefined, - * symbols, bigints, non-finite numbers, class instances — is a loud - * error naming the offending path instead of a corrupted asset. - */ -export function serializeHeartbeatWorkflow(definition: WorkflowDefinition): string { - assertJsonPortable(definition, "definition"); - return JSON.stringify(definition); -} - -function assertJsonPortable(value: unknown, path: string): void { - if (value === null) return; - switch (typeof value) { - case "string": - case "boolean": - return; - case "number": - if (!Number.isFinite(value)) { - throw new Error(`${path} is a non-finite number; JSON drops it`); - } - return; - case "object": - break; - default: - throw new Error( - `${path} is a ${typeof value}, which does not survive JSON ` + "serialization", - ); - } - if (Array.isArray(value)) { - value.forEach((element, index) => { - assertJsonPortable(element, `${path}[${index}]`); - }); - return; - } - const proto: unknown = Object.getPrototypeOf(value); - if (proto !== Object.prototype && proto !== null) { - throw new Error(`${path} is a non-plain object; JSON would flatten it lossily`); - } - for (const [key, entry] of Object.entries(value)) { - assertJsonPortable(entry, `${path}.${key}`); - } -} diff --git a/workflows/heartbeat/test/boundary.test.ts b/workflows/heartbeat/test/boundary.test.ts deleted file mode 100644 index 7cd3621b2f..0000000000 --- a/workflows/heartbeat/test/boundary.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -// This package is installable data on the native workflow contract: -// its shipped sources import only published platform packages, so the -// source tree plus the package manifest deploys on any Interchange -// instance without a wrapper contract. - -import { readFile, readdir } from "node:fs/promises"; -import path from "node:path"; -import { expect, test } from "bun:test"; - -const ALLOWED_IMPORT_PREFIXES = ["@intx/", "./", "../"]; - -async function listFiles(dir: string): Promise { - const entries = await readdir(dir, { withFileTypes: true }); - const files: string[] = []; - for (const entry of entries) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...(await listFiles(full))); - } else { - files.push(full); - } - } - return files; -} - -async function shippedFiles(): Promise { - const packageRoot = path.join(import.meta.dir, ".."); - return [ - ...(await listFiles(path.join(packageRoot, "src"))), - path.join(packageRoot, "package.json"), - ]; -} - -test("shipped sources import only published platform packages", async () => { - const importPattern = /from\s+"([^"]+)"/g; - const violations: string[] = []; - for (const file of await shippedFiles()) { - if (!file.endsWith(".ts")) continue; - const content = await readFile(file, "utf8"); - for (const match of content.matchAll(importPattern)) { - const specifier = match[1] ?? ""; - const allowed = ALLOWED_IMPORT_PREFIXES.some((prefix) => specifier.startsWith(prefix)); - if (!allowed) { - violations.push(`${path.basename(file)}: ${specifier}`); - } - } - } - expect(violations).toEqual([]); -}); diff --git a/workflows/heartbeat/test/definition.test.ts b/workflows/heartbeat/test/definition.test.ts deleted file mode 100644 index c068b2bd6c..0000000000 --- a/workflows/heartbeat/test/definition.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -// Tests for this package's own contract: the shape our factory -// commits to, its serialization guarantees, and its boundary. The -// platform's own normalization and validation are its business, not -// re-proven here. - -import { expect, test } from "bun:test"; -import type { StepPrimitive, WorkflowDefinition } from "@intx/workflow"; - -import { - HEARTBEAT_STEP_ID, - HEARTBEAT_SYSTEM_PROMPT, - HEARTBEAT_WORKFLOW_ID, - buildHeartbeatWorkflow, - serializeHeartbeatWorkflow, -} from "../src/index"; - -const INPUT = { - triggerAddress: "ins_dep000000000000@example.test", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 60000, -} as const; - -function heartbeatStep(definition: WorkflowDefinition): StepPrimitive { - const primitive = definition.steps[HEARTBEAT_STEP_ID]; - if (primitive === undefined || primitive.kind !== "step") { - throw new Error(`definition has no step primitive named ${HEARTBEAT_STEP_ID}`); - } - return primitive; -} - -test("the definition has exactly one step", () => { - const definition = buildHeartbeatWorkflow(INPUT); - expect(definition.stepOrder).toEqual([HEARTBEAT_STEP_ID]); - expect(Object.keys(definition.steps)).toEqual([HEARTBEAT_STEP_ID]); -}); - -test("the step carries an explicit per-turn timeout", () => { - const definition = buildHeartbeatWorkflow(INPUT); - expect(heartbeatStep(definition).timeout).toBe(INPUT.turnTimeoutMs); -}); - -test("the workflow is triggered by mail to the given deployment address", () => { - const definition = buildHeartbeatWorkflow(INPUT); - expect(definition.id).toBe(HEARTBEAT_WORKFLOW_ID); - expect(definition.triggers).toEqual([{ type: "mail", to: INPUT.triggerAddress }]); -}); - -test("the agent carries the fixed prompt, the preferences, and inlines no tools", () => { - const agent = heartbeatStep(buildHeartbeatWorkflow(INPUT)).agent; - expect(agent.systemPrompt).toBe(HEARTBEAT_SYSTEM_PROMPT); - expect(agent.inference.sources).toEqual([...INPUT.inferencePreferences]); - // Tools arrive as packages on the deploy, never inlined here: an - // inline factory is a function-valued field the asset cannot carry. - expect(agent.toolFactories).toEqual([]); -}); - -test("the definition survives the workflow-asset JSON round-trip", () => { - const definition = buildHeartbeatWorkflow(INPUT); - const revived: unknown = JSON.parse(serializeHeartbeatWorkflow(definition)); - expect(revived).toEqual(definition); -}); - -test("serialization fails loud on a function-valued field, naming its path", () => { - const poisoned = { - id: HEARTBEAT_WORKFLOW_ID, - triggers: [{ type: "manual" }], - stepOrder: [HEARTBEAT_STEP_ID], - steps: { - heartbeat: { - kind: "step", - id: HEARTBEAT_STEP_ID, - drainBehavior: "cancel", - agent: { - id: HEARTBEAT_STEP_ID, - systemPrompt: HEARTBEAT_SYSTEM_PROMPT, - toolFactories: [() => []], - capabilities: [], - inference: { sources: [] }, - }, - }, - }, - } as unknown as WorkflowDefinition; - expect(() => serializeHeartbeatWorkflow(poisoned)).toThrow( - /steps\.heartbeat\.agent\.toolFactories\[0\]/, - ); -}); - -test("an empty trigger address is rejected", () => { - expect(() => buildHeartbeatWorkflow({ ...INPUT, triggerAddress: "" })).toThrow(/triggerAddress/); -}); - -test("a non-positive or fractional turn timeout is rejected", () => { - expect(() => buildHeartbeatWorkflow({ ...INPUT, turnTimeoutMs: 0 })).toThrow(/turnTimeoutMs/); - expect(() => buildHeartbeatWorkflow({ ...INPUT, turnTimeoutMs: 0.5 })).toThrow(/turnTimeoutMs/); -}); diff --git a/workflows/heartbeat/tsconfig.json b/workflows/heartbeat/tsconfig.json deleted file mode 100644 index a8c2b9966d..0000000000 --- a/workflows/heartbeat/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src", "test"], - "exclude": [] -} diff --git a/workflows/heartbeat/tsconfig.src.json b/workflows/heartbeat/tsconfig.src.json deleted file mode 100644 index fc8724305f..0000000000 --- a/workflows/heartbeat/tsconfig.src.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } -} diff --git a/workflows/last-30-days-research/LICENSE b/workflows/last-30-days-research/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/workflows/last-30-days-research/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/workflows/last-30-days-research/README.md b/workflows/last-30-days-research/README.md deleted file mode 100644 index 838917a831..0000000000 --- a/workflows/last-30-days-research/README.md +++ /dev/null @@ -1,209 +0,0 @@ -# @corbits/last-30-days-research-workflow - -A multi-step pipeline that researches a topic over the last 30 days -across web search and GitHub and writes it up as one long-form, cited -research report (CL-5879, ported from `gtm-workbench`'s ORIGINAL -`last30days-research` workflow — grounding -> gather -> entity-extract -> -gather -> curate -> write — replacing this repo's prior single-step -scoped-down port, CL-5997). - -## What the OG pipeline does - -1. **intake** — collect the topic and an optional focus. -2. **ground** — LLM turns the topic + focus into one tailored search - query per platform. -3. **groundQueries** — parse the grounding reply into a per-source query - map. -4. **sources (round 1)** — one fetch per platform (nine platforms in the - OG: three web queries, Hacker News, GitHub, Reddit, X, YouTube, - Polymarket), chained serially. -5. **entities** — LLM reads round 1's results and names the concrete - launches/entities worth chasing deeper, writing a follow-up query per - platform. -6. **entityQueries** — parse the entity reply into a round-2 query map. -7. **sources (round 2)** — four more fetches (web, Reddit, X, YouTube) - targeting the discovered entities, chained serially. -8. **collect** — deterministically dedupe, date-filter, and drop - structural junk from the pooled results. -9. **curate** — LLM (heavier writer-tier model) drops promo/off-topic - items, groups survivors into 3-6 named themes, and pulls verbatim - community quotes. -10. **brief** — assemble the structured report from curate's output (or a - deterministic fallback). -11. **write** — LLM (heavier writer-tier model) writes the long-form, - cited report from the brief. -12. **document** / **persist** — pair title + body and write the - `research` artifact. - -## Adaptations this port makes - -**Tool-dispatch shape.** The OG's per-source fetches and its -`groundQueries`/`entityQueries`/`collect`/`brief`/`document`/`persist` -steps were all native `@intx/workflow` `action` primitives, dispatched -through an `ActionInvoker` the OG's execution host wires up. This repo's -host (`apps/sidecar`) has never wired one — `action` is a real primitive -with no host support here, and every existing workflow package in this -catalog calls tools as agent capabilities inside a reasoning turn -instead. This port keeps the OG's step-by-step SHAPE but expresses every -former action step as a plain reasoning step, folding the OG's -deterministic `collect` dedupe/date-filter/junk-drop pass into the -`curate` step's own prompt (see `src/prompts.ts`'s header comment) since -there is no deterministic-step primitive available to carry it on its -own. This is the first genuine multi-step (non-folded) workflow -definition in this catalog — every other package here is single-step by -convention (see `docs/AGENTS-PAGE.md`'s CL-5999 gap). The deploy-time -per-step model resolution this shape assumes (next point) is accordingly -unproven end-to-end here; flagged as an honest gap below. - -**Model tiers.** The OG pinned a literal writer-tier model (`kimi-k2.6` -via `openai-compatible`) on `curate`/`write`, leaving `ground`/`entities` -on the deploy default. This catalog's benches carry Ollama (qwen, ...) -and Anthropic, never `openai-compatible`/kimi, so `curate`/`write` here -prepend `WRITER_INFERENCE_PREFERENCE` (`anthropic` / `claude-sonnet-5`) -ahead of the caller's own `inferencePreferences` — the same -"grounding/entity-extraction stay on the deploy default, genuine -editorial judgment and long-form synthesis get the heavier tier" split -the OG's own comment documents, just resolved against this deployment's -catalog instead of the OG's. **Known gap**: this repo's deploy-time model -resolution (`vendor/intx/hub-api/src/run-source-resolution.ts`, -`vendor/intx/workflow-deploy/src/fold-synthesis.ts`) is proven only for -single-step ("folded") definitions, which read a single, definition-wide -model off `steps[0].agent.inference.sources[0]`. Whether a genuine -multi-step definition's per-step `inference.sources` is honored past -step 0 by this repo's deploy pipeline has not been exercised end-to-end; -`WRITER_INFERENCE_PREFERENCE` is the correct definition-level intent -regardless, and this note exists so that gap gets closed deliberately, -not rediscovered. - -**Sources.** Same scope as the prior port: `web_search` -(`@corbits/web-search-tools`, Exa-backed — the same provider the OG -used) and `github_activity` (`@corbits/github-tools`, GitHub's public -REST search, keyless-capable). Hacker News, Reddit, X, YouTube, and -Polymarket stay named as "not yet connected" in the gathering steps' -prompt rather than silently dropped (Bluesky was already disabled -upstream for broken auth and is skipped entirely, as before). - -**Corbits vocabulary.** Kept verbatim (`src/prompts.ts`'s -`CORBITS_VOCABULARY`) on every reasoning step — the brand-correctness -guidance applies here exactly as it did in the OG. - -**Intake.** The OG parks on `awaitSignal("intake")` so a human stepper -can fill topic/focus after the run starts. Workbench collects those -fields on the routine _before_ launch and delivers them as the first-turn -mail. There is no intake gate: `ground` is the first step and every -reasoning step that needs the topic/focus reads `{ from: "trigger.payload" }`. - -**Report structure.** This port keeps its own established four-heading -contract (Overview / Key findings / Sources consulted / Citations) -rather than adopting the OG's TL;DR/per-theme-heading structure, since -that contract is what this deployment's delivery and tests already -commit to. - -## What of the prior (CL-5997) single-step port is kept vs. replaced - -**Replaced**: the single reasoning step and its monolithic system prompt -are gone, split into the six-step pipeline in `src/index.ts`, with -`src/prompts.ts` holding each step's prompt. - -**Kept**: `src/finalize-tool.ts` and `src/artifact-client.ts`, unchanged. -The OG's `persist` step dispatched a native `write_artifact` action tool -with no human approval; this repo has no equivalent action-dispatched -tool, and this repo's own ground rule ("every external side effect sits -behind human approval," `AGENTS.md`) means an unapproved persist would be -a policy regression even if one existed. `last_30_days_research_finalize` -(`approval: "ask"`) is this deployment's only proven way to reach the -Library engine from a workflow tool package -(`createWorkflowArtifact`, proven by `pain-point-collateral`/ -`collateral-generation`, CL-6000) and is exactly the shape -`packages/chat/src/artifact-delivery.ts` recognizes, so it stays, called -once by the final `write` step. - -## Delivery - -The `write` step is the pipeline's last step; its own reply (after its -one finalize tool call resolves) is delivered through the run's normal -mail-reply path — the same delivery every other workflow in this catalog -uses, unchanged by this port going multi-step. No separate -"document"/"persist" step is needed for delivery: the writer's own -tool-calling turn is the finalize-and-reply step. - -## Current limits (read before deploying) - -No tool-package pin yet (CL-5999): every reasoning step ships with -`tools: []`/`capabilities: []`, same as every other workflow in this -catalog, until a production workflow builder can thread -`toolPackagePins` onto a built definition. Without a pin, `web_search` -and `github_activity` are simply absent at runtime and the gathering -steps honestly report both as unreachable. - -Multi-step deploy resolution (see "Model tiers" above): this is the -first non-folded, multi-step definition in this catalog. Its per-step -`after` chaining is a generic `@intx/workflow` runtime feature and -should execute correctly; whether the deploy-time model-catalog -resolution honors a step's own `inference.sources` (rather than only the -`stepOrder[0]` step's) has not been proven against this repo's deploy -pipeline and should be verified before a production deploy. - -The `topic`/`focus` fields are collected on the routine before launch -(`routine_create` / `routine_update` `input`) and delivered as the -first-turn mail. There is no post-launch intake stepper. - -## Usage - -```ts -import { - buildLast30DaysResearchWorkflow, - serializeLast30DaysResearchWorkflow, -} from "@corbits/last-30-days-research-workflow"; - -const definition = buildLast30DaysResearchWorkflow({ - triggerAddress: "last-30-days-research@tenant.example", - inferencePreferences: [{ provider: "ollama", model: "qwen-test" }], - turnTimeoutMs: 300_000, -}); - -const json = serializeLast30DaysResearchWorkflow(definition); -``` - -Pin `@corbits/web-search-tools` (needs a `webSearchApiKey`, i.e. an Exa -key) and `@corbits/github-tools` (works keylessly, or with a -`githubApiKey` for a higher rate limit) on the deployment for the -gathering steps to reach real data. - -### Trigger fields (`topic`, `focus`) - -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 `@corbits/workflows`'s -`triggerFields` for this workflow exactly: - -- `topic` (required) — the free-text subject to research. -- `focus` (optional) — narrows which angle of `topic` to chase across - every source. - -## Registration - -See [`workflows/README.md`](../README.md#status-note) for what -registration/automatable/seeded mean — this one is `automatable: false`, -gated behind a human-supplied topic per run, and not seeded by default. - -## Deploy from the npm registry - -Once published, deploy this workflow definition to a tenant by name and -version pin: - -``` -POST /api/tenants/:id/workflows/deployments -{ - "source": { "kind": "registry", "registry": "npm" }, - "entry": "./src/index.ts", - "pin": "@corbits/last-30-days-research-workflow@0.0.1", - "sourceOfferingIds": ["", ...], - "defaultSourceOfferingId": "" -} -``` - -`entry` is the package's `interchange.workflow` module path; `pin` is -`"@corbits/last-30-days-research-workflow@0.0.1"` or a semver range on the same name. The hub -installs, probes, gates, and freezes the definition from the registry -tarball before creating the deployment. diff --git a/workflows/last-30-days-research/package.json b/workflows/last-30-days-research/package.json deleted file mode 100644 index 530f949b02..0000000000 --- a/workflows/last-30-days-research/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@corbits/last-30-days-research-workflow", - "version": "0.0.1", - "description": "Single-step, mail-triggered workflow that researches a topic across a handful of source platforms and writes a long-form, cited report", - "license": "LGPL-2.1-or-later", - "repository": { - "type": "git", - "url": "https://github.com/corbitsdev/workbench.git", - "directory": "workflows/last-30-days-research" - }, - "files": [ - "src", - "LICENSE", - "README.md" - ], - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "arktype": "catalog:" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/workflow": "0.3.0" - }, - "corbits": { - "workflow": { - "assetName": "last-30-days-research", - "displayName": "Last 30 days research report", - "automatable": false - } - }, - "interchange": { - "workflow": "./src/index.ts" - } -} diff --git a/workflows/last-30-days-research/src/artifact-client.test.ts b/workflows/last-30-days-research/src/artifact-client.test.ts deleted file mode 100644 index 1c86414419..0000000000 --- a/workflows/last-30-days-research/src/artifact-client.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { expect, test } from "bun:test"; - -import { createWorkflowArtifact } from "./artifact-client"; - -const CONFIG = { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - runAddress: "run_1@workflow", -}; - -test("posts to the workflow-artifacts endpoint and returns id/version", async () => { - const captured: { url: string; init: RequestInit | undefined } = { - url: "", - init: undefined, - }; - const fetchImpl = (async (input: URL | string, init?: RequestInit) => { - captured.url = String(input); - captured.init = init; - return new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - }); - }) as unknown as typeof fetch; - - const result = await createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ); - - expect(result).toEqual({ id: "art_1", version: 1 }); - expect(captured.url).toBe("https://hub.example.com/api/workflow-artifacts/artifacts"); - const headers = captured.init?.headers as Record; - expect(headers["authorization"]).toBe("Bearer sc-token"); - expect(headers["x-workflow-run-address"]).toBe("run_1@workflow"); -}); - -test("throws on a non-ok HTTP response", async () => { - const fetchImpl = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/500/); -}); - -test("throws when the response doesn't match the expected shape", async () => { - const fetchImpl = (async () => - new Response(JSON.stringify({ data: {} }), { - status: 201, - })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/did not match the expected shape/); -}); diff --git a/workflows/last-30-days-research/src/artifact-client.ts b/workflows/last-30-days-research/src/artifact-client.ts deleted file mode 100644 index b9d56ca425..0000000000 --- a/workflows/last-30-days-research/src/artifact-client.ts +++ /dev/null @@ -1,62 +0,0 @@ -// A minimal client for the sanctioned workflow-artifacts HTTP surface -// (`@corbits/artifacts-hub`'s `createWorkflowArtifactRoutes`, CL-6000). -// Deliberately duplicated from `@corbits/artifact-tools`' `client.ts` -// rather than imported: this package ships as installable data on the -// native workflow contract (see `./index.ts`'s header comment and -// `test/boundary.test.ts`), so its shipped sources import only `@intx/*`, -// `arktype`, and relative specifiers — never another `@corbits/*` -// package. Authenticates with the sidecar's own bearer token plus the -// run's own mailbox address (both already reach a workflow-process -// child's tool env), never a database handle. -import { type } from "arktype"; - -export interface WorkflowArtifactClientConfig { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly runAddress: string; - /** Override for tests; defaults to the global `fetch`. */ - readonly fetchImpl?: typeof fetch; -} - -export type CreateWorkflowArtifactInput = { - readonly title: string; - readonly kind: string; - readonly content: string; -}; - -export type CreatedWorkflowArtifact = { - readonly id: string; - readonly version: number; -}; - -const CreatedWorkflowArtifactResponse = type({ - data: { id: "string", version: "number" }, -}); - -/** Persists one artifact. Throws on any transport, HTTP, or shape failure. */ -export async function createWorkflowArtifact( - config: WorkflowArtifactClientConfig, - input: CreateWorkflowArtifactInput, -): Promise { - const doFetch = config.fetchImpl ?? fetch; - const response = await doFetch(`${config.hubArtifactsUrl}/api/workflow-artifacts/artifacts`, { - method: "POST", - headers: { - authorization: `Bearer ${config.sidecarToken}`, - "x-workflow-run-address": config.runAddress, - "content-type": "application/json", - }, - body: JSON.stringify(input), - }); - if (!response.ok) { - throw new Error(`Workflow artifact create failed: ${response.status} ${response.statusText}`); - } - const body: unknown = await response.json(); - const parsed = CreatedWorkflowArtifactResponse(body); - if (parsed instanceof type.errors) { - throw new Error( - `Workflow artifact create response did not match the expected shape: ${parsed.summary}`, - ); - } - return parsed.data; -} diff --git a/workflows/last-30-days-research/src/finalize-tool.test.ts b/workflows/last-30-days-research/src/finalize-tool.test.ts deleted file mode 100644 index 7020e4eeec..0000000000 --- a/workflows/last-30-days-research/src/finalize-tool.test.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { expect, test } from "bun:test"; - -import { - LAST_30_DAYS_RESEARCH_FINALIZE_TOOL, - LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME, - buildArtifactPayload, - type WorkflowArtifactEnv, -} from "./finalize-tool"; - -test("the tool's definition marks itself approval-gated", () => { - expect(LAST_30_DAYS_RESEARCH_FINALIZE_TOOL.definitions).toEqual([ - { name: LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME, approval: "ask" }, - ]); -}); - -test("the tool is namespaced under this workflow package, not a shared one", () => { - expect(LAST_30_DAYS_RESEARCH_FINALIZE_TOOL.id).toBe( - "@corbits/workflow-last-30-days-research/finalize", - ); -}); - -test("requires the sanctioned workflow-artifacts env keys", () => { - expect(LAST_30_DAYS_RESEARCH_FINALIZE_TOOL.requires).toEqual([ - "hubArtifactsUrl", - "sidecarToken", - "address", - ]); -}); - -test("buildArtifactPayload marks a real report as a text artifact", () => { - const payload = buildArtifactPayload({ - outcome: "report", - title: "Last 30 days: agentic coding tools", - content: "## Overview\n\nSomething happened", - }); - expect(payload).toEqual({ - title: "Last 30 days: agentic coding tools", - kind: "text", - content: "## Overview\n\nSomething happened", - }); -}); - -test("buildArtifactPayload marks a no-data teaching payload as status-note, not text", () => { - const payload = buildArtifactPayload({ - outcome: "status-note", - title: "Last 30 days: agentic coding tools — no results yet", - content: "`exa` is not connected yet.", - }); - expect(payload).toEqual({ - title: "Last 30 days: agentic coding tools — no results yet", - kind: "status-note", - content: "`exa` is not connected yet.", - }); -}); - -function testEnv(): WorkflowArtifactEnv { - return { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - address: "run_1@workflow", - } as unknown as WorkflowArtifactEnv; -} - -test("run persists the artifact on real invocation (i.e. after approval re-dispatches the call) and reports it persisted", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - })) as unknown as typeof fetch; - - try { - const bundle = LAST_30_DAYS_RESEARCH_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME, - arguments: { - outcome: "report", - title: "Last 30 days: agentic coding tools", - content: "## Overview\n\nSomething happened", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(String(result.content)) as { - id: string; - version: number; - title: string; - kind: string; - persisted: boolean; - }; - expect(parsed).toEqual({ - id: "art_1", - version: 1, - title: "Last 30 days: agentic coding tools", - kind: "text", - persisted: true, - }); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run persists a teaching payload on the no-data path with a status-note kind, distinct from a real report", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response(JSON.stringify({ data: { id: "art_2", version: 1 } }), { - status: 201, - })) as unknown as typeof fetch; - - try { - const bundle = LAST_30_DAYS_RESEARCH_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_2", - name: LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME, - arguments: { - outcome: "status-note", - title: "Last 30 days: agentic coding tools — no results yet", - content: - "This report would have searched the web (`exa`) and " + - "GitHub activity for this topic. `exa` is not connected " + - "yet — connect it in Settings to get a real report next " + - "time.", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(String(result.content)) as { - persisted: boolean; - kind: string; - }; - expect(parsed.persisted).toBe(true); - expect(parsed.kind).toBe("status-note"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run returns an honest error result when persistence fails, never fabricating persisted: true", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - try { - const bundle = LAST_30_DAYS_RESEARCH_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME, - arguments: { - outcome: "report", - title: "Last 30 days: agentic coding tools", - content: "## Overview\n\nSomething happened", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(true); - expect(result.content).toContain("Failed to persist"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run rejects malformed arguments without throwing", async () => { - const bundle = LAST_30_DAYS_RESEARCH_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME, - arguments: {}, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); - -test("run rejects an outcome value outside the two structural kinds", async () => { - const bundle = LAST_30_DAYS_RESEARCH_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME, - arguments: { - outcome: "summary", - title: "Last 30 days: agentic coding tools", - content: "## Overview\n\nSomething happened", - }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); diff --git a/workflows/last-30-days-research/src/finalize-tool.ts b/workflows/last-30-days-research/src/finalize-tool.ts deleted file mode 100644 index 3e16ce2ba5..0000000000 --- a/workflows/last-30-days-research/src/finalize-tool.ts +++ /dev/null @@ -1,162 +0,0 @@ -// The one piece of this workflow gated by human approval: finalizing the -// research report. Kept inside the workflow package rather than a shared -// tool package — same "workflow-specific logic lives in the definition" -// convention `pain-point-collateral`'s `finalize-tool.ts` established — -// this is not a reusable integration on its own. -// -// Approval mechanics: identical to `pain-point-collateral`'s -// `finalize-tool.ts` — `definitions` marks this tool's one definition -// `approval: "ask"`, the platform's native tool-approval gate. Calling it -// suspends the run and creates a real `approval` row; only executes once -// a human approves it. See that file's header comment for the full -// suspend/resume account, which applies unchanged here. -// -// Persistence: `run` below calls `createWorkflowArtifact` -// (`./artifact-client.ts`, duplicated from `@corbits/artifact-tools`' -// client rather than imported — see that file's header for why this -// installable-data package never imports another `@corbits/*` package) -// against the sanctioned workflow-artifacts HTTP surface — authenticated -// with the sidecar's own bearer token and this run's own mailbox -// address, both already present on `env`, never a database handle. A -// successful call returns the persisted artifact's id/version so the -// delivery pipeline can reference it with a file part -// (`packages/chat/src/artifact-delivery.ts`); a failed call surfaces as -// an honest `isError: true` result rather than a fabricated success. -// -// The finalized content is not always a "real" report: on the no-data -// path (no wired source returns anything for the topic) this workflow's -// system prompt (see `./index.ts`) still calls this same tool with a -// teaching payload — what it searched for and which connectors are -// missing — so a run always ends in a persisted, chip-visible artifact -// rather than silence. - -import { type } from "arktype"; -import { defineTool, type BaseEnv } from "@intx/agent"; -import { createWorkflowArtifact } from "./artifact-client"; - -export const LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME = "last_30_days_research_finalize"; - -export const LAST_30_DAYS_RESEARCH_FINALIZE_DESCRIPTION = - "Finalizes the research report, pending human approval, and persists it as a Library artifact."; - -const FinalizeArgs = type({ - /** - * Which of the two shapes this call is: `"report"` for a real, - * populated report; `"status-note"` for the no-data path's teaching - * payload. This is what fixes the persisted artifact's `kind` — the - * model names the outcome, but never supplies `kind` directly, so a - * run can never accidentally (or by prompt drift) mislabel a teaching - * payload as a real report or vice versa. - */ - outcome: "'report'|'status-note'", - /** Short, human-facing title. Doubles as the inbox/approve-card headline. */ - title: "string > 0", - /** The finished report's markdown body — a real report, or an honest teaching payload on the no-data path. */ - content: "string > 0", -}); - -export type FinalizeArgs = typeof FinalizeArgs.infer; - -export type ArtifactPayload = { - title: string; - kind: "text" | "status-note"; - content: string; -}; - -/** - * The payload `createWorkflowArtifact` persists (`{ title, kind, - * content }`). Built here so the persist call is a straight pass-through - * of this object, not a payload assembled at the call site. `kind` comes - * from `args.outcome`, never from free-text the model could drift on. - */ -export function buildArtifactPayload(args: FinalizeArgs): ArtifactPayload { - return { - title: args.title, - kind: args.outcome === "status-note" ? "status-note" : "text", - content: args.content, - }; -} - -/** - * The env this tool needs beyond `BaseEnv`: the sanctioned - * workflow-artifacts credential trio, populated for every workflow step - * (`apps/sidecar/src/workflow-substrate-factory/step-env.ts`) — the same - * three keys `@corbits/artifact-tools`' read-side bundle declares. - */ -export interface WorkflowArtifactEnv extends BaseEnv { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly address: string; -} - -/** - * `defineTool`'s env-DI factory shape. Needs the sanctioned - * workflow-artifacts credential trio beyond `BaseEnv`. - */ -export const LAST_30_DAYS_RESEARCH_FINALIZE_TOOL = defineTool({ - id: "@corbits/workflow-last-30-days-research/finalize", - requires: ["hubArtifactsUrl", "sidecarToken", "address"], - definitions: [ - { - name: LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME, - approval: "ask", - }, - ], - factory: (env) => ({ - definitions: [ - { - name: LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME, - description: LAST_30_DAYS_RESEARCH_FINALIZE_DESCRIPTION, - inputSchema: { - type: "object", - properties: { - outcome: { type: "string", enum: ["report", "status-note"] }, - title: { type: "string" }, - content: { type: "string" }, - }, - required: ["outcome", "title", "content"], - }, - }, - ], - run: async (call) => { - const parsed = FinalizeArgs(call.arguments); - if (parsed instanceof type.errors) { - return { - callId: call.id, - isError: true, - content: `Invalid arguments for ${LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME}: ${parsed.summary}`, - }; - } - const artifact = buildArtifactPayload(parsed); - try { - const created = await createWorkflowArtifact( - { - hubArtifactsUrl: env.hubArtifactsUrl, - sidecarToken: env.sidecarToken, - runAddress: env.address, - }, - artifact, - ); - return { - callId: call.id, - isError: false, - content: JSON.stringify({ - id: created.id, - version: created.version, - title: artifact.title, - kind: artifact.kind, - persisted: true, - }), - }; - } catch (err) { - return { - callId: call.id, - isError: true, - content: `Failed to persist "${artifact.title}" as a Library artifact: ${ - err instanceof Error ? err.message : String(err) - }`, - }; - } - }, - }), -}); diff --git a/workflows/last-30-days-research/src/index.ts b/workflows/last-30-days-research/src/index.ts deleted file mode 100644 index 5b4130850a..0000000000 --- a/workflows/last-30-days-research/src/index.ts +++ /dev/null @@ -1,264 +0,0 @@ -// The last-30-days-research workflow: a single-step, mail-triggered -// pipeline that researches a topic across the last 30 days and writes it -// up as one long-form, cited research report. Ported from the OG -// gtm-workbench's `last30days-research` workflow (CL-5879). -// -// ## What the OG pipeline does, step by step -// intake (await a topic + focus) -> ground (LLM: one tailored search -// query per platform) -> groundQueries (parse) -> nine source fetches, -// chained serially -> entities (LLM: name the concrete launches that -// surfaced, write deeper follow-up queries) -> entityQueries (parse) -> -// four more source fetches (round 2) -> collect (dedupe/date-filter/ -// junk-filter the pool) -> curate (LLM: drop promo/off-topic, group into -// themes, pull verbatim community quotes, on the heavier writer model) -> -// brief (assemble the structured report) -> write (LLM: the long-form -// report, on the heavier writer model) -> document (pair title + body) -> -// persist (write_artifact). -// -// ## Adaptations this port makes -// -// (1) Folded to one step (CL-6495). A prior port of this file (CL-5879) -// shipped the OG's six stages as six chained `@intx/workflow` steps -// (`after: [...]`) — the first genuinely multi-step definition in this -// catalog. Every routine "run now"/scheduled fire in this repo launches -// through `@corbits/workflows`' `readFoldedBody` -// (`apps/hub/src/native-workflow-routine-launch.ts`), and that reader has -// always required exactly one step -// (`packages/workflows/src/definition-projection.ts`): -// it throws `"definition ... is not single-step (N steps)"` synchronously, -// before any run row or sidecar deploy exists, which Hono turns into a -// bare 500 on every single launch. There is no second, native multi-step -// launcher wired anywhere a routine could reach it. So this port folds -// the OG's six stages back into ordered PHASES inside one reasoning -// turn (see `./prompts.ts`) — the same shape every other workflow -// package in this catalog already uses, and the only shape this -// deployment's launcher can actually run. -// -// (2) Tool-dispatch shape: the OG's per-source fetches, `groundQueries`/ -// `entityQueries`/`collect`/`brief`/`document`/`persist` were all native -// `action` primitives with a `handler` string ref an `ActionInvoker` -// resolves. This repo's execution host (`apps/sidecar`) has never wired -// an `ActionInvoker` — `action` is a real `@intx/workflow` primitive with -// no host support here, and every workflow package in this repo calls -// tools as agent capabilities INSIDE a reasoning turn instead. Every OG -// action becomes an instruction inside the single reasoning step's -// prompt (grounding/gathering/entity-extraction/curation/writing, all as -// phases of one turn); the OG's deterministic `collect` dedupe/ -// date-filter pass is folded into the curate phase's own instructions — -// see `./prompts.ts`. -// -// (3) Model tier: the OG pinned a literal writer-tier model -// (`kimi-k2.6`, `openai-compatible`) on the curate/write stages and left -// grounding/entities on the deploy default. Folded into one step, the -// whole turn now rides the caller's own `inferencePreferences` — the -// same single-tier-per-step shape every other folded workflow in this -// catalog already uses; there is no per-phase model swap inside one -// turn. -// -// (4) Sources: the OG fanned out across nine platforms (three web -// queries, Hacker News, GitHub, Reddit, X, YouTube, Polymarket). This -// deployment has a real, honest backend for exactly two — -// `web_search` (`@corbits/web-search-tools`, Exa-backed, the same -// 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 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`). -// -// (5) Corbits vocabulary: kept verbatim (`./prompts.ts`'s -// `CORBITS_VOCABULARY`) — the brand-correctness guidance applies here -// exactly as it did in the OG. -// -// (6) Intake: the OG parks on `awaitSignal("intake")` so a human stepper -// can fill topic/focus after the run starts. Workbench collects those -// fields on the routine *before* launch and delivers them as the -// first-turn mail (`renderRoutineInput` → `trigger.payload`). A gate -// that nobody fulfills hangs the run forever. There is no second -// intake path: the triggering mail *is* the topic/focus. -// -// ## Delivery -// -// `finalize-tool.ts` and `artifact-client.ts` are unchanged from every -// prior port. The OG's `persist` step dispatched a native -// `write_artifact` action tool with NO human approval; this repo has no -// such tool package, AND this repo's own ground rule ("every external -// side effect sits behind human approval", `AGENTS.md`) means an -// unapproved persist would be a policy regression even if a native -// action path existed here. `finalize-tool.ts`'s -// `last_30_days_research_finalize` (`approval: "ask"`) is this -// deployment's only way to reach the Library engine from a workflow tool -// package (`createWorkflowArtifact`, proven by `pain-point-collateral`/ -// `collateral-generation`, CL-6000), called once inside the single -// step's turn. The step's own reply (after its one finalize tool call -// resolves) is the turn output the platform delivers through the run's -// normal mail-reply path — the same delivery every other single-step -// workflow in this catalog already uses. -// -// This package is installable data. It imports only published platform -// packages, and nothing imports it statically: a host publishes the -// serialized definition as a workflow asset and deploys it through the -// platform's deploy machinery; the execution host materializes it at -// runtime from the deploy alone. - -import type { AgentDefinition, InferencePreference } from "@intx/agent"; -import { defineWorkflow, step } from "@intx/workflow"; -import type { WorkflowDefinition } from "@intx/workflow"; -import type { ToolPackagePin } from "@intx/types/tool-packages"; - -import { buildLast30DaysResearchSystemPrompt } from "./prompts"; -import { LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME } from "./finalize-tool"; - -export const LAST_30_DAYS_RESEARCH_WORKFLOW_ID = "wf_last_30_days_research"; - -// The definition's one step id. Exported so tests and any future -// delivery-card renderer can address it without hardcoding the literal. -export const LAST_30_DAYS_RESEARCH_STEP_ID = "last-30-days-research"; - -// Fixed section structure for the report — this deployment's own -// contract (see this file's header comment for why the OG's TL;DR/ -// per-theme-heading structure was not carried over instead). -export const LAST_30_DAYS_RESEARCH_SECTIONS = [ - "Overview", - "Key findings", - "Sources consulted", - "Citations", -] as const; - -export const LAST_30_DAYS_RESEARCH_WIRED_SOURCES = ["Web search", "GitHub"] as const; -export const LAST_30_DAYS_RESEARCH_PENDING_SOURCES = [ - "Hacker News", - "Reddit", - "X", - "YouTube", - "Polymarket", -] as const; - -/** - * Tool packages this definition pins (CL-5999): the two wired sources' - * real backends. Tools are never inlined on the definition — they - * arrive as pinned packages on the deploy, keeping the definition pure - * data. - */ -export const LAST_30_DAYS_RESEARCH_TOOL_PACKAGE_PINS: readonly ToolPackagePin[] = [ - { name: "@corbits/web-search-tools", version: "0.0.4" }, - { name: "@corbits/github-tools", version: "0.0.11" }, -]; - -const SYSTEM_PROMPT = [ - buildLast30DaysResearchSystemPrompt(LAST_30_DAYS_RESEARCH_SECTIONS), - `Finalizing: once you have written the report, call \`${LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME}\` exactly once with outcome "report", a short title (e.g. "Last 30 days: "), and the full markdown report as content. This call requires a human's approval before it completes. Always finalize, even when nothing usable surfaced at all: in that case, still call \`${LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME}\` once with outcome "status-note", a teaching title (e.g. "Last 30 days: — no results yet"), and content that honestly explains what was searched for, names the missing or unreachable connectors by id (\`exa\` for web search), and says what to do next. Never end a run without finalizing — a plain reply with no artifact is not an acceptable outcome, even on the no-data path.`, - "If the finalize call succeeds, present the finalized report as your reply exactly as written, with no commentary about the approval mechanism itself. If the call is denied, reply with one calm, plain sentence that the report was not approved and no action was taken; never present a denial as an error, and never apologize as if something broke.", -].join("\n\n"); - -/** - * Everything the definition needs that is per-deployment data. The - * trigger address names a specific deployment's inbox — for this - * workflow, the address a human messages with a topic to start a run — - * so a definition built here is per-deployment by construction. - */ -export interface Last30DaysResearchWorkflowInput { - /** The deployment's mail address; each inbound mail is one run. */ - readonly triggerAddress: string; - /** Provider/model preferences, in order; resolved at deploy time. */ - readonly inferencePreferences: readonly InferencePreference[]; - /** Per-turn timeout in milliseconds, enforced on the single step. */ - readonly turnTimeoutMs: number; -} - -/** - * Builds the last-30-days-research definition: exactly one mail- - * triggered reasoning step whose triggering mail carries the topic and - * optional focus (see this file's header comment for the full - * step-by-step account and every adaptation from the OG). Tools are - * never inlined on the definition: they arrive as packages on the - * deploy (`@corbits/web-search-tools`, `@corbits/github-tools`), keeping - * the definition pure data — matching every other definition in this - * catalog. - */ -export function buildLast30DaysResearchWorkflow( - input: Last30DaysResearchWorkflowInput, -): WorkflowDefinition { - if (input.triggerAddress === "") { - throw new Error("buildLast30DaysResearchWorkflow requires a non-empty triggerAddress"); - } - if (!Number.isInteger(input.turnTimeoutMs) || input.turnTimeoutMs <= 0) { - throw new Error( - "buildLast30DaysResearchWorkflow requires turnTimeoutMs to be a positive integer", - ); - } - - return defineWorkflow({ - id: LAST_30_DAYS_RESEARCH_WORKFLOW_ID, - trigger: { type: "mail", to: input.triggerAddress }, - steps: { - [LAST_30_DAYS_RESEARCH_STEP_ID]: step({ - agent: { - id: LAST_30_DAYS_RESEARCH_STEP_ID, - description: - "Researches a topic over the last 30 days across web search " + - "and GitHub, and writes a cited report, once a human approves it", - systemPrompt: SYSTEM_PROMPT, - toolFactories: [], - capabilities: [], - inference: { sources: input.inferencePreferences }, - toolPackagePins: LAST_30_DAYS_RESEARCH_TOOL_PACKAGE_PINS, - } satisfies AgentDefinition, - timeout: input.turnTimeoutMs, - }), - }, - }); -} - -/** - * Serializes a definition to the JSON a workflow asset carries. The - * definition must survive the asset round-trip byte-faithfully, so - * anything JSON would silently drop or mangle — functions, undefined, - * symbols, bigints, non-finite numbers, class instances — is a loud - * error naming the offending path instead of a corrupted asset. - */ -export function serializeLast30DaysResearchWorkflow(definition: WorkflowDefinition): string { - assertJsonPortable(definition, "definition"); - return JSON.stringify(definition); -} - -function assertJsonPortable(value: unknown, path: string): void { - if (value === null) return; - switch (typeof value) { - case "string": - case "boolean": - return; - case "number": - if (!Number.isFinite(value)) { - throw new Error(`${path} is a non-finite number; JSON drops it`); - } - return; - case "object": - break; - default: - throw new Error( - `${path} is a ${typeof value}, which does not survive JSON ` + "serialization", - ); - } - if (Array.isArray(value)) { - value.forEach((element, index) => { - assertJsonPortable(element, `${path}[${index}]`); - }); - return; - } - const proto: unknown = Object.getPrototypeOf(value); - if (proto !== Object.prototype && proto !== null) { - throw new Error(`${path} is a non-plain object; JSON would flatten it lossily`); - } - for (const [key, entry] of Object.entries(value)) { - assertJsonPortable(entry, `${path}.${key}`); - } -} - -export { - LAST_30_DAYS_RESEARCH_FINALIZE_TOOL, - LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME, - LAST_30_DAYS_RESEARCH_FINALIZE_DESCRIPTION, - buildArtifactPayload, -} from "./finalize-tool"; -export type { ArtifactPayload, FinalizeArgs } from "./finalize-tool"; diff --git a/workflows/last-30-days-research/src/prompts.test.ts b/workflows/last-30-days-research/src/prompts.test.ts deleted file mode 100644 index 4a162f6bfe..0000000000 --- a/workflows/last-30-days-research/src/prompts.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { CORBITS_VOCABULARY, buildLast30DaysResearchSystemPrompt } from "./prompts"; -import { LAST_30_DAYS_RESEARCH_SECTIONS } from "./index"; - -const prompt = buildLast30DaysResearchSystemPrompt(LAST_30_DAYS_RESEARCH_SECTIONS); - -describe("CORBITS_VOCABULARY", () => { - test("names every canonical Corbits term", () => { - for (const term of ["Corbits", "Corbits.dev", "Interchange", "Faremeter"]) { - expect(CORBITS_VOCABULARY).toContain(term); - } - }); -}); - -describe("phase 1 — ground", () => { - test("tailors a query per wired source and never reuses the raw topic verbatim", () => { - expect(prompt).toMatch(/phase 1/i); - expect(prompt.toLowerCase()).toContain("never reuse the raw topic"); - for (const key of ["web", "github"]) { - expect(prompt.toLowerCase()).toContain(key); - } - }); - - test("never asks for a source this deployment does not have wired", () => { - const groundSection = prompt.slice( - prompt.toLowerCase().indexOf("phase 1"), - prompt.toLowerCase().indexOf("phase 2"), - ); - for (const pending of ["reddit", "hacker news", "youtube", "polymarket"]) { - expect(groundSection.toLowerCase()).not.toContain(pending); - } - }); -}); - -describe("phase 2 & 4 — gather", () => { - test("calls web_search and github_activity and names every not-yet-connected source honestly", () => { - expect(prompt).toContain("web_search"); - expect(prompt).toContain("github_activity"); - expect(prompt.toLowerCase()).toContain("not yet connected"); - expect(prompt.toLowerCase()).toContain("never invent results"); - }); -}); - -describe("phase 3 — extract entities", () => { - test("targets discovered entities rather than the broad topic", () => { - expect(prompt.toLowerCase()).toContain("named entities"); - expect(prompt.toLowerCase()).toContain("not the broad topic"); - }); - - test("falls back to the base topic when a source found nothing usable", () => { - expect(prompt.toLowerCase()).toContain("repeat the base topic"); - }); -}); - -describe("phase 5 — curate", () => { - test("folds in the OG's deterministic collect step: dedupe, date-filter, drop GitHub noise", () => { - const lower = prompt.toLowerCase(); - expect(lower).toContain("dedup"); - expect(lower).toContain("last 30 days"); - expect(lower).toContain("github"); - expect(lower).toContain("50 stars"); - }); - - test("requires grouping into a capped number of named themes", () => { - expect(prompt.toLowerCase()).toContain("hard cap 5"); - }); - - test("excerpts are optional, never fabricated", () => { - const lower = prompt.toLowerCase(); - expect(lower).toContain("optional"); - expect(lower).toContain("never invent one"); - }); - - test("requires an honest unreachable/not-connected accounting", () => { - expect(prompt.toLowerCase()).toContain("unreachable"); - }); -}); - -describe("phase 6 — write", () => { - test("encodes this deployment's own four-heading report contract, in order", () => { - let lastIndex = -1; - for (const heading of LAST_30_DAYS_RESEARCH_SECTIONS) { - const index = prompt.indexOf(heading); - expect(index).toBeGreaterThan(lastIndex); - lastIndex = index; - } - }); - - test("requires every claim in Key findings to trace to a citation", () => { - expect(prompt.toLowerCase()).toContain("every claim here must trace to a citation"); - }); - - test("uses house style: collective voice and hyphen-spaced asides, not I", () => { - const lower = prompt.toLowerCase(); - expect(lower).toContain('collective voice ("we")'); - expect(lower).toContain("not em dashes"); - }); - - test("forbids inventing anything outside what the earlier phases surfaced", () => { - expect(prompt.toLowerCase()).toContain("invent nothing"); - }); - - test("requires an honest no-results state instead of padded empty sections", () => { - expect(prompt).toContain("no source results to report for this topic"); - }); -}); diff --git a/workflows/last-30-days-research/src/prompts.ts b/workflows/last-30-days-research/src/prompts.ts deleted file mode 100644 index 0202a6a9e9..0000000000 --- a/workflows/last-30-days-research/src/prompts.ts +++ /dev/null @@ -1,97 +0,0 @@ -// The single-step system prompt for last-30-days-research (CL-6495, -// ported from `gtm-workbench`'s `last30days-research` workflow — see -// `./index.ts`'s header comment for the full port account). -// -// The OG fanned grounding/gathering out across nine platforms (three web -// queries plus Hacker News, GitHub, Reddit, X, YouTube, Polymarket) with -// a dedicated entity-chasing round 2, each stage its own native `action` -// step. This port keeps the OG's SHAPE — ground -> gather -> extract -// entities -> gather again -> curate -> write — as ordered PHASES inside -// one reasoning turn rather than six chained workflow steps (see -// `./index.ts`'s header comment for why: this repo's only routine -// launcher runs exactly one step), and narrows every source list to the -// two platforms this deployment can actually reach today (`web_search`, -// `github_activity`; see `./index.ts`'s -// `LAST_30_DAYS_RESEARCH_WIRED_SOURCES`). The OG's community-voice -// requirements (verbatim Reddit/X quotes, per-platform engagement units) -// are softened to "when the source material has one" rather than a hard -// minimum, since neither wired source is a community platform the way -// Reddit/X/YouTube are. - -// Every phase carries this so the report spells Corbits/Corbits.dev/ -// Interchange/Faremeter consistently regardless of how the source -// material spelled them — ported verbatim from the OG. -export const CORBITS_VOCABULARY = - "Treat Corbits, Corbits.dev, Interchange, and Faremeter as canonical Corbits names; spell them exactly. When source material contains a clear speech-to-text or spelling variant, use the canonical spelling in your output. Do not replace an ambiguous term unless surrounding context identifies it."; - -/** - * Builds the full single-turn system prompt: six ordered phases the - * agent works through itself in one turn, calling `web_search` and - * `github_activity` as ordinary tools during the two gathering phases, - * then finalizing exactly once. - */ -export function buildLast30DaysResearchSystemPrompt(sections: readonly string[]): string { - const [overview, keyFindings, sourcesConsulted, citations] = sections; - return [ - CORBITS_VOCABULARY, - "You research a topic over the last 30 days and write one long-form, cited report. The message that started you carries a `topic` and an optional `focus` — that is the whole brief; never invent a topic, and never widen one. If `topic` is missing or empty, skip straight to finalizing a status note that explains what a topic looks like.", - "Work through the following phases yourself, in order, inside this one turn. Nothing you write for your own internal phases is shown to anyone; only your final reply and your one finalize call matter to the reader.", - - `## Phase 1 — Ground -Turn the topic (and focus, if given) into one tailored search query per platform. Each platform surfaces a topic differently, so tailor the query to how people actually post and search there — never reuse the raw topic verbatim on every platform. Honor the ACTION in the topic: if it is about launches/releases/debuts, search for the EVENT ("new AI coding agent launch", "just launched"), not the bare category noun, which pulls generic complaint threads instead of real news. Keep a web query (news-style, naming the event/launch/trend, no year) and a github query (the product/library/org/repo names behind the topic) in mind for phase 2.`, - - `## Phase 2 — Gather (round 1) -Call \`web_search\` and \`github_activity\` (when available), each at most once, with phase 1's tailored queries. A tool call that comes back as an error (missing credential, rate limit, failed request) means that source is not reachable right now — note it plainly and move on. Never fail because one source is unavailable, and never invent results for a source you could not reach. ${LAST_30_DAYS_RESEARCH_PENDING_SOURCES_LIST} have no workbench integration yet: always note them as "not yet connected" rather than a real result set — never fabricate discussion, launches, or activity for them.`, - - `## Phase 3 — Extract entities -Read round 1's raw results and find the 2-5 strongest named entities worth chasing deeper — the actual product launches, company names, and repos that surfaced. Ignore generic or off-topic noise. Write one follow-up query per platform (web, github) that targets those concrete entities, NOT the broad topic again. If a source returned nothing usable, repeat the base topic for that source's follow-up query rather than inventing an entity.`, - - `## Phase 4 — Gather (round 2) -Call \`web_search\` and \`github_activity\` again (when available), each at most once, with phase 3's entity-targeted queries. Same honesty rules as phase 2: never fail on one unreachable source, never fabricate for a not-yet-connected platform, never invent results.`, - - `## Phase 5 — Curate -Turn both rounds' raw results into a tight, honest, deduplicated brief. - -Clean the pool first: drop exact and near-duplicate urls (the same story/repo surfacing in both rounds counts once); drop anything clearly outside the last 30 days when a publish date is given; for GitHub, drop pet projects, forks, template repos, and anything under roughly 50 stars unless clearly central to the topic; drop bare keyword collisions. - -Honor the topic's intent: if it is about launches/releases/debuts, the PRIMARY themes are the actual launches and their coverage — name the specific products/companies that went live in the window. - -Group into 2-5 real themes — HARD CAP 5, never one-per-item. Name themes a human would actually name: the launch, the reaction, the open question. A theme must be backed by a real named launch/event or corroboration across 2+ items — a single low-signal item is never its own theme. - -Pull notable excerpts when the material has them: if an item quotes someone or states a specific, notable claim, you may excerpt it verbatim with attribution and a url. This is optional; web/GitHub results often have none — never invent one. - -Track, for your own use in phase 6: the themes (title, 1-2 sentence summary, the item urls backing it), any excerpts (quote, attribution, url), and every source that was unreachable, not connected, or genuinely returned nothing.`, - - `## Phase 6 — Write -Write ONE markdown document with exactly these four section headings, in order: "${String(overview)}", "${String(keyFindings)}", "${String(sourcesConsulted)}", "${String(citations)}". - -### ${String(overview)} -2-4 sentences telling the real story: the concrete findings and, when present, the sharpest excerpt. - -### ${String(keyFindings)} -One subsection per theme phase 5 supports, strongest first. Name the specific entities (products, companies, repos, numbers), weigh the evidence, and weave in any excerpts with inline links. Every claim here must trace to a citation. Note when a theme's evidence is thin; never pad, never truncate. - -### ${String(sourcesConsulted)} -Name which sources were actually reached, which returned nothing or errored, and which are not yet connected — an honest accounting, never a hidden failure. - -### ${String(citations)} -One entry per citation used above: link plus a one-line source description. - -Grounding: use ONLY what phases 2-5 actually surfaced; invent nothing (no claim, number, quote, citation, or date not backed by a real result). Cite inline as [label](url) using real urls; one link per substantive claim, never a bare URL. If nothing usable surfaced across both rounds, say so plainly at the top of Overview ("no source results to report for this topic") instead of presenting empty or padded sections as if there were real content. - -Style: collective voice ("we"), not "I". Use " - " for asides, not em dashes or en dashes. No filler ("it's worth noting", "in conclusion"); name specifics over "many sources suggest" generalities. Quote exactly when you have a real excerpt; never paraphrase one into a fake quote. The report stands alone: a reader who never saw the raw data understands what happened and can click through to verify.`, - ].join("\n\n"); -} - -/** Every wired source's `github_activity`/`web_search` neighbors that - * have no workbench integration yet — named so the prompt above can - * teach an honest "not yet connected" rather than silently drop them. */ -export const LAST_30_DAYS_RESEARCH_PENDING_SOURCES = [ - "Hacker News", - "Reddit", - "X", - "YouTube", - "Polymarket", -] as const; - -const LAST_30_DAYS_RESEARCH_PENDING_SOURCES_LIST = LAST_30_DAYS_RESEARCH_PENDING_SOURCES.join(", "); diff --git a/workflows/last-30-days-research/test/boundary.test.ts b/workflows/last-30-days-research/test/boundary.test.ts deleted file mode 100644 index 997540e14c..0000000000 --- a/workflows/last-30-days-research/test/boundary.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -// This package is installable data on the native workflow contract: -// its shipped sources import only published platform packages, so the -// source tree plus the package manifest deploys on any Interchange -// instance without a wrapper contract. Tool packages this workflow -// expects at deploy time (@corbits/web-search-tools, @corbits/github-tools) -// are pinned by the deployer, never imported here — see src/index.ts's -// header comment. - -import { readFile, readdir } from "node:fs/promises"; -import path from "node:path"; -import { expect, test } from "bun:test"; - -const ALLOWED_IMPORT_PREFIXES = ["@intx/", "arktype", "./", "../"]; - -async function listFiles(dir: string): Promise { - const entries = await readdir(dir, { withFileTypes: true }); - const files: string[] = []; - for (const entry of entries) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...(await listFiles(full))); - } else { - files.push(full); - } - } - return files; -} - -async function shippedFiles(): Promise { - const packageRoot = path.join(import.meta.dir, ".."); - return [ - ...(await listFiles(path.join(packageRoot, "src"))), - path.join(packageRoot, "package.json"), - ]; -} - -test("shipped sources import only published platform packages", async () => { - const importPattern = /from\s+"([^"]+)"/g; - const violations: string[] = []; - for (const file of await shippedFiles()) { - if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue; - const content = await readFile(file, "utf8"); - for (const match of content.matchAll(importPattern)) { - const specifier = match[1] ?? ""; - const allowed = ALLOWED_IMPORT_PREFIXES.some((prefix) => specifier.startsWith(prefix)); - if (!allowed) { - violations.push(`${path.basename(file)}: ${specifier}`); - } - } - } - expect(violations).toEqual([]); -}); diff --git a/workflows/last-30-days-research/test/definition.test.ts b/workflows/last-30-days-research/test/definition.test.ts deleted file mode 100644 index c78ee0b8d0..0000000000 --- a/workflows/last-30-days-research/test/definition.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -// Tests for this package's own contract: the shape our factory -// commits to, its serialization guarantees, and its boundary. The -// platform's own normalization and validation are its business, not -// re-proven here. - -import { expect, test } from "bun:test"; -import type { StepPrimitive, WorkflowDefinition } from "@intx/workflow"; - -import { - LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME, - LAST_30_DAYS_RESEARCH_PENDING_SOURCES, - LAST_30_DAYS_RESEARCH_SECTIONS, - LAST_30_DAYS_RESEARCH_STEP_ID, - LAST_30_DAYS_RESEARCH_TOOL_PACKAGE_PINS, - LAST_30_DAYS_RESEARCH_WIRED_SOURCES, - LAST_30_DAYS_RESEARCH_WORKFLOW_ID, - buildLast30DaysResearchWorkflow, - serializeLast30DaysResearchWorkflow, -} from "../src/index"; - -const INPUT = { - triggerAddress: "last-30-days-research@example.test", - inferencePreferences: [{ provider: "ollama", model: "qwen-test" }], - turnTimeoutMs: 300000, -} as const; - -function stepPrimitive(definition: WorkflowDefinition, id: string): StepPrimitive { - const primitive = definition.steps[id]; - if (primitive === undefined || primitive.kind !== "step") { - throw new Error(`expected step primitive for "${id}"`); - } - return primitive; -} - -test("the definition is a single-step pipeline — the only shape this repo's routine launcher can run", () => { - const definition = buildLast30DaysResearchWorkflow(INPUT); - expect(definition.stepOrder).toEqual([LAST_30_DAYS_RESEARCH_STEP_ID]); - expect(Object.keys(definition.steps)).toEqual([LAST_30_DAYS_RESEARCH_STEP_ID]); -}); - -test("the one step is mail-triggered and reads the triggering mail as topic/focus via trigger.payload", () => { - const definition = buildLast30DaysResearchWorkflow(INPUT); - expect(definition.triggers).toEqual([{ type: "mail", to: INPUT.triggerAddress }]); -}); - -test("the step carries an explicit per-turn timeout, no inline tools, and the two wired tool-package pins", () => { - const definition = buildLast30DaysResearchWorkflow(INPUT); - const only = stepPrimitive(definition, LAST_30_DAYS_RESEARCH_STEP_ID); - expect(only.timeout).toBe(INPUT.turnTimeoutMs); - expect(only.agent.toolFactories).toEqual([]); - expect(only.agent.capabilities).toEqual([]); - expect(LAST_30_DAYS_RESEARCH_TOOL_PACKAGE_PINS).toEqual([ - { name: "@corbits/web-search-tools", version: "0.0.4" }, - { name: "@corbits/github-tools", version: "0.0.11" }, - ]); - expect(only.agent.toolPackagePins).toEqual(LAST_30_DAYS_RESEARCH_TOOL_PACKAGE_PINS); -}); - -test("the workflow is triggered by mail to the given deployment address", () => { - const definition = buildLast30DaysResearchWorkflow(INPUT); - expect(definition.id).toBe(LAST_30_DAYS_RESEARCH_WORKFLOW_ID); -}); - -test("the step's turn rides the caller's own inference preferences, unmodified", () => { - const definition = buildLast30DaysResearchWorkflow(INPUT); - const only = stepPrimitive(definition, LAST_30_DAYS_RESEARCH_STEP_ID); - expect(only.agent.inference.sources).toEqual([...INPUT.inferencePreferences]); -}); - -test("the system prompt walks all six research phases and names every pending source, honestly", () => { - const definition = buildLast30DaysResearchWorkflow(INPUT); - const prompt = stepPrimitive(definition, LAST_30_DAYS_RESEARCH_STEP_ID).agent.systemPrompt; - expect(LAST_30_DAYS_RESEARCH_WIRED_SOURCES).toEqual(["Web search", "GitHub"]); - expect(prompt).toContain("web_search"); - expect(prompt).toContain("github_activity"); - expect(prompt).toMatch(/phase 1/i); - expect(prompt).toMatch(/phase 6/i); - expect(LAST_30_DAYS_RESEARCH_PENDING_SOURCES).toEqual([ - "Hacker News", - "Reddit", - "X", - "YouTube", - "Polymarket", - ]); - for (const source of LAST_30_DAYS_RESEARCH_PENDING_SOURCES) { - expect(prompt).toContain(source); - } - expect(prompt).toMatch(/not (yet )?connected/i); - expect(prompt).not.toMatch(/bluesky/i); -}); - -test("the prompt names the fixed report section headings, in order", () => { - const definition = buildLast30DaysResearchWorkflow(INPUT); - const prompt = stepPrimitive(definition, LAST_30_DAYS_RESEARCH_STEP_ID).agent.systemPrompt; - expect(LAST_30_DAYS_RESEARCH_SECTIONS).toEqual([ - "Overview", - "Key findings", - "Sources consulted", - "Citations", - ]); - let lastIndex = -1; - for (const heading of LAST_30_DAYS_RESEARCH_SECTIONS) { - const index = prompt.indexOf(heading); - expect(index).toBeGreaterThan(lastIndex); - lastIndex = index; - } -}); - -test("the prompt names the exact approval-gated finalize tool and commits to always finalizing", () => { - const definition = buildLast30DaysResearchWorkflow(INPUT); - const prompt = stepPrimitive(definition, LAST_30_DAYS_RESEARCH_STEP_ID).agent.systemPrompt; - expect(prompt).toContain(LAST_30_DAYS_RESEARCH_FINALIZE_TOOL_NAME); - expect(prompt).toMatch(/never end a run without finalizing/i); - expect(prompt).toContain("exa"); -}); - -test("the prompt commits to a calm terminal reply on denial, not an error", () => { - const definition = buildLast30DaysResearchWorkflow(INPUT); - const prompt = stepPrimitive(definition, LAST_30_DAYS_RESEARCH_STEP_ID).agent.systemPrompt; - expect(prompt).toMatch(/not approved/i); - expect(prompt).toMatch(/never present a denial as an error/i); -}); - -test("the definition survives the workflow-asset JSON round-trip", () => { - const definition = buildLast30DaysResearchWorkflow(INPUT); - const revived: unknown = JSON.parse(serializeLast30DaysResearchWorkflow(definition)); - expect(revived).toEqual(definition); -}); - -test("serialization fails loud on a function-valued field, naming its path", () => { - const poisoned = { - id: LAST_30_DAYS_RESEARCH_WORKFLOW_ID, - triggers: [{ type: "manual" }], - stepOrder: ["write"], - steps: { - write: { - kind: "step", - id: "write", - drainBehavior: "cancel", - agent: { - id: "write", - systemPrompt: "x", - toolFactories: [() => []], - capabilities: [], - inference: { sources: [] }, - }, - }, - }, - } as unknown as WorkflowDefinition; - expect(() => serializeLast30DaysResearchWorkflow(poisoned)).toThrow( - /steps\.write\.agent\.toolFactories\[0\]/, - ); -}); - -test("an empty trigger address is rejected", () => { - expect(() => buildLast30DaysResearchWorkflow({ ...INPUT, triggerAddress: "" })).toThrow( - /triggerAddress/, - ); -}); - -test("a non-positive or fractional turn timeout is rejected", () => { - expect(() => buildLast30DaysResearchWorkflow({ ...INPUT, turnTimeoutMs: 0 })).toThrow( - /turnTimeoutMs/, - ); - expect(() => buildLast30DaysResearchWorkflow({ ...INPUT, turnTimeoutMs: 0.5 })).toThrow( - /turnTimeoutMs/, - ); -}); diff --git a/workflows/last-30-days-research/tsconfig.json b/workflows/last-30-days-research/tsconfig.json deleted file mode 100644 index a8c2b9966d..0000000000 --- a/workflows/last-30-days-research/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src", "test"], - "exclude": [] -} diff --git a/workflows/last-30-days-research/tsconfig.src.json b/workflows/last-30-days-research/tsconfig.src.json deleted file mode 100644 index fc8724305f..0000000000 --- a/workflows/last-30-days-research/tsconfig.src.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } -} diff --git a/workflows/morning-brief/LICENSE b/workflows/morning-brief/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/workflows/morning-brief/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/workflows/morning-brief/README.md b/workflows/morning-brief/README.md deleted file mode 100644 index 7d6b61281b..0000000000 --- a/workflows/morning-brief/README.md +++ /dev/null @@ -1,168 +0,0 @@ -# @corbits/morning-brief-workflow - -A single mail-triggered step that pulls the sender's recent activity -across their connected sources and writes it up as one calm, scannable -daily brief. Ported from the OG gtm-workbench's `heartbeat` workflow -(CL-5993, a child of CL-5987's routines-catalog port) — renamed to -`morning-brief` because this repo's own zero-cost `heartbeat` catalog- -test fixture already owns that name for an unrelated definition. - -## What it does - -One step, one agent, tools arriving as packages at deploy time (never -inlined — see `test/boundary.test.ts`). The agent calls each source's -tool at most once, then writes a markdown reply with exactly three -fixed section headings, in order: - -1. **What happened** -2. **What needs attention today** -3. **Suggested next actions** - -## Sources: wired now, more later by design - -| Source | Status | Tool package | -| -------------------------------- | ------------- | ----------------------------- | -| Granola (recent call notes) | wired | `@corbits/granola-tools` | -| Linear (recently updated issues) | wired | `@corbits/linear-tools` | -| Attio (CRM activity) | not connected | no workbench tool package yet | -| Vercel (deployments) | not connected | no workbench tool package yet | - -The system prompt (`MORNING_BRIEF_SYSTEM_PROMPT`) is the single place -that owns the brief's structure and its degradation copy — the OG's -several bespoke merge/format tools (`heartbeat_merge_brief_sources`, -`heartbeat_format_brief_title`, ...) were workflow-specific, so they -are folded into this definition rather than ported as their own tool -packages. Only genuinely reusable integrations (Granola, Linear) stay -external. - -Every source call degrades gracefully, never fails the run: - -- A tool call that errors (missing credential, failed request) reads - as "not connected" in the brief — the model is instructed to say so - plainly and move on, never to fail the turn or invent activity. -- Attio and Vercel have no tool to call yet, so the prompt names them - as "not connected" directly — an honest line, not a fabricated - section. -- If every source is unavailable, the brief still gets finalized — see - "Finalizing and persistence" below — with a teaching payload instead - of a real brief, rather than presenting empty or padded sections as - if there were real content. - -Adding a source later (Attio, Vercel, or a new one) means: build its -tool package, add it to `MORNING_BRIEF_TOOL_PACKAGE_PINS` in this -definition, and add one line to the prompt's source list — never -restructuring the brief. - -## Finalizing and persistence - -The agent's last act is always one call to `morning_brief_finalize` -(`src/finalize-tool.ts`), gated behind a single human approval -(`approval: "ask"`, the platform's native tool-approval gate — see that -file's header for the full suspend/resume account). On approval, the -call persists the brief as a Library artifact via -`createWorkflowArtifact` (`src/artifact-client.ts`, duplicated from -`@corbits/artifact-tools`' client per this package's "installable data, -`@intx/*` and `arktype` only" import boundary — see -`test/boundary.test.ts`) and returns `{ id, version, title, kind, -persisted: true }`, the shape `packages/chat/src/artifact-delivery.ts` -recognizes and turns into a Library-linked chip in the thread. A failed -persist surfaces as an honest tool error, never a fabricated success. - -This runs on both paths, not just the happy one: - -- **Real brief**: every source that returned something feeds a normal - brief, finalized with a real title and the full markdown body. -- **No-data path**: when every source is not connected or came back - empty, the agent still calls `morning_brief_finalize` — with a - teaching title (e.g. "Morning brief — no connected sources yet") and - content that honestly explains what it would have looked for (recent - Granola call notes, recently updated Linear issues), names the - missing connectors by id (`granola`, `linear`), and says how to - connect them. A run never ends in silence or a bare markdown reply — - it always ends in a persisted, chip-visible artifact. - -**Teaching-artifact kind**: `outcome` (`"brief"` | `"status-note"`) is a -required, structural argument to `morning_brief_finalize` — the model -names which shape it is calling with, but never supplies `kind` -directly, so the tool (not the prompt) decides the persisted artifact's -`kind`: `"text"` for a real brief, `"status-note"` for the no-data -teaching payload. `"status-note"` is the one teaching-artifact kind -shared by every workflow in this catalog, so the Library's kind badge -always reads "Status note" for a no-data run, regardless of which -workflow made it — never the same `"text"` kind a real brief uses. - -## Usage - -```ts -import { - buildMorningBriefWorkflow, - serializeMorningBriefWorkflow, -} from "@corbits/morning-brief-workflow"; - -const definition = buildMorningBriefWorkflow({ - triggerAddress: "morning-brief@tenant.example", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 120_000, -}); - -const json = serializeMorningBriefWorkflow(definition); -``` - -`buildMorningBriefWorkflow` bakes `@corbits/granola-tools` and -`@corbits/linear-tools` into the definition's `toolPackagePins` -(`MORNING_BRIEF_TOOL_PACKAGE_PINS` in `src/index.ts`) — there is no -separate operator step to pin them on the deployment. What still -depends on the deployer is credentials: without a real Granola or -Linear credential for the connecting tenant, each source's tool call -errors and the brief honestly reports that source as not connected -(see each package's README for its credential requirement). - -## Scheduling and delivery - -This package carries no schedule of its own — cadence lives as a native -`ScheduleTrigger` on the frozen definition (`@corbits/workflows` -schedule/cron), ticked by the hub poller. The OG ran daily at 13:00 UTC; -the closest match to its actual cadence (weekday mornings) is a cron -expression, since daily/weekly presets cannot express "every weekday": - -```json -{ - "kind": "cron", - "expression": "0 13 * * 1-5", - "timezone": "America/Los_Angeles" -} -``` - -Delivery — posting the brief into a channel and/or the recipient's -inbox — is the scheduled-fire path's job: a matching `ScheduleTrigger` -tick launches the definition, and the run's reply lands in the -workbench it is deployed against. This workflow does not need its own -persist/notify steps for that — it only needs to reply with one clean -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. `workbench-digest` is not seeded either; both are deployable -through the catalog instantiate route (CL-7073). - -## Deploy from the npm registry - -Once published, deploy this workflow definition to a tenant by name and -version pin: - -``` -POST /api/tenants/:id/workflows/deployments -{ - "source": { "kind": "registry", "registry": "npm" }, - "entry": "./src/index.ts", - "pin": "@corbits/morning-brief-workflow@0.0.1", - "sourceOfferingIds": ["", ...], - "defaultSourceOfferingId": "" -} -``` - -`entry` is the package's `interchange.workflow` module path; `pin` is -`"@corbits/morning-brief-workflow@0.0.1"` or a semver range on the same name. The hub -installs, probes, gates, and freezes the definition from the registry -tarball before creating the deployment. diff --git a/workflows/morning-brief/package.json b/workflows/morning-brief/package.json deleted file mode 100644 index 94cc69f452..0000000000 --- a/workflows/morning-brief/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@corbits/morning-brief-workflow", - "version": "0.0.1", - "description": "Single-step, mail-triggered workflow that writes a daily brief from the sender's recent activity across connected sources", - "license": "LGPL-2.1-or-later", - "repository": { - "type": "git", - "url": "https://github.com/corbitsdev/workbench.git", - "directory": "workflows/morning-brief" - }, - "files": [ - "src", - "LICENSE", - "README.md" - ], - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "arktype": "catalog:" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0" - }, - "corbits": { - "workflow": { - "assetName": "morning-brief", - "displayName": "Morning brief", - "automatable": true - } - }, - "interchange": { - "workflow": "./src/index.ts" - } -} diff --git a/workflows/morning-brief/src/artifact-client.test.ts b/workflows/morning-brief/src/artifact-client.test.ts deleted file mode 100644 index 1c86414419..0000000000 --- a/workflows/morning-brief/src/artifact-client.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { expect, test } from "bun:test"; - -import { createWorkflowArtifact } from "./artifact-client"; - -const CONFIG = { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - runAddress: "run_1@workflow", -}; - -test("posts to the workflow-artifacts endpoint and returns id/version", async () => { - const captured: { url: string; init: RequestInit | undefined } = { - url: "", - init: undefined, - }; - const fetchImpl = (async (input: URL | string, init?: RequestInit) => { - captured.url = String(input); - captured.init = init; - return new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - }); - }) as unknown as typeof fetch; - - const result = await createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ); - - expect(result).toEqual({ id: "art_1", version: 1 }); - expect(captured.url).toBe("https://hub.example.com/api/workflow-artifacts/artifacts"); - const headers = captured.init?.headers as Record; - expect(headers["authorization"]).toBe("Bearer sc-token"); - expect(headers["x-workflow-run-address"]).toBe("run_1@workflow"); -}); - -test("throws on a non-ok HTTP response", async () => { - const fetchImpl = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/500/); -}); - -test("throws when the response doesn't match the expected shape", async () => { - const fetchImpl = (async () => - new Response(JSON.stringify({ data: {} }), { - status: 201, - })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/did not match the expected shape/); -}); diff --git a/workflows/morning-brief/src/artifact-client.ts b/workflows/morning-brief/src/artifact-client.ts deleted file mode 100644 index b9d56ca425..0000000000 --- a/workflows/morning-brief/src/artifact-client.ts +++ /dev/null @@ -1,62 +0,0 @@ -// A minimal client for the sanctioned workflow-artifacts HTTP surface -// (`@corbits/artifacts-hub`'s `createWorkflowArtifactRoutes`, CL-6000). -// Deliberately duplicated from `@corbits/artifact-tools`' `client.ts` -// rather than imported: this package ships as installable data on the -// native workflow contract (see `./index.ts`'s header comment and -// `test/boundary.test.ts`), so its shipped sources import only `@intx/*`, -// `arktype`, and relative specifiers — never another `@corbits/*` -// package. Authenticates with the sidecar's own bearer token plus the -// run's own mailbox address (both already reach a workflow-process -// child's tool env), never a database handle. -import { type } from "arktype"; - -export interface WorkflowArtifactClientConfig { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly runAddress: string; - /** Override for tests; defaults to the global `fetch`. */ - readonly fetchImpl?: typeof fetch; -} - -export type CreateWorkflowArtifactInput = { - readonly title: string; - readonly kind: string; - readonly content: string; -}; - -export type CreatedWorkflowArtifact = { - readonly id: string; - readonly version: number; -}; - -const CreatedWorkflowArtifactResponse = type({ - data: { id: "string", version: "number" }, -}); - -/** Persists one artifact. Throws on any transport, HTTP, or shape failure. */ -export async function createWorkflowArtifact( - config: WorkflowArtifactClientConfig, - input: CreateWorkflowArtifactInput, -): Promise { - const doFetch = config.fetchImpl ?? fetch; - const response = await doFetch(`${config.hubArtifactsUrl}/api/workflow-artifacts/artifacts`, { - method: "POST", - headers: { - authorization: `Bearer ${config.sidecarToken}`, - "x-workflow-run-address": config.runAddress, - "content-type": "application/json", - }, - body: JSON.stringify(input), - }); - if (!response.ok) { - throw new Error(`Workflow artifact create failed: ${response.status} ${response.statusText}`); - } - const body: unknown = await response.json(); - const parsed = CreatedWorkflowArtifactResponse(body); - if (parsed instanceof type.errors) { - throw new Error( - `Workflow artifact create response did not match the expected shape: ${parsed.summary}`, - ); - } - return parsed.data; -} diff --git a/workflows/morning-brief/src/finalize-tool.test.ts b/workflows/morning-brief/src/finalize-tool.test.ts deleted file mode 100644 index 8df92510c4..0000000000 --- a/workflows/morning-brief/src/finalize-tool.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { expect, test } from "bun:test"; - -import { - MORNING_BRIEF_FINALIZE_TOOL, - MORNING_BRIEF_FINALIZE_TOOL_NAME, - buildArtifactPayload, - type WorkflowArtifactEnv, -} from "./finalize-tool"; - -test("the tool's definition marks itself approval-gated", () => { - expect(MORNING_BRIEF_FINALIZE_TOOL.definitions).toEqual([ - { name: MORNING_BRIEF_FINALIZE_TOOL_NAME, approval: "ask" }, - ]); -}); - -test("the tool is namespaced under this workflow package, not a shared one", () => { - expect(MORNING_BRIEF_FINALIZE_TOOL.id).toBe("@corbits/workflow-morning-brief/finalize"); -}); - -test("requires the sanctioned workflow-artifacts env keys", () => { - expect(MORNING_BRIEF_FINALIZE_TOOL.requires).toEqual([ - "hubArtifactsUrl", - "sidecarToken", - "address", - ]); -}); - -test("buildArtifactPayload marks a real brief as a text artifact", () => { - const payload = buildArtifactPayload({ - outcome: "brief", - title: "Morning brief — Tuesday", - content: "## What happened\n\n- Shipped the thing", - }); - expect(payload).toEqual({ - title: "Morning brief — Tuesday", - kind: "text", - content: "## What happened\n\n- Shipped the thing", - }); -}); - -test("buildArtifactPayload marks a no-data teaching payload as status-note, not text", () => { - const payload = buildArtifactPayload({ - outcome: "status-note", - title: "Morning brief — no connected sources yet", - content: "Neither `granola` nor `linear` is connected yet.", - }); - expect(payload).toEqual({ - title: "Morning brief — no connected sources yet", - kind: "status-note", - content: "Neither `granola` nor `linear` is connected yet.", - }); -}); - -function testEnv(): WorkflowArtifactEnv { - return { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - address: "run_1@workflow", - } as unknown as WorkflowArtifactEnv; -} - -test("run persists the artifact on real invocation (i.e. after approval re-dispatches the call) and reports it persisted", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - })) as unknown as typeof fetch; - - try { - const bundle = MORNING_BRIEF_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: MORNING_BRIEF_FINALIZE_TOOL_NAME, - arguments: { - outcome: "brief", - title: "Morning brief — Tuesday", - content: "## What happened\n\n- Shipped the thing", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(String(result.content)) as { - id: string; - version: number; - title: string; - kind: string; - persisted: boolean; - }; - expect(parsed).toEqual({ - id: "art_1", - version: 1, - title: "Morning brief — Tuesday", - kind: "text", - persisted: true, - }); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run persists a teaching payload on the no-data path with a status-note kind, distinct from a real brief", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response(JSON.stringify({ data: { id: "art_2", version: 1 } }), { - status: 201, - })) as unknown as typeof fetch; - - try { - const bundle = MORNING_BRIEF_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_2", - name: MORNING_BRIEF_FINALIZE_TOOL_NAME, - arguments: { - outcome: "status-note", - title: "Morning brief — no connected sources yet", - content: - "This brief would have looked for recent Granola call " + - "notes and recently updated Linear issues. Neither `granola` " + - "nor `linear` is connected yet — connect them in Settings to " + - "get a real brief tomorrow.", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(String(result.content)) as { - persisted: boolean; - kind: string; - }; - expect(parsed.persisted).toBe(true); - expect(parsed.kind).toBe("status-note"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run returns an honest error result when persistence fails, never fabricating persisted: true", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - try { - const bundle = MORNING_BRIEF_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: MORNING_BRIEF_FINALIZE_TOOL_NAME, - arguments: { - outcome: "brief", - title: "Morning brief — Tuesday", - content: "## What happened\n\n- Shipped the thing", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(true); - expect(result.content).toContain("Failed to persist"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run rejects malformed arguments without throwing", async () => { - const bundle = MORNING_BRIEF_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: MORNING_BRIEF_FINALIZE_TOOL_NAME, - arguments: {}, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); - -test("run rejects an outcome value outside the two structural kinds", async () => { - const bundle = MORNING_BRIEF_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: MORNING_BRIEF_FINALIZE_TOOL_NAME, - arguments: { - outcome: "summary", - title: "Morning brief — Tuesday", - content: "## What happened\n\n- Shipped the thing", - }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); diff --git a/workflows/morning-brief/src/finalize-tool.ts b/workflows/morning-brief/src/finalize-tool.ts deleted file mode 100644 index cd0f4fa2d1..0000000000 --- a/workflows/morning-brief/src/finalize-tool.ts +++ /dev/null @@ -1,162 +0,0 @@ -// The one piece of this workflow gated by human approval: finalizing the -// day's brief. Kept inside the workflow package rather than a shared tool -// package — same "workflow-specific logic lives in the definition" -// convention `pain-point-collateral`'s `finalize-tool.ts` established — -// this is not a reusable integration on its own. -// -// Approval mechanics: identical to `pain-point-collateral`'s -// `finalize-tool.ts` — `definitions` marks this tool's one definition -// `approval: "ask"`, the platform's native tool-approval gate. Calling it -// suspends the run and creates a real `approval` row; only executes once -// a human approves it. See that file's header comment for the full -// suspend/resume account, which applies unchanged here. -// -// Persistence: `run` below calls `createWorkflowArtifact` -// (`./artifact-client.ts`, duplicated from `@corbits/artifact-tools`' -// client rather than imported — see that file's header for why this -// installable-data package never imports another `@corbits/*` package) -// against the sanctioned workflow-artifacts HTTP surface — authenticated -// with the sidecar's own bearer token and this run's own mailbox -// address, both already present on `env`, never a database handle. A -// successful call returns the persisted artifact's id/version so the -// delivery pipeline can reference it with a file part -// (`packages/chat/src/artifact-delivery.ts`); a failed call surfaces as -// an honest `isError: true` result rather than a fabricated success. -// -// The finalized content is not always a "real" brief: on the no-data -// path (every source not connected or empty) this workflow's system -// prompt (see `./index.ts`) still calls this same tool with a teaching -// payload — what the brief would have looked for and which connectors -// are missing — so a run always ends in a persisted, chip-visible -// artifact rather than silence. - -import { type } from "arktype"; -import { defineTool, type BaseEnv } from "@intx/agent"; -import { createWorkflowArtifact } from "./artifact-client"; - -export const MORNING_BRIEF_FINALIZE_TOOL_NAME = "morning_brief_finalize"; - -export const MORNING_BRIEF_FINALIZE_DESCRIPTION = - "Finalizes the daily brief, pending human approval, and persists it as a Library artifact."; - -const FinalizeArgs = type({ - /** - * Which of the two shapes this call is: `"brief"` for a real, populated - * brief; `"status-note"` for the no-data path's teaching payload. This - * is what fixes the persisted artifact's `kind` — the model names the - * outcome, but never supplies `kind` directly, so a run can never - * accidentally (or by prompt drift) mislabel a teaching payload as a - * real brief or vice versa. - */ - outcome: "'brief'|'status-note'", - /** Short, human-facing title. Doubles as the inbox/approve-card headline. */ - title: "string > 0", - /** The finished brief's markdown body — a real brief, or an honest teaching payload on the no-data path. */ - content: "string > 0", -}); - -export type FinalizeArgs = typeof FinalizeArgs.infer; - -export type ArtifactPayload = { - title: string; - kind: "text" | "status-note"; - content: string; -}; - -/** - * The payload `createWorkflowArtifact` persists (`{ title, kind, - * content }`). Built here so the persist call is a straight pass-through - * of this object, not a payload assembled at the call site. `kind` comes - * from `args.outcome`, never from free-text the model could drift on. - */ -export function buildArtifactPayload(args: FinalizeArgs): ArtifactPayload { - return { - title: args.title, - kind: args.outcome === "status-note" ? "status-note" : "text", - content: args.content, - }; -} - -/** - * The env this tool needs beyond `BaseEnv`: the sanctioned - * workflow-artifacts credential trio, populated for every workflow step - * (`apps/sidecar/src/workflow-substrate-factory/step-env.ts`) — the same - * three keys `@corbits/artifact-tools`' read-side bundle declares. - */ -export interface WorkflowArtifactEnv extends BaseEnv { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly address: string; -} - -/** - * `defineTool`'s env-DI factory shape. Needs the sanctioned - * workflow-artifacts credential trio beyond `BaseEnv`. - */ -export const MORNING_BRIEF_FINALIZE_TOOL = defineTool({ - id: "@corbits/workflow-morning-brief/finalize", - requires: ["hubArtifactsUrl", "sidecarToken", "address"], - definitions: [ - { - name: MORNING_BRIEF_FINALIZE_TOOL_NAME, - approval: "ask", - }, - ], - factory: (env) => ({ - definitions: [ - { - name: MORNING_BRIEF_FINALIZE_TOOL_NAME, - description: MORNING_BRIEF_FINALIZE_DESCRIPTION, - inputSchema: { - type: "object", - properties: { - outcome: { type: "string", enum: ["brief", "status-note"] }, - title: { type: "string" }, - content: { type: "string" }, - }, - required: ["outcome", "title", "content"], - }, - }, - ], - run: async (call) => { - const parsed = FinalizeArgs(call.arguments); - if (parsed instanceof type.errors) { - return { - callId: call.id, - isError: true, - content: `Invalid arguments for ${MORNING_BRIEF_FINALIZE_TOOL_NAME}: ${parsed.summary}`, - }; - } - const artifact = buildArtifactPayload(parsed); - try { - const created = await createWorkflowArtifact( - { - hubArtifactsUrl: env.hubArtifactsUrl, - sidecarToken: env.sidecarToken, - runAddress: env.address, - }, - artifact, - ); - return { - callId: call.id, - isError: false, - content: JSON.stringify({ - id: created.id, - version: created.version, - title: artifact.title, - kind: artifact.kind, - persisted: true, - }), - }; - } catch (err) { - return { - callId: call.id, - isError: true, - content: `Failed to persist "${artifact.title}" as a Library artifact: ${ - err instanceof Error ? err.message : String(err) - }`, - }; - } - }, - }), -}); diff --git a/workflows/morning-brief/src/index.ts b/workflows/morning-brief/src/index.ts deleted file mode 100644 index c0c0b5f5d3..0000000000 --- a/workflows/morning-brief/src/index.ts +++ /dev/null @@ -1,262 +0,0 @@ -// The morning-brief workflow: a single-step, mail-triggered definition -// whose agent pulls the caller's recent activity across connected -// sources and writes it up as one calm, scannable brief. Ported from -// the OG gtm-workbench's `heartbeat` workflow (CL-5993) — renamed to -// avoid colliding with this repo's own zero-cost `heartbeat` catalog- -// test fixture, which is an unrelated definition. -// -// Structural difference from the OG: the OG split brief assembly -// across several bespoke tools (`heartbeat_merge_brief_sources`, -// `heartbeat_format_brief_title`, `heartbeat_format_brief_document`, -// `heartbeat_format_brief_notify`) that existed only to serve this one -// workflow. Everything that specific to morning-brief — the section -// structure, the per-source degradation copy — is folded directly into -// this definition's system prompt instead: a workflow-owned constant, -// not a workflow-shaped tool package. Only the genuinely reusable -// integrations (Granola, Linear) stay external tool packages -// (`@corbits/granola-tools`, `@corbits/linear-tools`), the same way -// every other tool a workbench agent uses arrives as a package pinned -// at deploy time — never inlined on the definition (see the boundary -// test in `test/boundary.test.ts`). -// -// This package is installable data. It imports only published platform -// packages, and nothing imports it statically: a host publishes the -// serialized definition as a workflow asset and deploys it through the -// platform's deploy machinery; the execution host materializes it at -// runtime from the deploy alone. -// -// Approval mechanics and persistence: see `finalize-tool.ts`'s header -// comment for the exact suspend/resume path and how a finalize call -// persists the brief as a Library artifact. In short — -// `morning_brief_finalize` is declared `approval: "ask"`, the platform's -// native tool-approval gate, so calling it suspends the run, creates a -// real `approval` row (visible in the inbox via `@corbits/approvals`), -// and only executes once a human approves it. This closes the gap the -// OG's plain markdown reply left open: every run now ends in a -// persisted, chip-visible artifact — a real brief, or (on the no-data -// path) an honest teaching payload — never silence. - -import type { AgentDefinition, InferencePreference } from "@intx/agent"; -import { defineWorkflow, step } from "@intx/workflow"; -import type { WorkflowDefinition } from "@intx/workflow"; -import type { ToolPackagePin } from "@intx/types/tool-packages"; -import type { CredentialBinding } from "@intx/types"; - -import { MORNING_BRIEF_FINALIZE_TOOL_NAME } from "./finalize-tool"; - -export const MORNING_BRIEF_WORKFLOW_ID = "wf_morning_brief"; -export const MORNING_BRIEF_STEP_ID = "morning-brief"; - -// Fixed section structure, matching the OG brief's shape. Kept as a -// single named export so a future consumer (e.g. a delivery-card -// renderer that wants to recognise these headings) reads the same -// three strings the prompt commits the model to, rather than -// re-deriving them from prose. -export const MORNING_BRIEF_SECTIONS = [ - "What happened", - "What needs attention today", - "Suggested next actions", -] as const; - -// Sources this deployment can actually reach today, each backed by a -// real, pinnable tool package. Attio and Vercel have no workbench tool -// package yet (CL-5993's survey found neither corbits/Interchange -// integration exists) — they are named in the prompt as honestly -// "not connected" rather than silently omitted, so the brief's shape -// stays stable as sources come online: adding Attio later means giving -// it a tool package and a line here, never restructuring the brief. -export const MORNING_BRIEF_WIRED_SOURCES = ["Granola", "Linear"] as const; -export const MORNING_BRIEF_PENDING_SOURCES = ["Attio", "Vercel"] as const; - -/** - * Tool packages this definition pins (CL-5999). `@intx/agent`'s - * `defineAgent` does not yet accept a `toolPackagePins` field on its - * authoring-time config — it is vendored, read-only source for this - * change — so the agent below is built directly against - * `AgentDefinition`'s own type, which already carries the field. The - * native provisioned deploy path resolves `toolPackagePins` into a - * tool closure at launch time; this is the declaration side of that - * pipeline. - */ -export const MORNING_BRIEF_TOOL_PACKAGE_PINS: readonly ToolPackagePin[] = [ - { name: "@corbits/granola-tools", version: "0.0.5" }, - { name: "@corbits/linear-tools", version: "0.0.5" }, -]; - -/** - * Binds `@corbits/granola-tools`' and `@corbits/linear-tools`' declared - * handles to their tenant-owned credentials (CL-6028); see - * `workflows/granola-call/src/index.ts`'s sibling constant for the full - * rationale. - */ -export const MORNING_BRIEF_CREDENTIAL_BINDINGS: readonly CredentialBinding[] = [ - { - package: "@corbits/granola-tools", - handle: "granola", - provider: "granola", - locator: "tenant", - }, - { - package: "@corbits/linear-tools", - handle: "linear", - provider: "linear", - locator: "tenant", - }, -]; - -export const MORNING_BRIEF_SYSTEM_PROMPT = [ - "You write a short daily brief summarizing the sender's recent " + - "activity, for the sender to read at the start of their day.", - "Use the granola_list_recent_notes and linear_list_recent_issues " + - "tools (when available) to pull what actually happened — recent " + - "Granola call notes and recently updated Linear issues. Call each " + - "at most once per source.", - "A tool call that comes back as an error (missing credential, " + - "failed request) means that source is not connected right now — " + - 'note it plainly (e.g. "Linear: not connected") and move on. ' + - "Never fail the brief because one source is unavailable, and never " + - "invent activity for a source you could not reach.", - `Attio and Vercel have no workbench integration yet: always list ` + - `them as "not connected" rather than a real section — never ` + - `fabricate CRM or deployment activity for them.`, - "Structure the reply as markdown with exactly these three section " + - `headings, in order: "${MORNING_BRIEF_SECTIONS[0]}", ` + - `"${MORNING_BRIEF_SECTIONS[1]}", "${MORNING_BRIEF_SECTIONS[2]}". ` + - "Keep it calm and scannable: short bullet points, no filler, no " + - "restating the tool output verbatim.", - "If every source is not connected, say that plainly at the top " + - '("no connected sources to report from today") instead of ' + - "presenting empty or padded sections as if there were real content.", - `Finalizing: once you have written the brief, call ` + - `\`${MORNING_BRIEF_FINALIZE_TOOL_NAME}\` exactly once with ` + - `outcome "brief", a short title (e.g. "Morning brief — "), and the full markdown brief as content. This call ' + - "requires a human's approval before it completes. Always finalize, " + - "even when every source is not connected: in that case, still call " + - `\`${MORNING_BRIEF_FINALIZE_TOOL_NAME}\` once with outcome ` + - '"status-note", a teaching title (e.g. "Morning brief — no ' + - 'connected sources yet"), and content that honestly explains what ' + - "the brief would have looked for (recent Granola call notes, " + - "recently updated Linear issues), names the missing connectors by " + - "id (`granola`, `linear`), and tells the reader how to connect " + - "them. Never end a run without finalizing — a plain reply with no " + - "artifact is not an acceptable outcome, even on the no-data path.", - "If the finalize call succeeds, present the finalized brief as your " + - "reply exactly as written, with no commentary about the approval " + - "mechanism itself. If the call is denied, reply with one calm, " + - "plain sentence that the brief was not approved and no action was " + - "taken; never present a denial as an error, and never apologize as " + - "if something broke.", -].join("\n\n"); - -/** - * Everything the definition needs that is per-deployment data. The - * trigger address names a specific deployment's inbox; a native - * ScheduleTrigger on the definition launches this workflow on a clock - * and is independent of this field, matching how every other - * workflow package in this catalog is authored. - */ -export interface MorningBriefWorkflowInput { - /** The deployment's mail address; each inbound mail is one run. */ - readonly triggerAddress: string; - /** Provider/model preferences, in order; resolved at deploy time. */ - readonly inferencePreferences: readonly InferencePreference[]; - /** Per-turn timeout in milliseconds, enforced on the single step. */ - readonly turnTimeoutMs: number; -} - -/** - * Builds the morning-brief definition. Exactly one step, matching the - * shape every other definition in this repo commits to: a single - * reasoning-with-tools turn that calls each source tool at most once - * and writes the brief, rather than a multi-step DAG of bespoke - * formatting tools. - * - * The step always sets an explicit `timeout` — the singular `agent:` - * shorthand sets none, and a wedged inference call would then hang a - * run forever. Tools are never inlined on the definition: they arrive - * as packages on the deploy (`@corbits/granola-tools`, - * `@corbits/linear-tools`), keeping the definition pure data. - */ -export function buildMorningBriefWorkflow(input: MorningBriefWorkflowInput): WorkflowDefinition { - if (input.triggerAddress === "") { - throw new Error("buildMorningBriefWorkflow requires a non-empty triggerAddress"); - } - if (!Number.isInteger(input.turnTimeoutMs) || input.turnTimeoutMs <= 0) { - throw new Error("buildMorningBriefWorkflow requires turnTimeoutMs to be a positive integer"); - } - return defineWorkflow({ - id: MORNING_BRIEF_WORKFLOW_ID, - trigger: { type: "mail", to: input.triggerAddress }, - credentialBindings: MORNING_BRIEF_CREDENTIAL_BINDINGS, - steps: { - "morning-brief": step({ - agent: { - id: MORNING_BRIEF_STEP_ID, - description: - "Pulls recent activity across the sender's connected " + - "sources and writes it up as a short daily brief", - systemPrompt: MORNING_BRIEF_SYSTEM_PROMPT, - toolFactories: [], - capabilities: [], - inference: { sources: input.inferencePreferences }, - toolPackagePins: MORNING_BRIEF_TOOL_PACKAGE_PINS, - } satisfies AgentDefinition, - timeout: input.turnTimeoutMs, - }), - }, - }); -} - -/** - * Serializes a definition to the JSON a workflow asset carries. The - * definition must survive the asset round-trip byte-faithfully, so - * anything JSON would silently drop or mangle — functions, undefined, - * symbols, bigints, non-finite numbers, class instances — is a loud - * error naming the offending path instead of a corrupted asset. - */ -export function serializeMorningBriefWorkflow(definition: WorkflowDefinition): string { - assertJsonPortable(definition, "definition"); - return JSON.stringify(definition); -} - -function assertJsonPortable(value: unknown, path: string): void { - if (value === null) return; - switch (typeof value) { - case "string": - case "boolean": - return; - case "number": - if (!Number.isFinite(value)) { - throw new Error(`${path} is a non-finite number; JSON drops it`); - } - return; - case "object": - break; - default: - throw new Error( - `${path} is a ${typeof value}, which does not survive JSON ` + "serialization", - ); - } - if (Array.isArray(value)) { - value.forEach((element, index) => { - assertJsonPortable(element, `${path}[${index}]`); - }); - return; - } - const proto: unknown = Object.getPrototypeOf(value); - if (proto !== Object.prototype && proto !== null) { - throw new Error(`${path} is a non-plain object; JSON would flatten it lossily`); - } - for (const [key, entry] of Object.entries(value)) { - assertJsonPortable(entry, `${path}.${key}`); - } -} - -export { - MORNING_BRIEF_FINALIZE_TOOL, - MORNING_BRIEF_FINALIZE_TOOL_NAME, - MORNING_BRIEF_FINALIZE_DESCRIPTION, - buildArtifactPayload, -} from "./finalize-tool"; -export type { ArtifactPayload, FinalizeArgs } from "./finalize-tool"; diff --git a/workflows/morning-brief/test/boundary.test.ts b/workflows/morning-brief/test/boundary.test.ts deleted file mode 100644 index b1551d330a..0000000000 --- a/workflows/morning-brief/test/boundary.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -// This package is installable data on the native workflow contract: -// its shipped sources import only published platform packages, so the -// source tree plus the package manifest deploys on any Interchange -// instance without a wrapper contract. Tool packages this workflow -// expects at deploy time (@corbits/granola-tools, @corbits/linear-tools) -// are pinned by the deployer, never imported here — see src/index.ts's -// header comment. - -import { readFile, readdir } from "node:fs/promises"; -import path from "node:path"; -import { expect, test } from "bun:test"; - -const ALLOWED_IMPORT_PREFIXES = ["@intx/", "arktype", "./", "../"]; - -async function listFiles(dir: string): Promise { - const entries = await readdir(dir, { withFileTypes: true }); - const files: string[] = []; - for (const entry of entries) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...(await listFiles(full))); - } else { - files.push(full); - } - } - return files; -} - -async function shippedFiles(): Promise { - const packageRoot = path.join(import.meta.dir, ".."); - return [ - ...(await listFiles(path.join(packageRoot, "src"))), - path.join(packageRoot, "package.json"), - ]; -} - -test("shipped sources import only published platform packages", async () => { - const importPattern = /from\s+"([^"]+)"/g; - const violations: string[] = []; - for (const file of await shippedFiles()) { - if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue; - const content = await readFile(file, "utf8"); - for (const match of content.matchAll(importPattern)) { - const specifier = match[1] ?? ""; - const allowed = ALLOWED_IMPORT_PREFIXES.some((prefix) => specifier.startsWith(prefix)); - if (!allowed) { - violations.push(`${path.basename(file)}: ${specifier}`); - } - } - } - expect(violations).toEqual([]); -}); diff --git a/workflows/morning-brief/test/definition.test.ts b/workflows/morning-brief/test/definition.test.ts deleted file mode 100644 index 8d2966a136..0000000000 --- a/workflows/morning-brief/test/definition.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -// Tests for this package's own contract: the shape our factory -// commits to, its serialization guarantees, and its boundary. The -// platform's own normalization and validation are its business, not -// re-proven here. - -import { expect, test } from "bun:test"; -import type { StepPrimitive, WorkflowDefinition } from "@intx/workflow"; - -import { - MORNING_BRIEF_CREDENTIAL_BINDINGS, - MORNING_BRIEF_FINALIZE_TOOL_NAME, - MORNING_BRIEF_PENDING_SOURCES, - MORNING_BRIEF_SECTIONS, - MORNING_BRIEF_STEP_ID, - MORNING_BRIEF_SYSTEM_PROMPT, - MORNING_BRIEF_TOOL_PACKAGE_PINS, - MORNING_BRIEF_WIRED_SOURCES, - MORNING_BRIEF_WORKFLOW_ID, - buildMorningBriefWorkflow, - serializeMorningBriefWorkflow, -} from "../src/index"; - -const INPUT = { - triggerAddress: "morning-brief@example.test", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 120000, -} as const; - -function morningBriefStep(definition: WorkflowDefinition): StepPrimitive { - const primitive = definition.steps[MORNING_BRIEF_STEP_ID]; - if (primitive === undefined || primitive.kind !== "step") { - throw new Error(`definition has no step primitive named ${MORNING_BRIEF_STEP_ID}`); - } - return primitive; -} - -test("the definition has exactly one step", () => { - const definition = buildMorningBriefWorkflow(INPUT); - expect(definition.stepOrder).toEqual([MORNING_BRIEF_STEP_ID]); - expect(Object.keys(definition.steps)).toEqual([MORNING_BRIEF_STEP_ID]); -}); - -test("the step carries an explicit per-turn timeout", () => { - const definition = buildMorningBriefWorkflow(INPUT); - expect(morningBriefStep(definition).timeout).toBe(INPUT.turnTimeoutMs); -}); - -test("the workflow is triggered by mail to the given deployment address", () => { - const definition = buildMorningBriefWorkflow(INPUT); - expect(definition.id).toBe(MORNING_BRIEF_WORKFLOW_ID); - expect(definition.triggers).toEqual([{ type: "mail", to: INPUT.triggerAddress }]); -}); - -test("the agent carries the fixed prompt, the preferences, and inlines no tools", () => { - const agent = morningBriefStep(buildMorningBriefWorkflow(INPUT)).agent; - expect(agent.systemPrompt).toBe(MORNING_BRIEF_SYSTEM_PROMPT); - expect(agent.inference.sources).toEqual([...INPUT.inferencePreferences]); - // Tools arrive as packages on the deploy, never inlined here: an - // inline factory is a function-valued field the asset cannot carry. - expect(agent.toolFactories).toEqual([]); -}); - -test("the agent pins the wired sources' tool packages by name and version", () => { - const agent = morningBriefStep(buildMorningBriefWorkflow(INPUT)).agent; - expect(agent.toolPackagePins).toEqual([...MORNING_BRIEF_TOOL_PACKAGE_PINS]); - expect(MORNING_BRIEF_TOOL_PACKAGE_PINS).toEqual([ - { name: "@corbits/granola-tools", version: "0.0.5" }, - { name: "@corbits/linear-tools", version: "0.0.5" }, - ]); -}); - -test("the prompt names the fixed section headings, in order", () => { - expect(MORNING_BRIEF_SECTIONS).toEqual([ - "What happened", - "What needs attention today", - "Suggested next actions", - ]); - let lastIndex = -1; - for (const heading of MORNING_BRIEF_SECTIONS) { - const index = MORNING_BRIEF_SYSTEM_PROMPT.indexOf(heading); - expect(index).toBeGreaterThan(lastIndex); - lastIndex = index; - } -}); - -test("the prompt instructs honest degradation for every wired and pending source", () => { - for (const source of MORNING_BRIEF_WIRED_SOURCES) { - expect(MORNING_BRIEF_SYSTEM_PROMPT).toContain(source); - } - for (const source of MORNING_BRIEF_PENDING_SOURCES) { - expect(MORNING_BRIEF_SYSTEM_PROMPT).toContain(source); - } - expect(MORNING_BRIEF_SYSTEM_PROMPT).toMatch(/not connected/); - expect(MORNING_BRIEF_SYSTEM_PROMPT).toMatch(/never fabricate/i); -}); - -test("the prompt requires an honest failure state when nothing is connected", () => { - expect(MORNING_BRIEF_SYSTEM_PROMPT).toMatch(/no connected sources to report from today/); -}); - -test("the prompt names the exact approval-gated finalize tool", () => { - expect(MORNING_BRIEF_SYSTEM_PROMPT).toContain(MORNING_BRIEF_FINALIZE_TOOL_NAME); -}); - -test("the prompt commits to always finalizing, even with a teaching payload on the no-data path", () => { - expect(MORNING_BRIEF_SYSTEM_PROMPT).toMatch(/teaching/i); - expect(MORNING_BRIEF_SYSTEM_PROMPT).toMatch(/never end a run without finalizing/i); - expect(MORNING_BRIEF_SYSTEM_PROMPT).toContain("granola"); - expect(MORNING_BRIEF_SYSTEM_PROMPT).toContain("linear"); -}); - -test("the prompt commits to a calm terminal reply on denial, not an error", () => { - expect(MORNING_BRIEF_SYSTEM_PROMPT).toMatch(/not approved/i); - expect(MORNING_BRIEF_SYSTEM_PROMPT).toMatch(/never present a denial as an error/i); -}); - -test("the definition binds @corbits/granola-tools' and @corbits/linear-tools' declared handles to their tenant-owned credentials", () => { - const definition = buildMorningBriefWorkflow(INPUT); - expect(definition.credentialBindings).toEqual([...MORNING_BRIEF_CREDENTIAL_BINDINGS]); - expect(MORNING_BRIEF_CREDENTIAL_BINDINGS).toEqual([ - { - package: "@corbits/granola-tools", - handle: "granola", - provider: "granola", - locator: "tenant", - }, - { - package: "@corbits/linear-tools", - handle: "linear", - provider: "linear", - locator: "tenant", - }, - ]); -}); - -test("the definition survives the workflow-asset JSON round-trip", () => { - const definition = buildMorningBriefWorkflow(INPUT); - const revived: unknown = JSON.parse(serializeMorningBriefWorkflow(definition)); - expect(revived).toEqual(definition); -}); - -test("serialization fails loud on a function-valued field, naming its path", () => { - const poisoned = { - id: MORNING_BRIEF_WORKFLOW_ID, - triggers: [{ type: "manual" }], - stepOrder: [MORNING_BRIEF_STEP_ID], - steps: { - "morning-brief": { - kind: "step", - id: MORNING_BRIEF_STEP_ID, - drainBehavior: "cancel", - agent: { - id: MORNING_BRIEF_STEP_ID, - systemPrompt: MORNING_BRIEF_SYSTEM_PROMPT, - toolFactories: [() => []], - capabilities: [], - inference: { sources: [] }, - }, - }, - }, - } as unknown as WorkflowDefinition; - expect(() => serializeMorningBriefWorkflow(poisoned)).toThrow( - /steps\.morning-brief\.agent\.toolFactories\[0\]/, - ); -}); - -test("an empty trigger address is rejected", () => { - expect(() => buildMorningBriefWorkflow({ ...INPUT, triggerAddress: "" })).toThrow( - /triggerAddress/, - ); -}); - -test("a non-positive or fractional turn timeout is rejected", () => { - expect(() => buildMorningBriefWorkflow({ ...INPUT, turnTimeoutMs: 0 })).toThrow(/turnTimeoutMs/); - expect(() => buildMorningBriefWorkflow({ ...INPUT, turnTimeoutMs: 0.5 })).toThrow( - /turnTimeoutMs/, - ); -}); diff --git a/workflows/morning-brief/test/deploy-wiring.test.ts b/workflows/morning-brief/test/deploy-wiring.test.ts deleted file mode 100644 index ce8625bdc9..0000000000 --- a/workflows/morning-brief/test/deploy-wiring.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Proves this definition's tool-package pins are on the live -// `AgentDefinition` a native provisioned deploy renders from — not a -// folded-run launch body. -import { expect, test } from "bun:test"; - -import { - MORNING_BRIEF_STEP_ID, - MORNING_BRIEF_TOOL_PACKAGE_PINS, - buildMorningBriefWorkflow, -} from "../src/index"; - -const INPUT = { - triggerAddress: "morning-brief@example.test", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 120000, -} as const; - -test("a workflow built from this definition carries its tool-package pins on the agent step", () => { - const definition = buildMorningBriefWorkflow(INPUT); - const step = definition.steps[MORNING_BRIEF_STEP_ID]; - expect(step?.kind).toBe("step"); - expect(step?.kind === "step" ? step.agent.toolPackagePins : undefined).toEqual([ - ...MORNING_BRIEF_TOOL_PACKAGE_PINS, - ]); -}); - -test("the workflow declares exactly one step", () => { - const definition = buildMorningBriefWorkflow(INPUT); - expect(Object.keys(definition.steps)).toEqual([MORNING_BRIEF_STEP_ID]); -}); diff --git a/workflows/morning-brief/tsconfig.json b/workflows/morning-brief/tsconfig.json deleted file mode 100644 index a8c2b9966d..0000000000 --- a/workflows/morning-brief/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src", "test"], - "exclude": [] -} diff --git a/workflows/morning-brief/tsconfig.src.json b/workflows/morning-brief/tsconfig.src.json deleted file mode 100644 index fc8724305f..0000000000 --- a/workflows/morning-brief/tsconfig.src.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } -} diff --git a/workflows/pain-point-collateral/LICENSE b/workflows/pain-point-collateral/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/workflows/pain-point-collateral/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/workflows/pain-point-collateral/README.md b/workflows/pain-point-collateral/README.md deleted file mode 100644 index b15de9f90d..0000000000 --- a/workflows/pain-point-collateral/README.md +++ /dev/null @@ -1,170 +0,0 @@ -# @corbits/pain-point-collateral-workflow - -Turns one sales call transcript into one piece of targeted collateral, -gated behind a single human approval before anything is finalized -(CL-5995, ported from `gtm-workbench`'s `workflows/pain-point-collateral`, -child of CL-5987). - -## What it does - -One step, one agent, matching every other definition in this catalog. -The system prompt commits it to: - -- **Intake**: the trigger names exactly two fields, either a pasted - transcript (`transcript`) or a Granola note id (`noteId`). The model - calls `pain_point_collateral_intake` (`./src/intake-tool.ts`) first, - passing along whichever field it actually found — the tool - arktype-validates both against a strict schema (rejecting anything - malformed or unexpected, never passing raw JSON further into the run) - and reports which one, if either, carried real content. A `noteId` - hit is then fetched via `granola_get_note` (`@corbits/granola-tools` — - the same package, and the same tool bundle, - `@corbits/morning-brief-workflow` pins for its own - `granola_list_recent_notes` call). -- **Extraction**: the customer's real pain points from the transcript — - specific problems, not generic categories. -- **Drafting**: one piece of collateral targeted at the most significant - pain point found. -- **Finalizing**: exactly one call to `pain_point_collateral_finalize` - (`./src/finalize-tool.ts`), gated behind a human approval. -- **Honest failure**: one plain sentence, and stop, when a `noteId` was - given but there is no way to fetch it (no Granola tool available, or - the fetch fails) — never a fabricated transcript or pain points. -- **Teaching artifact on no data**: when intake reports neither field - carried usable content, the run still calls - `pain_point_collateral_finalize` — with a title such as "No transcript - available", the pain point stated honestly as none found, and a body - naming what was checked and the concrete next step (paste a - transcript, or give a Granola note id). This persists a Library entry - and chip instead of ending in a bare, artifact-less reply. -- **Calm denial**: one plain sentence when the collateral is not - approved — never presented as an error. - -**Teaching-artifact kind**: `outcome` (`"collateral"` | `"status-note"`) -is a required, structural argument to `pain_point_collateral_finalize` -— the model names which shape it is calling with, but never supplies -`kind` directly, so the tool (not the prompt) decides the persisted -artifact's `kind`: `"text"` for real collateral, `"status-note"` for the -no-data teaching payload above. `"status-note"` is the one -teaching-artifact kind shared by every workflow in this catalog, so the -Library's kind badge always reads "Status note" for a no-data run, -regardless of which workflow made it — never the same `"text"` kind -real collateral uses. - -## Intake validation - -`pain_point_collateral_intake` (`./src/intake-tool.ts`) is the one -place the trigger's two named fields are actually parsed against a -schema — an arktype object type with `"+": "reject"`, so an unexpected -field or a wrong-typed `transcript`/`noteId` comes back as an honest -`isError: true` result rather than being silently coerced or passed -through. It carries no `approval` declaration: unlike -`pain_point_collateral_finalize`, it has no external side effect, only -normalizes which of the two fields (if either) actually carried -content. Like the finalize tool, it is a workflow-local export, not a -`toolPackagePins` entry — see "Current limits" below for what that -means for deploys today. - -## Approval mechanics - -`pain_point_collateral_finalize` is declared `approval: "ask"` -(`@intx/agent`'s `ToolDeclaration`), the platform's native tool-approval -gate — not a workflow-DSL `awaitSignal`, and not anything invented for -this port. Calling it suspends the run: the sidecar co-writes a real -`approval` row (`vendor/intx/db/src/schema/approvals.ts`), visible in -the native pending-approvals list the web client reads, and the run parks -until a human approves or rejects it -(`@intx/inference/src/reactor.ts`). On approval the parked call -re-dispatches and the tool actually runs, exactly once. On rejection the -tool never runs at all — the model sees a synthetic "denied by approver" -error, which the system prompt turns into the calm terminal reply above. - -**Inbox headline**: `@corbits/approvals`' `headlineFor` was extended -(this port's one small change to a shared package, not workflow-specific -plumbing) to prefer a tool's `description` over its bare name, and to -fold in the live call's `title` argument when present — so this -approval reads as "Finalizes one piece of pain-point sales collateral…: -'Faster onboarding for Acme Corp'" in the inbox, not the raw tool name. -Any future approval-gated tool that names its own `title` argument gets -the same richer headline for free. - -**Chat approve block**: rendering the approval as an in-channel approve -block (`packages/chat/src/blocks.ts`'s `ApproveBlockData`) is not wired -today for ANY workflow in this repo — a suspended run does not yet post -a chat message announcing itself; only a completed reply turn does -(`packages/chat/src/chat-orchestrator.ts`). Building that wiring is a -platform-level change (a new listener on the `reactor.gate.blocked` -event already flowing through `SidecarEventEmitter`'s `agent.event` -stream) that touches shared chat delivery, not this one workflow — out -of scope for this port. Today, the human approves from the inbox; the -approval is real and works end-to-end there. - -## Current limits (read before deploying) - -One real gap stands between this definition and a fully live deploy, -platform-level, not specific to this workflow: - -- **Tool-package pin resolves only once published** (CL-5999 closed the - pinning gap; publishing is still an operator step). This definition - pins `@corbits/granola-tools` (`PAIN_POINT_COLLATERAL_TOOL_PACKAGE_PINS`) - for `granola_get_note`; the pin resolves at deploy time once an - operator publishes the package to a registry the host's - `toolPackageRegistries` config reaches — npmjs, or a `package-registry` - asset for the `@corbits` scope (see `apps/hub/src/index.ts`'s - `CORBITS_TOOLS_REGISTRY` wiring). The finalize tool itself persists - for real (CL-6000): it writes through the workflow-artifacts HTTP - surface and the finished collateral lands in the Library with a - file-part chip in the channel. - -**Library persistence is real (CL-6000).** `finalize-tool.ts`'s `run` -persists the finalized collateral via `./artifact-client.ts` against the -sanctioned workflow-artifacts HTTP surface -(`@corbits/artifacts-hub`'s `createWorkflowArtifactRoutes`), authenticated -with the sidecar's own bearer token and this run's own mailbox address — -never a database handle. On success it returns the artifact's id/version; -the delivery pipeline (`packages/chat/src/artifact-delivery.ts`) turns -that into a file-part chip on the reply that finalized it. A failed -persist surfaces as an honest tool error, never a fabricated Library row. - -## Usage - -```ts -import { - buildPainPointCollateralWorkflow, - serializePainPointCollateralWorkflow, -} from "@corbits/pain-point-collateral-workflow"; - -const definition = buildPainPointCollateralWorkflow({ - triggerAddress: "pain-point-collateral@tenant.example", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 300000, -}); - -const json = serializePainPointCollateralWorkflow(definition); -``` - -See [`workflows/README.md`](../README.md#status-note) for what -registration/automatable/seeded mean — this one is `automatable: false` -for the same reason as `collateral-generation`: its approval gate is a -poor fit for unattended scheduling. - -## Deploy from the npm registry - -Once published, deploy this workflow definition to a tenant by name and -version pin: - -``` -POST /api/tenants/:id/workflows/deployments -{ - "source": { "kind": "registry", "registry": "npm" }, - "entry": "./src/index.ts", - "pin": "@corbits/pain-point-collateral-workflow@0.0.1", - "sourceOfferingIds": ["", ...], - "defaultSourceOfferingId": "" -} -``` - -`entry` is the package's `interchange.workflow` module path; `pin` is -`"@corbits/pain-point-collateral-workflow@0.0.1"` or a semver range on the same name. The hub -installs, probes, gates, and freezes the definition from the registry -tarball before creating the deployment. diff --git a/workflows/pain-point-collateral/package.json b/workflows/pain-point-collateral/package.json deleted file mode 100644 index 18ad961b3b..0000000000 --- a/workflows/pain-point-collateral/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@corbits/pain-point-collateral-workflow", - "version": "0.0.1", - "description": "Turns one sales call transcript into one piece of targeted collateral, gated behind a single human approval before anything is finalized", - "license": "LGPL-2.1-or-later", - "repository": { - "type": "git", - "url": "https://github.com/corbitsdev/workbench.git", - "directory": "workflows/pain-point-collateral" - }, - "files": [ - "src", - "LICENSE", - "README.md" - ], - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "arktype": "catalog:" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0" - }, - "corbits": { - "workflow": { - "assetName": "pain-point-collateral", - "displayName": "Pain-point collateral", - "automatable": false - } - }, - "interchange": { - "workflow": "./src/index.ts" - } -} diff --git a/workflows/pain-point-collateral/src/artifact-client.test.ts b/workflows/pain-point-collateral/src/artifact-client.test.ts deleted file mode 100644 index 1c86414419..0000000000 --- a/workflows/pain-point-collateral/src/artifact-client.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { expect, test } from "bun:test"; - -import { createWorkflowArtifact } from "./artifact-client"; - -const CONFIG = { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - runAddress: "run_1@workflow", -}; - -test("posts to the workflow-artifacts endpoint and returns id/version", async () => { - const captured: { url: string; init: RequestInit | undefined } = { - url: "", - init: undefined, - }; - const fetchImpl = (async (input: URL | string, init?: RequestInit) => { - captured.url = String(input); - captured.init = init; - return new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - }); - }) as unknown as typeof fetch; - - const result = await createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ); - - expect(result).toEqual({ id: "art_1", version: 1 }); - expect(captured.url).toBe("https://hub.example.com/api/workflow-artifacts/artifacts"); - const headers = captured.init?.headers as Record; - expect(headers["authorization"]).toBe("Bearer sc-token"); - expect(headers["x-workflow-run-address"]).toBe("run_1@workflow"); -}); - -test("throws on a non-ok HTTP response", async () => { - const fetchImpl = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/500/); -}); - -test("throws when the response doesn't match the expected shape", async () => { - const fetchImpl = (async () => - new Response(JSON.stringify({ data: {} }), { - status: 201, - })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/did not match the expected shape/); -}); diff --git a/workflows/pain-point-collateral/src/artifact-client.ts b/workflows/pain-point-collateral/src/artifact-client.ts deleted file mode 100644 index b9d56ca425..0000000000 --- a/workflows/pain-point-collateral/src/artifact-client.ts +++ /dev/null @@ -1,62 +0,0 @@ -// A minimal client for the sanctioned workflow-artifacts HTTP surface -// (`@corbits/artifacts-hub`'s `createWorkflowArtifactRoutes`, CL-6000). -// Deliberately duplicated from `@corbits/artifact-tools`' `client.ts` -// rather than imported: this package ships as installable data on the -// native workflow contract (see `./index.ts`'s header comment and -// `test/boundary.test.ts`), so its shipped sources import only `@intx/*`, -// `arktype`, and relative specifiers — never another `@corbits/*` -// package. Authenticates with the sidecar's own bearer token plus the -// run's own mailbox address (both already reach a workflow-process -// child's tool env), never a database handle. -import { type } from "arktype"; - -export interface WorkflowArtifactClientConfig { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly runAddress: string; - /** Override for tests; defaults to the global `fetch`. */ - readonly fetchImpl?: typeof fetch; -} - -export type CreateWorkflowArtifactInput = { - readonly title: string; - readonly kind: string; - readonly content: string; -}; - -export type CreatedWorkflowArtifact = { - readonly id: string; - readonly version: number; -}; - -const CreatedWorkflowArtifactResponse = type({ - data: { id: "string", version: "number" }, -}); - -/** Persists one artifact. Throws on any transport, HTTP, or shape failure. */ -export async function createWorkflowArtifact( - config: WorkflowArtifactClientConfig, - input: CreateWorkflowArtifactInput, -): Promise { - const doFetch = config.fetchImpl ?? fetch; - const response = await doFetch(`${config.hubArtifactsUrl}/api/workflow-artifacts/artifacts`, { - method: "POST", - headers: { - authorization: `Bearer ${config.sidecarToken}`, - "x-workflow-run-address": config.runAddress, - "content-type": "application/json", - }, - body: JSON.stringify(input), - }); - if (!response.ok) { - throw new Error(`Workflow artifact create failed: ${response.status} ${response.statusText}`); - } - const body: unknown = await response.json(); - const parsed = CreatedWorkflowArtifactResponse(body); - if (parsed instanceof type.errors) { - throw new Error( - `Workflow artifact create response did not match the expected shape: ${parsed.summary}`, - ); - } - return parsed.data; -} diff --git a/workflows/pain-point-collateral/src/finalize-tool.test.ts b/workflows/pain-point-collateral/src/finalize-tool.test.ts deleted file mode 100644 index 3e0d938be4..0000000000 --- a/workflows/pain-point-collateral/src/finalize-tool.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { expect, test } from "bun:test"; - -import { - PAIN_POINT_COLLATERAL_FINALIZE_TOOL, - PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME, - buildArtifactPayload, - type WorkflowArtifactEnv, -} from "./finalize-tool"; - -test("the tool's definition marks itself approval-gated", () => { - expect(PAIN_POINT_COLLATERAL_FINALIZE_TOOL.definitions).toEqual([ - { name: PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME, approval: "ask" }, - ]); -}); - -test("the tool is namespaced under this workflow package, not a shared one", () => { - expect(PAIN_POINT_COLLATERAL_FINALIZE_TOOL.id).toBe( - "@corbits/workflow-pain-point-collateral/finalize", - ); -}); - -test("requires the sanctioned workflow-artifacts env keys", () => { - expect(PAIN_POINT_COLLATERAL_FINALIZE_TOOL.requires).toEqual([ - "hubArtifactsUrl", - "sidecarToken", - "address", - ]); -}); - -test("buildArtifactPayload marks real collateral as a text artifact and folds the targeted pain point into the body", () => { - const payload = buildArtifactPayload({ - outcome: "collateral", - title: "Faster onboarding for Acme Corp", - painPoint: "Onboarding takes six weeks", - content: "Our platform cuts onboarding to two weeks by...", - }); - expect(payload).toEqual({ - title: "Faster onboarding for Acme Corp", - kind: "text", - content: - "Targets: Onboarding takes six weeks\n\nOur platform cuts onboarding to two weeks by...", - }); -}); - -test("buildArtifactPayload marks a no-data teaching payload as status-note, not text", () => { - const payload = buildArtifactPayload({ - outcome: "status-note", - title: "No transcript available", - painPoint: "none found", - content: "Neither the transcript nor noteId field carried content.", - }); - expect(payload).toEqual({ - title: "No transcript available", - kind: "status-note", - content: "Targets: none found\n\nNeither the transcript nor noteId field carried content.", - }); -}); - -function testEnv(): WorkflowArtifactEnv { - return { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - address: "run_1@workflow", - } as unknown as WorkflowArtifactEnv; -} - -test("run persists the artifact on real invocation (i.e. after approval re-dispatches the call) and reports it persisted", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - })) as unknown as typeof fetch; - - try { - const bundle = PAIN_POINT_COLLATERAL_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME, - arguments: { - outcome: "collateral", - title: "Faster onboarding for Acme Corp", - painPoint: "Onboarding takes six weeks", - content: "Our platform cuts onboarding to two weeks by...", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(String(result.content)) as { - id: string; - version: number; - title: string; - kind: string; - persisted: boolean; - }; - expect(parsed).toEqual({ - id: "art_1", - version: 1, - title: "Faster onboarding for Acme Corp", - kind: "text", - persisted: true, - }); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run persists a teaching payload on the no-data path with a status-note kind, distinct from real collateral", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response(JSON.stringify({ data: { id: "art_2", version: 1 } }), { - status: 201, - })) as unknown as typeof fetch; - - try { - const bundle = PAIN_POINT_COLLATERAL_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_2", - name: PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME, - arguments: { - outcome: "status-note", - title: "No transcript available", - painPoint: "none found", - content: "Neither the transcript nor noteId field carried content.", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(String(result.content)) as { - persisted: boolean; - kind: string; - }; - expect(parsed.persisted).toBe(true); - expect(parsed.kind).toBe("status-note"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run returns an honest error result when persistence fails, never fabricating persisted: true", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - try { - const bundle = PAIN_POINT_COLLATERAL_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME, - arguments: { - outcome: "collateral", - title: "Faster onboarding for Acme Corp", - painPoint: "Onboarding takes six weeks", - content: "Our platform cuts onboarding to two weeks by...", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(true); - expect(result.content).toContain("Failed to persist"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run rejects malformed arguments without throwing", async () => { - const bundle = PAIN_POINT_COLLATERAL_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME, - arguments: {}, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); - -test("run rejects an outcome value outside the two structural kinds", async () => { - const bundle = PAIN_POINT_COLLATERAL_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME, - arguments: { - outcome: "draft", - title: "Faster onboarding for Acme Corp", - painPoint: "Onboarding takes six weeks", - content: "Our platform cuts onboarding to two weeks by...", - }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); diff --git a/workflows/pain-point-collateral/src/finalize-tool.ts b/workflows/pain-point-collateral/src/finalize-tool.ts deleted file mode 100644 index c1060ca2d3..0000000000 --- a/workflows/pain-point-collateral/src/finalize-tool.ts +++ /dev/null @@ -1,170 +0,0 @@ -// The one piece of this workflow gated by human approval: finalizing a -// drafted collateral piece. Kept inside the workflow package rather than -// 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 -// @corbits/workflows's "workflow-specific logic lives in the definition" -// convention (only genuinely reusable integrations, like -// `@corbits/granola-tools`, live outside it). -// -// Approval mechanics: `defineTool`'s `definitions` array marks this -// tool's one definition `approval: "ask"` (see -// `@intx/agent/src/tool.ts`'s `ToolDeclaration`/`toolApprovalEffect`), -// the platform's native gate. When the model calls this tool, the -// authz extension floors the call's grant at `"ask"` and suspends the -// run instead of invoking `run` below — the sidecar co-writes an -// `approval` row (visible in the inbox via `@corbits/approvals`) and the -// run parks until a human approves or rejects it -// (`@intx/inference/src/reactor.ts`'s `resolveApprovalDecision`). -// On approval the parked call is re-dispatched and `run` below executes -// for real, exactly once. On rejection `run` never executes at all — the -// model instead sees a synthetic `isError: true` result ("denied by -// approver"), which this workflow's system prompt (see `./index.ts`'s -// `PAIN_POINT_COLLATERAL_SYSTEM_PROMPT`) turns into a calm, plain -// terminal reply rather than an error. -// -// Persistence (CL-6000): `run` below calls `createWorkflowArtifact` -// (`./artifact-client.ts`, duplicated from `@corbits/artifact-tools`' -// client rather than imported — see that file's header for why this -// installable-data package never imports another `@corbits/*` package) -// against the sanctioned workflow-artifacts HTTP surface — authenticated -// with the sidecar's own bearer token and this run's own mailbox -// address, both already present on `env`, never a database handle. A -// successful call returns the persisted artifact's id/version so the -// delivery pipeline can reference it with a file part -// (`packages/chat/src/artifact-delivery.ts`); a failed call surfaces as -// an honest `isError: true` result rather than a fabricated success. - -import { type } from "arktype"; -import { defineTool, type BaseEnv } from "@intx/agent"; -import { createWorkflowArtifact } from "./artifact-client"; - -export const PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME = "pain_point_collateral_finalize"; - -export const PAIN_POINT_COLLATERAL_FINALIZE_DESCRIPTION = - "Finalizes one piece of pain-point sales collateral, pending human approval, and persists it as a Library artifact."; - -const FinalizeArgs = type({ - /** - * Which of the two shapes this call is: `"collateral"` for a real, - * drafted piece; `"status-note"` for the no-data path's teaching - * payload. This is what fixes the persisted artifact's `kind` — the - * model names the outcome, but never supplies `kind` directly, so a - * run can never accidentally (or by prompt drift) mislabel a teaching - * payload as real collateral or vice versa. - */ - outcome: "'collateral'|'status-note'", - /** Short, human-facing title. Doubles as the inbox/approve-card headline. */ - title: "string > 0", - /** The customer pain point this piece targets, verbatim from the transcript. */ - painPoint: "string > 0", - /** The drafted collateral body. */ - content: "string > 0", -}); - -export type FinalizeArgs = typeof FinalizeArgs.infer; - -export type ArtifactPayload = { - title: string; - kind: "text" | "status-note"; - content: string; -}; - -/** - * The payload `createWorkflowArtifact` persists (`{ title, kind, - * content }`). Built here so the persist call is a straight pass-through - * of this object, not a payload assembled at the call site. `kind` comes - * from `args.outcome`, never from free-text the model could drift on. - */ -export function buildArtifactPayload(args: FinalizeArgs): ArtifactPayload { - return { - title: args.title, - kind: args.outcome === "status-note" ? "status-note" : "text", - content: `Targets: ${args.painPoint}\n\n${args.content}`, - }; -} - -/** - * The env this tool needs beyond `BaseEnv`: the sanctioned - * workflow-artifacts credential trio, populated for every workflow step - * (`apps/sidecar/src/workflow-substrate-factory/step-env.ts`) — the same - * three keys `@corbits/artifact-tools`' read-side bundle declares. - */ -export interface WorkflowArtifactEnv extends BaseEnv { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly address: string; -} - -/** - * `defineTool`'s env-DI factory shape. Needs the sanctioned - * workflow-artifacts credential trio beyond `BaseEnv`. - */ -export const PAIN_POINT_COLLATERAL_FINALIZE_TOOL = defineTool({ - id: "@corbits/workflow-pain-point-collateral/finalize", - requires: ["hubArtifactsUrl", "sidecarToken", "address"], - definitions: [ - { - name: PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME, - approval: "ask", - }, - ], - factory: (env) => ({ - definitions: [ - { - name: PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME, - description: PAIN_POINT_COLLATERAL_FINALIZE_DESCRIPTION, - inputSchema: { - type: "object", - properties: { - outcome: { type: "string", enum: ["collateral", "status-note"] }, - title: { type: "string" }, - painPoint: { type: "string" }, - content: { type: "string" }, - }, - required: ["outcome", "title", "painPoint", "content"], - }, - }, - ], - run: async (call) => { - const parsed = FinalizeArgs(call.arguments); - if (parsed instanceof type.errors) { - return { - callId: call.id, - isError: true, - content: `Invalid arguments for ${PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME}: ${parsed.summary}`, - }; - } - const artifact = buildArtifactPayload(parsed); - try { - const created = await createWorkflowArtifact( - { - hubArtifactsUrl: env.hubArtifactsUrl, - sidecarToken: env.sidecarToken, - runAddress: env.address, - }, - artifact, - ); - return { - callId: call.id, - isError: false, - content: JSON.stringify({ - id: created.id, - version: created.version, - title: artifact.title, - kind: artifact.kind, - persisted: true, - }), - }; - } catch (err) { - return { - callId: call.id, - isError: true, - content: `Failed to persist "${artifact.title}" as a Library artifact: ${ - err instanceof Error ? err.message : String(err) - }`, - }; - } - }, - }), -}); diff --git a/workflows/pain-point-collateral/src/index.ts b/workflows/pain-point-collateral/src/index.ts deleted file mode 100644 index 313e07e512..0000000000 --- a/workflows/pain-point-collateral/src/index.ts +++ /dev/null @@ -1,238 +0,0 @@ -// The pain-point-collateral workflow (CL-5995, ported from -// `gtm-workbench`'s `workflows/pain-point-collateral`): turns one sales -// call transcript into one piece of targeted collateral, gated behind a -// single human approval before anything is finalized. -// -// One step, one agent — matching the shape every other definition in -// this catalog commits to (`step`/`defineAgent`, mail-triggered, tools -// arrive as packages on the deploy, never inlined here). All -// pipeline-specific logic — how intake resolves a transcript, what -// counts as extracted pain points, how a draft gets finalized, the -// exact wording of an honest "no transcript"/"not approved" reply — -// lives in this definition's own system prompt and its one -// workflow-local tool (`./finalize-tool.ts`), not a separate tool -// package: none of it is a reusable integration on its own. The one -// genuinely reusable piece, fetching a Granola note by id, is -// `granola_get_note` from `@corbits/granola-tools` — the same package -// the morning-brief workflow (CL-5993) pins for its own -// `granola_list_recent_notes` call; both tools share that one bundle -// rather than forking a second Granola client. -// -// Approval mechanics: see `finalize-tool.ts`'s header comment for the -// exact suspend/resume path. In short — `pain_point_collateral_finalize` -// is declared `approval: "ask"`, the platform's native tool-approval -// gate, so calling it suspends the run, creates a real `approval` row -// (visible in the inbox via `@corbits/approvals`), and only executes -// once a human approves it. -// -// Tool-package pins (CL-5999): `@intx/agent`'s `defineAgent` still does -// not accept a `toolPackagePins` field on its authoring-time config — -// it is vendored, read-only source for this change — so the agent -// below is built directly against `AgentDefinition`'s own type, which -// already carries the field. `@corbits/granola-tools` is pinned below -// for `granola_get_note`; whether the pin *resolves* at deploy time -// still depends on an operator publishing it to a registry the host's -// tool-package resolver can reach (see `apps/hub/src/index.ts`'s -// `toolPackageRegistries` wiring). See `finalize-tool.ts`'s header for -// the separate, still-open gap around this workflow's local finalize -// tool. Until a deploy actually resolves the pin, this definition's -// system prompt still commits it to saying plainly when it has no way -// to reach Granola or finalize a piece, rather than inventing a -// transcript, pain points, or an approval that never happened. -// -// This package is installable data. It imports only published platform -// packages, and nothing imports it statically: a host publishes the -// serialized definition as a workflow asset and deploys it through the -// platform's deploy machinery; the execution host materializes it at -// runtime from the deploy alone. - -import type { AgentDefinition, InferencePreference } from "@intx/agent"; -import { defineWorkflow, step } from "@intx/workflow"; -import type { WorkflowDefinition } from "@intx/workflow"; -import type { ToolPackagePin } from "@intx/types/tool-packages"; -import type { CredentialBinding } from "@intx/types"; - -import { PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME } from "./finalize-tool"; -import { PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME } from "./intake-tool"; - -export const PAIN_POINT_COLLATERAL_WORKFLOW_ID = "wf_pain_point_collateral"; -export const PAIN_POINT_COLLATERAL_STEP_ID = "pain-point-collateral"; - -export const PAIN_POINT_COLLATERAL_SYSTEM_PROMPT = - "You turn one sales call transcript into one piece of collateral " + - "targeted at a customer's real pain points, with a mandatory human " + - "approval step before anything is finalized.\n\n" + - "Intake: the trigger carries exactly two named fields — a pasted " + - "transcript (`transcript`) or a Granola note id (`noteId`). Call " + - `\`${PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME}\` first, passing along ` + - "whichever of `transcript`/`noteId` you actually found in the " + - "trigger, verbatim (omit a field you did not receive — never invent " + - "one). Its result names which field, if either, carried real intake " + - 'content: `"transcript"`, `"noteId"`, or `"none"`. If it reports ' + - '`"transcript"`, use that text verbatim. If it reports `"noteId"`, ' + - "fetch that note's transcript with `granola_get_note` (the `granola` " + - "connector). If you have no way to fetch it — no Granola tool " + - "available, or the fetch fails — say so plainly in one sentence and " + - 'stop. If it reports `"none"`, or the transcript you end up with is ' + - `still empty, call \`${PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME}\` ` + - 'with outcome "status-note" and a teaching payload instead of ' + - 'stopping silently: a title such as "No transcript available", the ' + - "pain point stated honestly as none found, and a body naming what " + - "was checked (both the `transcript` and `noteId` trigger fields), " + - "that neither carried usable content, and the concrete next step — " + - "reply with a pasted transcript, or with a Granola call's note id. " + - "Never invent a transcript, pain points, or collateral content.\n\n" + - "Extraction: from the transcript, identify the customer's real pain " + - "points — specific problems they described, not generic categories.\n\n" + - "Drafting: draft one piece of collateral that speaks directly to the " + - "most significant pain point you found.\n\n" + - `Finalizing: call \`${PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME}\` ` + - 'exactly once, with outcome "collateral", a short title, the exact ' + - "pain point you are targeting, and the drafted collateral body. " + - "This call requires a human's approval before it completes.\n\n" + - "If the call succeeds, present the finalized collateral as your " + - "reply — the title, then the body, clearly formatted — with no " + - "commentary about the approval mechanism itself. If the call is " + - "denied, reply with one calm, plain sentence that the collateral was " + - "not approved and no action was taken; never present a denial as an " + - "error, and never apologize as if something broke."; - -/** Tool packages this definition pins (CL-5999); see the header comment. */ -export const PAIN_POINT_COLLATERAL_TOOL_PACKAGE_PINS: readonly ToolPackagePin[] = [ - { name: "@corbits/granola-tools", version: "0.0.5" }, -]; - -/** - * Binds `@corbits/granola-tools`' declared "granola" handle to a - * tenant-owned Granola credential (CL-6028); see - * `workflows/granola-call/src/index.ts`'s sibling constant for the full - * rationale. - */ -export const PAIN_POINT_COLLATERAL_CREDENTIAL_BINDINGS: readonly CredentialBinding[] = [ - { - package: "@corbits/granola-tools", - handle: "granola", - provider: "granola", - locator: "tenant", - }, -]; - -/** - * Everything the definition needs that is per-deployment data. The - * trigger address names a specific deployment's inbox — for this - * workflow, the address a human messages to start a run — so a - * definition built here is per-deployment by construction. - */ -export interface PainPointCollateralWorkflowInput { - /** The deployment's mail address; each inbound mail is one run. */ - readonly triggerAddress: string; - /** Provider/model preferences, in order; resolved at deploy time. */ - readonly inferencePreferences: readonly InferencePreference[]; - /** Per-turn timeout in milliseconds, enforced on the single step. */ - readonly turnTimeoutMs: number; -} - -/** - * Builds the pain-point-collateral definition. Exactly one step, - * matching the shape every other definition in this repo commits to. - * - * The step always sets an explicit `timeout` — the singular `agent:` - * shorthand sets none, and a wedged inference call (or one parked on an - * unresolved approval past a sane bound) would otherwise hang a run - * forever. Tools are never inlined on the definition: they arrive as - * packages on the deploy, keeping the definition pure data. - */ -export function buildPainPointCollateralWorkflow( - input: PainPointCollateralWorkflowInput, -): WorkflowDefinition { - if (input.triggerAddress === "") { - throw new Error("buildPainPointCollateralWorkflow requires a non-empty triggerAddress"); - } - if (!Number.isInteger(input.turnTimeoutMs) || input.turnTimeoutMs <= 0) { - throw new Error( - "buildPainPointCollateralWorkflow requires turnTimeoutMs to be a positive integer", - ); - } - return defineWorkflow({ - id: PAIN_POINT_COLLATERAL_WORKFLOW_ID, - trigger: { type: "mail", to: input.triggerAddress }, - credentialBindings: PAIN_POINT_COLLATERAL_CREDENTIAL_BINDINGS, - steps: { - [PAIN_POINT_COLLATERAL_STEP_ID]: step({ - agent: { - id: PAIN_POINT_COLLATERAL_STEP_ID, - description: - "Drafts sales collateral targeted at a call transcript's " + - "pain points and finalizes it only once a human approves", - systemPrompt: PAIN_POINT_COLLATERAL_SYSTEM_PROMPT, - toolFactories: [], - capabilities: [], - inference: { sources: input.inferencePreferences }, - toolPackagePins: PAIN_POINT_COLLATERAL_TOOL_PACKAGE_PINS, - } satisfies AgentDefinition, - timeout: input.turnTimeoutMs, - }), - }, - }); -} - -/** - * Serializes a definition to the JSON a workflow asset carries. The - * definition must survive the asset round-trip byte-faithfully, so - * anything JSON would silently drop or mangle — functions, undefined, - * symbols, bigints, non-finite numbers, class instances — is a loud - * error naming the offending path instead of a corrupted asset. - */ -export function serializePainPointCollateralWorkflow(definition: WorkflowDefinition): string { - assertJsonPortable(definition, "definition"); - return JSON.stringify(definition); -} - -function assertJsonPortable(value: unknown, path: string): void { - if (value === null) return; - switch (typeof value) { - case "string": - case "boolean": - return; - case "number": - if (!Number.isFinite(value)) { - throw new Error(`${path} is a non-finite number; JSON drops it`); - } - return; - case "object": - break; - default: - throw new Error( - `${path} is a ${typeof value}, which does not survive JSON ` + "serialization", - ); - } - if (Array.isArray(value)) { - value.forEach((element, index) => { - assertJsonPortable(element, `${path}[${index}]`); - }); - return; - } - const proto: unknown = Object.getPrototypeOf(value); - if (proto !== Object.prototype && proto !== null) { - throw new Error(`${path} is a non-plain object; JSON would flatten it lossily`); - } - for (const [key, entry] of Object.entries(value)) { - assertJsonPortable(entry, `${path}.${key}`); - } -} - -export { - PAIN_POINT_COLLATERAL_FINALIZE_TOOL, - PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME, - PAIN_POINT_COLLATERAL_FINALIZE_DESCRIPTION, - buildArtifactPayload, -} from "./finalize-tool"; -export type { ArtifactPayload, FinalizeArgs } from "./finalize-tool"; - -export { - PAIN_POINT_COLLATERAL_INTAKE_TOOL, - PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME, - PAIN_POINT_COLLATERAL_INTAKE_DESCRIPTION, - resolveIntake, -} from "./intake-tool"; -export type { IntakeArgs, IntakeResult } from "./intake-tool"; diff --git a/workflows/pain-point-collateral/src/intake-tool.test.ts b/workflows/pain-point-collateral/src/intake-tool.test.ts deleted file mode 100644 index 954c919eda..0000000000 --- a/workflows/pain-point-collateral/src/intake-tool.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { expect, test } from "bun:test"; -import type { BaseEnv } from "@intx/agent"; - -import { - PAIN_POINT_COLLATERAL_INTAKE_TOOL, - PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME, -} from "./intake-tool"; - -function testEnv(): BaseEnv { - return {} as unknown as BaseEnv; -} - -test("the tool is namespaced under this workflow package, not a shared one", () => { - expect(PAIN_POINT_COLLATERAL_INTAKE_TOOL.id).toBe( - "@corbits/workflow-pain-point-collateral/intake", - ); -}); - -test("the tool carries no approval gate — it only validates and normalizes intake", () => { - expect(PAIN_POINT_COLLATERAL_INTAKE_TOOL.definitions).toEqual([ - { name: PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME }, - ]); -}); - -test("run reports the transcript source when a non-empty transcript is given", async () => { - const bundle = PAIN_POINT_COLLATERAL_INTAKE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME, - arguments: { transcript: "Customer said onboarding is too slow." }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(false); - expect(JSON.parse(String(result.content))).toEqual({ - source: "transcript", - transcript: "Customer said onboarding is too slow.", - }); -}); - -test("run reports the noteId source when a non-empty noteId is given and no transcript is", async () => { - const bundle = PAIN_POINT_COLLATERAL_INTAKE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME, - arguments: { noteId: "note_123" }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(false); - expect(JSON.parse(String(result.content))).toEqual({ - source: "noteId", - noteId: "note_123", - }); -}); - -test("run prefers transcript over noteId when both are given", async () => { - const bundle = PAIN_POINT_COLLATERAL_INTAKE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME, - arguments: { transcript: "Real transcript.", noteId: "note_123" }, - }, - new AbortController().signal, - ); - expect(JSON.parse(String(result.content))).toEqual({ - source: "transcript", - transcript: "Real transcript.", - }); -}); - -test("run reports no source when neither field is given", async () => { - const bundle = PAIN_POINT_COLLATERAL_INTAKE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME, - arguments: {}, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(false); - expect(JSON.parse(String(result.content))).toEqual({ source: "none" }); -}); - -test("run reports no source when both fields are given but blank", async () => { - const bundle = PAIN_POINT_COLLATERAL_INTAKE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME, - arguments: { transcript: " ", noteId: "" }, - }, - new AbortController().signal, - ); - expect(JSON.parse(String(result.content))).toEqual({ source: "none" }); -}); - -test("run rejects malformed arguments (wrong types) without throwing, never passing raw JSON through", async () => { - const bundle = PAIN_POINT_COLLATERAL_INTAKE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME, - arguments: { transcript: 12345 }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid trigger input"); -}); - -test("run rejects unknown fields rather than passing raw unknown JSON through", async () => { - const bundle = PAIN_POINT_COLLATERAL_INTAKE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME, - arguments: { transcript: "hi", extra: "not allowed" }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid trigger input"); -}); diff --git a/workflows/pain-point-collateral/src/intake-tool.ts b/workflows/pain-point-collateral/src/intake-tool.ts deleted file mode 100644 index 17bcd0baf6..0000000000 --- a/workflows/pain-point-collateral/src/intake-tool.ts +++ /dev/null @@ -1,102 +0,0 @@ -// The trust boundary for this workflow's trigger-carried input. The -// trigger is mail: the run's entire raw input is whatever text and -// fields a sender's mail body carries, read by the model itself (there -// is no server-side code between the trigger and the agent — see -// `./index.ts`'s header for why this definition is pure prompt-driven -// data). Two fields are named and load-bearing: `transcript` (a pasted -// call transcript) and `noteId` (a Granola note id). Rather than let the -// model treat that raw trigger content as unexamined free-form JSON, the -// system prompt (`./index.ts`'s `PAIN_POINT_COLLATERAL_SYSTEM_PROMPT`) -// requires calling this tool first, passing along whichever of the two -// named fields it found — `run` below is the one place both fields are -// actually parsed against a schema, honestly rejecting anything that -// does not match rather than passing raw unknown JSON further into the -// run. -// -// No approval gate: unlike `./finalize-tool.ts`, this tool has no -// external side effect — it only validates and normalizes intake — so -// it carries no `approval` declaration. - -import { type } from "arktype"; -import { defineTool, type BaseEnv } from "@intx/agent"; - -export const PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME = "pain_point_collateral_intake"; - -export const PAIN_POINT_COLLATERAL_INTAKE_DESCRIPTION = - "Validates and normalizes the trigger's named intake fields (a pasted transcript or a Granola note id), reporting which one, if either, was actually given."; - -const IntakeArgs = type({ - /** A pasted call transcript, verbatim from the trigger, if given. */ - "transcript?": "string", - /** A Granola note id, verbatim from the trigger, if given. */ - "noteId?": "string", - "+": "reject", -}); - -export type IntakeArgs = typeof IntakeArgs.infer; - -export type IntakeResult = - | { readonly source: "transcript"; readonly transcript: string } - | { readonly source: "noteId"; readonly noteId: string } - | { readonly source: "none" }; - -/** - * A blank or whitespace-only string counts the same as an absent field: - * neither names real intake content. - */ -function normalized(value: string | undefined): string { - return (value ?? "").trim(); -} - -/** - * Resolves which named field, if either, carries real intake content. - * `transcript` wins when both are given — it needs no further fetch. - */ -export function resolveIntake(args: IntakeArgs): IntakeResult { - const transcript = normalized(args.transcript); - if (transcript !== "") { - return { source: "transcript", transcript }; - } - const noteId = normalized(args.noteId); - if (noteId !== "") { - return { source: "noteId", noteId }; - } - return { source: "none" }; -} - -/** `defineTool`'s env-DI factory shape. No env needs beyond `BaseEnv`. */ -export const PAIN_POINT_COLLATERAL_INTAKE_TOOL = defineTool({ - id: "@corbits/workflow-pain-point-collateral/intake", - requires: [], - definitions: [{ name: PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME }], - factory: () => ({ - definitions: [ - { - name: PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME, - description: PAIN_POINT_COLLATERAL_INTAKE_DESCRIPTION, - inputSchema: { - type: "object", - properties: { - transcript: { type: "string" }, - noteId: { type: "string" }, - }, - }, - }, - ], - run: async (call) => { - const parsed = IntakeArgs(call.arguments); - if (parsed instanceof type.errors) { - return { - callId: call.id, - isError: true, - content: `Invalid trigger input for ${PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME}: ${parsed.summary}`, - }; - } - return { - callId: call.id, - isError: false, - content: JSON.stringify(resolveIntake(parsed)), - }; - }, - }), -}); diff --git a/workflows/pain-point-collateral/test/boundary.test.ts b/workflows/pain-point-collateral/test/boundary.test.ts deleted file mode 100644 index 4f55feb147..0000000000 --- a/workflows/pain-point-collateral/test/boundary.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -// This package is installable data on the native workflow contract: -// its shipped sources import only published platform packages, so the -// source tree plus the package manifest deploys on any Interchange -// instance without a wrapper contract. - -import { readFile, readdir } from "node:fs/promises"; -import path from "node:path"; -import { expect, test } from "bun:test"; - -const ALLOWED_IMPORT_PREFIXES = ["@intx/", "arktype", "./", "../"]; - -async function listFiles(dir: string): Promise { - const entries = await readdir(dir, { withFileTypes: true }); - const files: string[] = []; - for (const entry of entries) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...(await listFiles(full))); - } else { - files.push(full); - } - } - return files; -} - -async function shippedFiles(): Promise { - const packageRoot = path.join(import.meta.dir, ".."); - return [ - ...(await listFiles(path.join(packageRoot, "src"))), - path.join(packageRoot, "package.json"), - ]; -} - -test("shipped sources import only published platform packages", async () => { - const importPattern = /from\s+"([^"]+)"/g; - const violations: string[] = []; - for (const file of await shippedFiles()) { - if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue; - const content = await readFile(file, "utf8"); - for (const match of content.matchAll(importPattern)) { - const specifier = match[1] ?? ""; - const allowed = ALLOWED_IMPORT_PREFIXES.some((prefix) => specifier.startsWith(prefix)); - if (!allowed) { - violations.push(`${path.basename(file)}: ${specifier}`); - } - } - } - expect(violations).toEqual([]); -}); diff --git a/workflows/pain-point-collateral/test/definition.test.ts b/workflows/pain-point-collateral/test/definition.test.ts deleted file mode 100644 index 4c6d093525..0000000000 --- a/workflows/pain-point-collateral/test/definition.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -// Tests for this package's own contract: the shape our factory -// commits to, its serialization guarantees, and its boundary. The -// platform's own normalization and validation are its business, not -// re-proven here. - -import { expect, test } from "bun:test"; -import type { StepPrimitive, WorkflowDefinition } from "@intx/workflow"; - -import { - PAIN_POINT_COLLATERAL_CREDENTIAL_BINDINGS, - PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME, - PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME, - PAIN_POINT_COLLATERAL_STEP_ID, - PAIN_POINT_COLLATERAL_SYSTEM_PROMPT, - PAIN_POINT_COLLATERAL_TOOL_PACKAGE_PINS, - PAIN_POINT_COLLATERAL_WORKFLOW_ID, - buildPainPointCollateralWorkflow, - serializePainPointCollateralWorkflow, -} from "../src/index"; - -const INPUT = { - triggerAddress: "ins_dep000000000000@example.test", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 300000, -} as const; - -function collateralStep(definition: WorkflowDefinition): StepPrimitive { - const primitive = definition.steps[PAIN_POINT_COLLATERAL_STEP_ID]; - if (primitive === undefined || primitive.kind !== "step") { - throw new Error(`definition has no step primitive named ${PAIN_POINT_COLLATERAL_STEP_ID}`); - } - return primitive; -} - -test("the definition has exactly one step", () => { - const definition = buildPainPointCollateralWorkflow(INPUT); - expect(definition.stepOrder).toEqual([PAIN_POINT_COLLATERAL_STEP_ID]); - expect(Object.keys(definition.steps)).toEqual([PAIN_POINT_COLLATERAL_STEP_ID]); -}); - -test("the step carries an explicit per-turn timeout", () => { - const definition = buildPainPointCollateralWorkflow(INPUT); - expect(collateralStep(definition).timeout).toBe(INPUT.turnTimeoutMs); -}); - -test("the workflow is triggered by mail to the given deployment address", () => { - const definition = buildPainPointCollateralWorkflow(INPUT); - expect(definition.id).toBe(PAIN_POINT_COLLATERAL_WORKFLOW_ID); - expect(definition.triggers).toEqual([{ type: "mail", to: INPUT.triggerAddress }]); -}); - -test("the agent carries the fixed prompt, the preferences, and inlines no tools", () => { - const agent = collateralStep(buildPainPointCollateralWorkflow(INPUT)).agent; - expect(agent.systemPrompt).toBe(PAIN_POINT_COLLATERAL_SYSTEM_PROMPT); - expect(agent.inference.sources).toEqual([...INPUT.inferencePreferences]); - // Tools arrive as packages on the deploy, never inlined here: an - // inline factory is a function-valued field the asset cannot carry. - expect(agent.toolFactories).toEqual([]); -}); - -test("the agent pins @corbits/granola-tools by name and version", () => { - const agent = collateralStep(buildPainPointCollateralWorkflow(INPUT)).agent; - expect(agent.toolPackagePins).toEqual([...PAIN_POINT_COLLATERAL_TOOL_PACKAGE_PINS]); - expect(PAIN_POINT_COLLATERAL_TOOL_PACKAGE_PINS).toEqual([ - { name: "@corbits/granola-tools", version: "0.0.5" }, - ]); -}); - -test("the system prompt names the exact approval-gated finalize tool", () => { - expect(PAIN_POINT_COLLATERAL_SYSTEM_PROMPT).toContain(PAIN_POINT_COLLATERAL_FINALIZE_TOOL_NAME); -}); - -test("the system prompt names the intake tool and requires calling it first", () => { - expect(PAIN_POINT_COLLATERAL_SYSTEM_PROMPT).toContain(PAIN_POINT_COLLATERAL_INTAKE_TOOL_NAME); -}); - -test("the system prompt names both trigger-carried fields unambiguously", () => { - expect(PAIN_POINT_COLLATERAL_SYSTEM_PROMPT).toContain("`transcript`"); - expect(PAIN_POINT_COLLATERAL_SYSTEM_PROMPT).toContain("`noteId`"); -}); - -test("the system prompt commits to a teaching artifact, not silence, when intake finds nothing", () => { - expect(PAIN_POINT_COLLATERAL_SYSTEM_PROMPT).toMatch(/no transcript/i); - expect(PAIN_POINT_COLLATERAL_SYSTEM_PROMPT).toMatch(/never invent/i); - expect(PAIN_POINT_COLLATERAL_SYSTEM_PROMPT).toMatch( - /teaching payload instead of stopping silently/i, - ); -}); - -test("the system prompt commits to a calm terminal reply on denial, not an error", () => { - expect(PAIN_POINT_COLLATERAL_SYSTEM_PROMPT).toMatch(/not approved/i); - expect(PAIN_POINT_COLLATERAL_SYSTEM_PROMPT).toMatch(/never present a denial as an error/i); -}); - -test("the definition binds @corbits/granola-tools' declared handle to a tenant-owned granola credential", () => { - const definition = buildPainPointCollateralWorkflow(INPUT); - expect(definition.credentialBindings).toEqual([...PAIN_POINT_COLLATERAL_CREDENTIAL_BINDINGS]); - expect(PAIN_POINT_COLLATERAL_CREDENTIAL_BINDINGS).toEqual([ - { - package: "@corbits/granola-tools", - handle: "granola", - provider: "granola", - locator: "tenant", - }, - ]); -}); - -test("the definition survives the workflow-asset JSON round-trip", () => { - const definition = buildPainPointCollateralWorkflow(INPUT); - const revived: unknown = JSON.parse(serializePainPointCollateralWorkflow(definition)); - expect(revived).toEqual(definition); -}); - -test("an empty trigger address is rejected", () => { - expect(() => buildPainPointCollateralWorkflow({ ...INPUT, triggerAddress: "" })).toThrow( - /triggerAddress/, - ); -}); - -test("a non-positive or fractional turn timeout is rejected", () => { - expect(() => buildPainPointCollateralWorkflow({ ...INPUT, turnTimeoutMs: 0 })).toThrow( - /turnTimeoutMs/, - ); - expect(() => buildPainPointCollateralWorkflow({ ...INPUT, turnTimeoutMs: 0.5 })).toThrow( - /turnTimeoutMs/, - ); -}); diff --git a/workflows/pain-point-collateral/tsconfig.json b/workflows/pain-point-collateral/tsconfig.json deleted file mode 100644 index a8c2b9966d..0000000000 --- a/workflows/pain-point-collateral/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src", "test"], - "exclude": [] -} diff --git a/workflows/pain-point-collateral/tsconfig.src.json b/workflows/pain-point-collateral/tsconfig.src.json deleted file mode 100644 index fc8724305f..0000000000 --- a/workflows/pain-point-collateral/tsconfig.src.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } -} diff --git a/workflows/process-granola-call/LICENSE b/workflows/process-granola-call/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/workflows/process-granola-call/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/workflows/process-granola-call/README.md b/workflows/process-granola-call/README.md deleted file mode 100644 index 76ba0754f5..0000000000 --- a/workflows/process-granola-call/README.md +++ /dev/null @@ -1,108 +0,0 @@ -# @corbits/process-granola-call-workflow - -The child half of the Granola call-notes pipeline (CL-5998, ported from -`gtm-workbench`'s `workflows/process-granola-call`). One run processes -exactly one Granola call. It is spawned by `@corbits/granola-call-workflow`, -never scheduled or invoked directly — see `automatable: false` in this -package's `corbits.workflow` block. - -## What it does - -One step, one agent. The system prompt commits it to, per call: - -1. fetching the transcript and saving it as a raw transcript artifact, -2. extracting working notes with exactly five sections — Participants, - Summary, Pain points, Decisions, Action items — grounded in the - transcript (quoted or closely paraphrased, "None noted" where a - section is empty, never invented), -3. verifying that draft against the transcript, then calling - `process_granola_call_finalize` (`./src/finalize-tool.ts`) once with - `status: "notes"` to persist the final call-notes artifact. - -If it cannot fetch the call's transcript — no Granola connection, an -unknown call id, an empty transcript — it does not fabricate call notes. -Instead it calls the same finalize tool once with `status: "no-data"`: a -plain-language reason grounded in what actually happened, and next -steps a human can check, starting with the `granola` connector's -connection status for this workspace -(`packages/connections/src/registry.ts`). That failure is scoped to the -one run: because the parent spawns one run per call, one bad transcript -never blocks the rest of a batch — and the human still gets something -useful instead of a bare "nothing to report" line. - -Both finalize calls are approval-gated (`approval: "ask"`, the -platform's native tool-approval gate — see -`workflows/pain-point-collateral/src/finalize-tool.ts`'s header comment -for the exact suspend/resume mechanics, identical here) and, once -approved, persist a real Library artifact via -`createWorkflowArtifact` (`./src/artifact-client.ts`, CL-6000), which -becomes a chip through `packages/chat/src/artifact-delivery.ts`. A -denied call gets a calm, plain reply that nothing was published, never -an error. - -**Teaching-artifact kind**: `status` (`"notes"` | `"no-data"`), already -the structural argument that picks which of the two content shapes to -build, also picks the persisted artifact's `kind`: `"text"` for real -call notes, `"status-note"` for the no-data teaching payload — decided -by `buildArtifactPayload` from `args.status`, never left to free text. -`"status-note"` is the one teaching-artifact kind shared by every -workflow in this catalog, so the Library's kind badge always reads -"Status note" for a no-data run, regardless of which workflow made it. - -## Current limits (read before deploying) - -One platform-level gap remains, shared with the parent -(`@corbits/granola-call-workflow`'s README): no shipped host resolves -`@intx/workflow`'s `action` primitive, so nothing can spawn this -workflow programmatically yet — it must be started by hand (or by -future host machinery) with a call id until that lands. Separately, -`corbitsdev/granola-tools` still needs to be published somewhere this -host's tool loader can resolve it from before a real transcript fetch -succeeds; until an operator does that, every run's finalize call -reports `status: "no-data"` honestly rather than fabricating notes. - -## Usage - -```ts -import { - buildProcessGranolaCallWorkflow, - serializeProcessGranolaCallWorkflow, -} from "@corbits/process-granola-call-workflow"; - -const definition = buildProcessGranolaCallWorkflow({ - triggerAddress: "process-granola-call@tenant.example", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 300_000, -}); - -const json = serializeProcessGranolaCallWorkflow(definition); -``` - -A transcript-plus-extraction-plus-verification pass over a long call -needs more headroom than the catalog's shortest steps — size -`turnTimeoutMs` accordingly (the example above gives it five minutes). - -See [`workflows/README.md`](../README.md#status-note) for what -registration/automatable/seeded mean — this one is spawned only by -`granola-call`, so it's `automatable: false` and never seeded on its own. - -## Deploy from the npm registry - -Once published, deploy this workflow definition to a tenant by name and -version pin: - -``` -POST /api/tenants/:id/workflows/deployments -{ - "source": { "kind": "registry", "registry": "npm" }, - "entry": "./src/index.ts", - "pin": "@corbits/process-granola-call-workflow@0.0.1", - "sourceOfferingIds": ["", ...], - "defaultSourceOfferingId": "" -} -``` - -`entry` is the package's `interchange.workflow` module path; `pin` is -`"@corbits/process-granola-call-workflow@0.0.1"` or a semver range on the same name. The hub -installs, probes, gates, and freezes the definition from the registry -tarball before creating the deployment. diff --git a/workflows/process-granola-call/package.json b/workflows/process-granola-call/package.json deleted file mode 100644 index ccbea557e7..0000000000 --- a/workflows/process-granola-call/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@corbits/process-granola-call-workflow", - "version": "0.0.1", - "description": "Processes one Granola call end to end: raw transcript artifact, extracted working notes, then a verified, published call-notes artifact", - "license": "LGPL-2.1-or-later", - "repository": { - "type": "git", - "url": "https://github.com/corbitsdev/workbench.git", - "directory": "workflows/process-granola-call" - }, - "files": [ - "src", - "LICENSE", - "README.md" - ], - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "arktype": "catalog:" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0" - }, - "corbits": { - "workflow": { - "assetName": "process-granola-call", - "displayName": "Process Granola call", - "automatable": false - } - }, - "interchange": { - "workflow": "./src/index.ts" - } -} diff --git a/workflows/process-granola-call/src/artifact-client.test.ts b/workflows/process-granola-call/src/artifact-client.test.ts deleted file mode 100644 index 1c86414419..0000000000 --- a/workflows/process-granola-call/src/artifact-client.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { expect, test } from "bun:test"; - -import { createWorkflowArtifact } from "./artifact-client"; - -const CONFIG = { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - runAddress: "run_1@workflow", -}; - -test("posts to the workflow-artifacts endpoint and returns id/version", async () => { - const captured: { url: string; init: RequestInit | undefined } = { - url: "", - init: undefined, - }; - const fetchImpl = (async (input: URL | string, init?: RequestInit) => { - captured.url = String(input); - captured.init = init; - return new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - }); - }) as unknown as typeof fetch; - - const result = await createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ); - - expect(result).toEqual({ id: "art_1", version: 1 }); - expect(captured.url).toBe("https://hub.example.com/api/workflow-artifacts/artifacts"); - const headers = captured.init?.headers as Record; - expect(headers["authorization"]).toBe("Bearer sc-token"); - expect(headers["x-workflow-run-address"]).toBe("run_1@workflow"); -}); - -test("throws on a non-ok HTTP response", async () => { - const fetchImpl = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/500/); -}); - -test("throws when the response doesn't match the expected shape", async () => { - const fetchImpl = (async () => - new Response(JSON.stringify({ data: {} }), { - status: 201, - })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/did not match the expected shape/); -}); diff --git a/workflows/process-granola-call/src/artifact-client.ts b/workflows/process-granola-call/src/artifact-client.ts deleted file mode 100644 index b9d56ca425..0000000000 --- a/workflows/process-granola-call/src/artifact-client.ts +++ /dev/null @@ -1,62 +0,0 @@ -// A minimal client for the sanctioned workflow-artifacts HTTP surface -// (`@corbits/artifacts-hub`'s `createWorkflowArtifactRoutes`, CL-6000). -// Deliberately duplicated from `@corbits/artifact-tools`' `client.ts` -// rather than imported: this package ships as installable data on the -// native workflow contract (see `./index.ts`'s header comment and -// `test/boundary.test.ts`), so its shipped sources import only `@intx/*`, -// `arktype`, and relative specifiers — never another `@corbits/*` -// package. Authenticates with the sidecar's own bearer token plus the -// run's own mailbox address (both already reach a workflow-process -// child's tool env), never a database handle. -import { type } from "arktype"; - -export interface WorkflowArtifactClientConfig { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly runAddress: string; - /** Override for tests; defaults to the global `fetch`. */ - readonly fetchImpl?: typeof fetch; -} - -export type CreateWorkflowArtifactInput = { - readonly title: string; - readonly kind: string; - readonly content: string; -}; - -export type CreatedWorkflowArtifact = { - readonly id: string; - readonly version: number; -}; - -const CreatedWorkflowArtifactResponse = type({ - data: { id: "string", version: "number" }, -}); - -/** Persists one artifact. Throws on any transport, HTTP, or shape failure. */ -export async function createWorkflowArtifact( - config: WorkflowArtifactClientConfig, - input: CreateWorkflowArtifactInput, -): Promise { - const doFetch = config.fetchImpl ?? fetch; - const response = await doFetch(`${config.hubArtifactsUrl}/api/workflow-artifacts/artifacts`, { - method: "POST", - headers: { - authorization: `Bearer ${config.sidecarToken}`, - "x-workflow-run-address": config.runAddress, - "content-type": "application/json", - }, - body: JSON.stringify(input), - }); - if (!response.ok) { - throw new Error(`Workflow artifact create failed: ${response.status} ${response.statusText}`); - } - const body: unknown = await response.json(); - const parsed = CreatedWorkflowArtifactResponse(body); - if (parsed instanceof type.errors) { - throw new Error( - `Workflow artifact create response did not match the expected shape: ${parsed.summary}`, - ); - } - return parsed.data; -} diff --git a/workflows/process-granola-call/src/finalize-tool.test.ts b/workflows/process-granola-call/src/finalize-tool.test.ts deleted file mode 100644 index 842097e2df..0000000000 --- a/workflows/process-granola-call/src/finalize-tool.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { expect, test } from "bun:test"; - -import { - PROCESS_GRANOLA_CALL_FINALIZE_TOOL, - PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME, - buildArtifactPayload, - type WorkflowArtifactEnv, -} from "./finalize-tool"; - -test("the tool's definition marks itself approval-gated", () => { - expect(PROCESS_GRANOLA_CALL_FINALIZE_TOOL.definitions).toEqual([ - { name: PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME, approval: "ask" }, - ]); -}); - -test("the tool is namespaced under this workflow package, not a shared one", () => { - expect(PROCESS_GRANOLA_CALL_FINALIZE_TOOL.id).toBe( - "@corbits/workflow-process-granola-call/finalize", - ); -}); - -test("requires the sanctioned workflow-artifacts env keys", () => { - expect(PROCESS_GRANOLA_CALL_FINALIZE_TOOL.requires).toEqual([ - "hubArtifactsUrl", - "sidecarToken", - "address", - ]); -}); - -test("buildArtifactPayload formats a notes artifact with all five sections", () => { - const payload = buildArtifactPayload({ - status: "notes", - callId: "call_123", - title: "Acme kickoff call", - participants: "Jane Doe (Acme), John Smith (Corbits)", - summary: "Discussed onboarding timeline and pricing.", - painPoints: "Existing tool is too slow to configure.", - decisions: "Move forward with a pilot.", - actionItems: "Corbits to send pilot agreement by Friday.", - }); - expect(payload.title).toBe("Acme kickoff call"); - expect(payload.kind).toBe("text"); - expect(payload.content).toContain("call_123"); - expect(payload.content).toContain("Participants"); - expect(payload.content).toContain("Jane Doe (Acme), John Smith (Corbits)"); - expect(payload.content).toContain("Summary"); - expect(payload.content).toContain("Pain points"); - expect(payload.content).toContain("Decisions"); - expect(payload.content).toContain("Action items"); -}); - -test("buildArtifactPayload formats a teaching artifact for the no-data case", () => { - const payload = buildArtifactPayload({ - status: "no-data", - callId: "call_456", - title: "Call call_456 could not be processed", - reason: "The Granola connection returned no transcript for this call id.", - nextSteps: - "Check the granola connector's status in Settings > Connections and reconnect if needed.", - }); - expect(payload.kind).toBe("status-note"); - expect(payload.content).toContain("call_456"); - expect(payload.content).toContain( - "The Granola connection returned no transcript for this call id.", - ); - expect(payload.content).toContain("granola connector"); -}); - -function testEnv(): WorkflowArtifactEnv { - return { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - address: "run_1@workflow", - } as unknown as WorkflowArtifactEnv; -} - -test("run persists a notes artifact on real invocation and reports it persisted", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - })) as unknown as typeof fetch; - - try { - const bundle = PROCESS_GRANOLA_CALL_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME, - arguments: { - status: "notes", - callId: "call_123", - title: "Acme kickoff call", - participants: "Jane Doe (Acme)", - summary: "Discussed onboarding.", - painPoints: "None noted", - decisions: "None noted", - actionItems: "None noted", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(String(result.content)) as { - id: string; - version: number; - title: string; - kind: string; - persisted: boolean; - }; - expect(parsed).toEqual({ - id: "art_1", - version: 1, - title: "Acme kickoff call", - kind: "text", - persisted: true, - }); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run persists a teaching artifact for the no-data case, still chip-visible", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response(JSON.stringify({ data: { id: "art_2", version: 1 } }), { - status: 201, - })) as unknown as typeof fetch; - - try { - const bundle = PROCESS_GRANOLA_CALL_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_2", - name: PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME, - arguments: { - status: "no-data", - callId: "call_456", - title: "Call call_456 could not be processed", - reason: "No Granola connection for this workspace.", - nextSteps: "Reconnect Granola in Settings > Connections.", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(String(result.content)) as { - id: string; - version: number; - title: string; - kind: string; - persisted: boolean; - }; - expect(parsed).toEqual({ - id: "art_2", - version: 1, - title: "Call call_456 could not be processed", - kind: "status-note", - persisted: true, - }); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run returns an honest error result when persistence fails, never fabricating persisted: true", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - try { - const bundle = PROCESS_GRANOLA_CALL_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME, - arguments: { - status: "notes", - callId: "call_123", - title: "Acme kickoff call", - participants: "Jane Doe (Acme)", - summary: "Discussed onboarding.", - painPoints: "None noted", - decisions: "None noted", - actionItems: "None noted", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(true); - expect(result.content).toContain("Failed to persist"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("run rejects malformed arguments without throwing", async () => { - const bundle = PROCESS_GRANOLA_CALL_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME, - arguments: {}, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); - -test("run rejects a notes payload missing a required section", async () => { - const bundle = PROCESS_GRANOLA_CALL_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME, - arguments: { - status: "notes", - callId: "call_123", - title: "Acme kickoff call", - participants: "Jane Doe (Acme)", - summary: "Discussed onboarding.", - painPoints: "None noted", - decisions: "None noted", - // actionItems missing - }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); diff --git a/workflows/process-granola-call/src/finalize-tool.ts b/workflows/process-granola-call/src/finalize-tool.ts deleted file mode 100644 index 03c69088f9..0000000000 --- a/workflows/process-granola-call/src/finalize-tool.ts +++ /dev/null @@ -1,205 +0,0 @@ -// The one piece of this workflow gated by human approval: finalizing one -// call's outcome, either the verified call-notes artifact or — when the -// transcript could not be fetched — a teaching artifact explaining what -// was attempted and what to check next. Kept inside the workflow package -// 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 -// @corbits/workflows's "workflow-specific logic lives in the definition" -// convention. -// -// Approval mechanics: `defineTool`'s `definitions` array marks this -// tool's one definition `approval: "ask"` (see -// `@intx/agent/src/tool.ts`'s `ToolDeclaration`/`toolApprovalEffect`), -// the platform's native gate — see `workflows/pain-point-collateral/src/finalize-tool.ts`'s -// header comment for the full suspend/resume path, identical here. -// -// Persistence (CL-6000): `run` below calls `createWorkflowArtifact` -// (`./artifact-client.ts`) against the sanctioned workflow-artifacts HTTP -// surface — authenticated with the sidecar's own bearer token and this -// run's own mailbox address, both already present on `env`, never a -// database handle. A successful call returns the persisted artifact's -// id/version so the delivery pipeline can reference it with a file part -// (`packages/chat/src/artifact-delivery.ts`); a failed call surfaces as -// an honest `isError: true` result rather than a fabricated success. -// -// Two shapes, one tool: a real call gets a five-section call-notes -// artifact (`status: "notes"`); a call whose transcript could not be -// fetched gets a teaching artifact instead of a bare "nothing to -// report" reply (`status: "no-data"`) — what call was attempted, why it -// came up empty, and what to check next (the `granola` connector from -// `packages/connections/src/registry.ts`). Both are real persisted, -// chip-visible Library artifacts; neither invents call content. - -import { type } from "arktype"; -import { defineTool, type BaseEnv } from "@intx/agent"; -import { createWorkflowArtifact } from "./artifact-client"; - -export const PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME = "process_granola_call_finalize"; - -export const PROCESS_GRANOLA_CALL_FINALIZE_DESCRIPTION = - "Finalizes one Granola call, pending human approval, and persists the " + - "outcome as a Library artifact — a five-section call-notes artifact " + - "when the transcript was read, or a teaching artifact explaining what " + - "went wrong when it could not be fetched."; - -const NotesFinalizeArgs = type({ - status: "'notes'", - /** The Granola call id this run was started with. */ - callId: "string > 0", - /** Short, human-facing title. Doubles as the inbox/approve-card headline. */ - title: "string > 0", - participants: "string > 0", - summary: "string > 0", - painPoints: "string > 0", - decisions: "string > 0", - actionItems: "string > 0", -}); - -const NoDataFinalizeArgs = type({ - status: "'no-data'", - /** The Granola call id this run was started with. */ - callId: "string > 0", - /** Short, human-facing title. Doubles as the inbox/approve-card headline. */ - title: "string > 0", - /** Why the transcript could not be fetched, grounded in what actually happened. */ - reason: "string > 0", - /** What a human should check next (e.g. the granola connector's status). */ - nextSteps: "string > 0", -}); - -const FinalizeArgs = NotesFinalizeArgs.or(NoDataFinalizeArgs); - -export type FinalizeArgs = typeof FinalizeArgs.infer; - -export type ArtifactPayload = { - title: string; - kind: "text" | "status-note"; - content: string; -}; - -/** - * The payload `createWorkflowArtifact` persists (`{ title, kind, - * content }`). Built here so the persist call is a straight pass-through - * of this object, not a payload assembled at the call site. `kind` - * comes from `args.status` — the same structural discriminator that - * already picks which of the two content shapes to build — never from - * free-text the model could drift on. - */ -export function buildArtifactPayload(args: FinalizeArgs): ArtifactPayload { - if (args.status === "notes") { - return { - title: args.title, - kind: "text", - content: - `Call: ${args.callId}\n\n` + - `Participants\n${args.participants}\n\n` + - `Summary\n${args.summary}\n\n` + - `Pain points\n${args.painPoints}\n\n` + - `Decisions\n${args.decisions}\n\n` + - `Action items\n${args.actionItems}`, - }; - } - return { - title: args.title, - kind: "status-note", - content: - `Call: ${args.callId}\n\n` + - "This call could not be processed.\n\n" + - `Why\n${args.reason}\n\n` + - `Next steps\n${args.nextSteps}\n\n` + - "Check the granola connector's status for this workspace under " + - "Settings > Connections.", - }; -} - -/** - * The env this tool needs beyond `BaseEnv`: the sanctioned - * workflow-artifacts credential trio, populated for every workflow step - * (`apps/sidecar/src/workflow-substrate-factory/step-env.ts`) — the same - * three keys `@corbits/artifact-tools`' read-side bundle declares. - */ -export interface WorkflowArtifactEnv extends BaseEnv { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly address: string; -} - -/** - * `defineTool`'s env-DI factory shape. Needs the sanctioned - * workflow-artifacts credential trio beyond `BaseEnv`. - */ -export const PROCESS_GRANOLA_CALL_FINALIZE_TOOL = defineTool({ - id: "@corbits/workflow-process-granola-call/finalize", - requires: ["hubArtifactsUrl", "sidecarToken", "address"], - definitions: [ - { - name: PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME, - approval: "ask", - }, - ], - factory: (env) => ({ - definitions: [ - { - name: PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME, - description: PROCESS_GRANOLA_CALL_FINALIZE_DESCRIPTION, - inputSchema: { - type: "object", - properties: { - status: { type: "string", enum: ["notes", "no-data"] }, - callId: { type: "string" }, - title: { type: "string" }, - participants: { type: "string" }, - summary: { type: "string" }, - painPoints: { type: "string" }, - decisions: { type: "string" }, - actionItems: { type: "string" }, - reason: { type: "string" }, - nextSteps: { type: "string" }, - }, - required: ["status", "callId", "title"], - }, - }, - ], - run: async (call) => { - const parsed = FinalizeArgs(call.arguments); - if (parsed instanceof type.errors) { - return { - callId: call.id, - isError: true, - content: `Invalid arguments for ${PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME}: ${parsed.summary}`, - }; - } - const artifact = buildArtifactPayload(parsed); - try { - const created = await createWorkflowArtifact( - { - hubArtifactsUrl: env.hubArtifactsUrl, - sidecarToken: env.sidecarToken, - runAddress: env.address, - }, - artifact, - ); - return { - callId: call.id, - isError: false, - content: JSON.stringify({ - id: created.id, - version: created.version, - title: artifact.title, - kind: artifact.kind, - persisted: true, - }), - }; - } catch (err) { - return { - callId: call.id, - isError: true, - content: `Failed to persist "${artifact.title}" as a Library artifact: ${ - err instanceof Error ? err.message : String(err) - }`, - }; - } - }, - }), -}); diff --git a/workflows/process-granola-call/src/index.ts b/workflows/process-granola-call/src/index.ts deleted file mode 100644 index 7083b90d48..0000000000 --- a/workflows/process-granola-call/src/index.ts +++ /dev/null @@ -1,217 +0,0 @@ -// The process-granola-call workflow: the child half of the Granola -// call-notes pipeline (CL-5998). One run processes exactly one Granola -// call: fetch its transcript, save the transcript as a raw artifact, -// extract working notes (participants, summary, pain points, decisions, -// action items), verify them against the transcript, then publish the -// final call-notes artifact. It is spawned by the granola-call parent, -// 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/workflows`). -// -// This package is installable data. It imports only published platform -// packages, and nothing imports it statically: a host publishes the -// serialized definition as a workflow asset and deploys it through the -// platform's deploy machinery; the execution host materializes it at -// runtime from the deploy alone. -// -// Infrastructure gap (see README "Current limits", and -// `@corbits/granola-call-workflow`'s header comment): nothing in this -// host can spawn this workflow programmatically yet. That gap is -// unrelated to tool access: CL-5999 closed the tool-pin gap this file -// used to document here (`@intx/agent`'s `defineAgent` still does not -// accept a `toolPackagePins` field — it is vendored, read-only source — -// so the agent below is built directly against `AgentDefinition`'s own -// type, which already carries the field). `@corbits/granola-tools` is -// pinned below; whether it *resolves* at deploy time still depends on -// an operator publishing it to a registry the host's tool-package -// resolver can reach (see `apps/hub/src/index.ts`'s -// `toolPackageRegistries` wiring). Until a deploy actually resolves the -// pin, and until spawning itself lands, this definition's system -// prompt still commits it to saying plainly that it cannot reach -// Granola or the call it was asked about, rather than inventing call -// notes for a transcript it never read. -// -// Finalizing (CL-6029): `process_granola_call_finalize` -// (`./finalize-tool.ts`) persists the run's outcome as a real Library -// artifact either way — a five-section call-notes artifact when the -// transcript was read, or a teaching artifact (what call was attempted, -// why it came up empty, what to check next) when it was not. Both are -// approval-gated the same way `pain-point-collateral`'s finalize tool -// is; see that package's `finalize-tool.ts` header for the full -// suspend/resume mechanics, identical here. - -import type { AgentDefinition, InferencePreference } from "@intx/agent"; -import { defineWorkflow, step } from "@intx/workflow"; -import type { WorkflowDefinition } from "@intx/workflow"; -import type { ToolPackagePin } from "@intx/types/tool-packages"; -import type { CredentialBinding } from "@intx/types"; - -import { PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME } from "./finalize-tool"; - -export const PROCESS_GRANOLA_CALL_WORKFLOW_ID = "wf_process_granola_call"; -export const PROCESS_GRANOLA_CALL_STEP_ID = "process-granola-call"; - -export const PROCESS_GRANOLA_CALL_SYSTEM_PROMPT = - "You process one Granola call end to end, for the call id this run " + - "was started with. Fetch its transcript and save it as a raw " + - "transcript artifact. Read the whole transcript and extract working " + - "notes with exactly five sections: Participants (names and, when " + - "stated, roles or companies), Summary (one or two plain paragraphs), " + - "Pain points, Decisions, and Action items (each a short bullet list " + - 'grounded in the transcript — write "None noted" when the ' + - "transcript shows none; quote or closely paraphrase, never invent). " + - "Verify that working document against the transcript, fixing anything " + - "unsupported or misattributed, then call " + - `\`${PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME}\` exactly once with ` + - '`status: "notes"` and the five sections to publish the final ' + - "call-notes artifact. " + - "If you cannot fetch this call's transcript — no Granola connection, " + - "the call id does not exist, or the transcript is empty — do not " + - "fabricate call notes. Instead call " + - `\`${PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME}\` exactly once with ` + - '`status: "no-data"`, a plain-language reason grounded in what ' + - "actually happened, and next steps a human can check, starting with " + - "the granola connector's connection status for this workspace. Do not " + - "publish a call-notes artifact for it — that failure is this one run " + - "failing honestly, never a fabricated document, but it still teaches " + - "the human what to do next rather than leaving them with nothing. " + - "Both calls require a human's approval before they complete; if " + - "denied, reply with one calm, plain sentence that nothing was " + - "published and no action was taken, never present a denial as an " + - "error."; - -/** Tool packages this definition pins (CL-5999); see the header comment. */ -export const PROCESS_GRANOLA_CALL_TOOL_PACKAGE_PINS: readonly ToolPackagePin[] = [ - { name: "@corbits/granola-tools", version: "0.0.5" }, -]; - -/** - * Binds `@corbits/granola-tools`' declared "granola" handle to a - * tenant-owned Granola credential (CL-6028); see - * `workflows/granola-call/src/index.ts`'s sibling constant for the full - * rationale. - */ -export const PROCESS_GRANOLA_CALL_CREDENTIAL_BINDINGS: readonly CredentialBinding[] = [ - { - package: "@corbits/granola-tools", - handle: "granola", - provider: "granola", - locator: "tenant", - }, -]; - -/** - * Everything the definition needs that is per-deployment data. The - * trigger address names a specific deployment's inbox — for this - * workflow, the address the granola-call parent's spawn mechanism - * targets once spawning is wired (see README) — so a definition built - * here is per-deployment by construction. - */ -export interface ProcessGranolaCallWorkflowInput { - /** The deployment's mail address; each inbound mail is one run. */ - readonly triggerAddress: string; - /** Provider/model preferences, in order; resolved at deploy time. */ - readonly inferencePreferences: readonly InferencePreference[]; - /** Per-turn timeout in milliseconds, enforced on the single step. */ - readonly turnTimeoutMs: number; -} - -/** - * Builds the process-granola-call definition. Exactly one step, matching - * the shape every other definition in this repo commits to: tools arrive - * as packages on the deploy, never inlined here, keeping the definition - * pure data. - * - * The step always sets an explicit `timeout` — the singular `agent:` - * shorthand sets none, and a wedged inference call would then hang a run - * forever. A transcript-plus-extraction-plus-verification pass over a - * long call can run well past a short default, so deployers should give - * this step more headroom than the shortest steps in the catalog (see - * README). - */ -export function buildProcessGranolaCallWorkflow( - input: ProcessGranolaCallWorkflowInput, -): WorkflowDefinition { - if (input.triggerAddress === "") { - throw new Error("buildProcessGranolaCallWorkflow requires a non-empty triggerAddress"); - } - if (!Number.isInteger(input.turnTimeoutMs) || input.turnTimeoutMs <= 0) { - throw new Error( - "buildProcessGranolaCallWorkflow requires turnTimeoutMs to be a positive integer", - ); - } - return defineWorkflow({ - id: PROCESS_GRANOLA_CALL_WORKFLOW_ID, - trigger: { type: "mail", to: input.triggerAddress }, - credentialBindings: PROCESS_GRANOLA_CALL_CREDENTIAL_BINDINGS, - steps: { - [PROCESS_GRANOLA_CALL_STEP_ID]: step({ - agent: { - id: PROCESS_GRANOLA_CALL_STEP_ID, - description: - "Processes one Granola call into a verified, published " + "call-notes artifact", - systemPrompt: PROCESS_GRANOLA_CALL_SYSTEM_PROMPT, - toolFactories: [], - capabilities: [], - inference: { sources: input.inferencePreferences }, - toolPackagePins: PROCESS_GRANOLA_CALL_TOOL_PACKAGE_PINS, - } satisfies AgentDefinition, - timeout: input.turnTimeoutMs, - }), - }, - }); -} - -/** - * Serializes a definition to the JSON a workflow asset carries. The - * definition must survive the asset round-trip byte-faithfully, so - * anything JSON would silently drop or mangle — functions, undefined, - * symbols, bigints, non-finite numbers, class instances — is a loud - * error naming the offending path instead of a corrupted asset. - */ -export function serializeProcessGranolaCallWorkflow(definition: WorkflowDefinition): string { - assertJsonPortable(definition, "definition"); - return JSON.stringify(definition); -} - -function assertJsonPortable(value: unknown, path: string): void { - if (value === null) return; - switch (typeof value) { - case "string": - case "boolean": - return; - case "number": - if (!Number.isFinite(value)) { - throw new Error(`${path} is a non-finite number; JSON drops it`); - } - return; - case "object": - break; - default: - throw new Error( - `${path} is a ${typeof value}, which does not survive JSON ` + "serialization", - ); - } - if (Array.isArray(value)) { - value.forEach((element, index) => { - assertJsonPortable(element, `${path}[${index}]`); - }); - return; - } - const proto: unknown = Object.getPrototypeOf(value); - if (proto !== Object.prototype && proto !== null) { - throw new Error(`${path} is a non-plain object; JSON would flatten it lossily`); - } - for (const [key, entry] of Object.entries(value)) { - assertJsonPortable(entry, `${path}.${key}`); - } -} - -export { - PROCESS_GRANOLA_CALL_FINALIZE_TOOL, - PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME, - PROCESS_GRANOLA_CALL_FINALIZE_DESCRIPTION, - buildArtifactPayload, -} from "./finalize-tool"; -export type { ArtifactPayload, FinalizeArgs } from "./finalize-tool"; diff --git a/workflows/process-granola-call/test/boundary.test.ts b/workflows/process-granola-call/test/boundary.test.ts deleted file mode 100644 index 4f55feb147..0000000000 --- a/workflows/process-granola-call/test/boundary.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -// This package is installable data on the native workflow contract: -// its shipped sources import only published platform packages, so the -// source tree plus the package manifest deploys on any Interchange -// instance without a wrapper contract. - -import { readFile, readdir } from "node:fs/promises"; -import path from "node:path"; -import { expect, test } from "bun:test"; - -const ALLOWED_IMPORT_PREFIXES = ["@intx/", "arktype", "./", "../"]; - -async function listFiles(dir: string): Promise { - const entries = await readdir(dir, { withFileTypes: true }); - const files: string[] = []; - for (const entry of entries) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...(await listFiles(full))); - } else { - files.push(full); - } - } - return files; -} - -async function shippedFiles(): Promise { - const packageRoot = path.join(import.meta.dir, ".."); - return [ - ...(await listFiles(path.join(packageRoot, "src"))), - path.join(packageRoot, "package.json"), - ]; -} - -test("shipped sources import only published platform packages", async () => { - const importPattern = /from\s+"([^"]+)"/g; - const violations: string[] = []; - for (const file of await shippedFiles()) { - if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue; - const content = await readFile(file, "utf8"); - for (const match of content.matchAll(importPattern)) { - const specifier = match[1] ?? ""; - const allowed = ALLOWED_IMPORT_PREFIXES.some((prefix) => specifier.startsWith(prefix)); - if (!allowed) { - violations.push(`${path.basename(file)}: ${specifier}`); - } - } - } - expect(violations).toEqual([]); -}); diff --git a/workflows/process-granola-call/test/definition.test.ts b/workflows/process-granola-call/test/definition.test.ts deleted file mode 100644 index 84f7680daf..0000000000 --- a/workflows/process-granola-call/test/definition.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -// Tests for this package's own contract: the shape our factory -// commits to, its serialization guarantees, and its boundary. The -// platform's own normalization and validation are its business, not -// re-proven here. - -import { expect, test } from "bun:test"; -import type { StepPrimitive, WorkflowDefinition } from "@intx/workflow"; - -import { - PROCESS_GRANOLA_CALL_CREDENTIAL_BINDINGS, - PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME, - PROCESS_GRANOLA_CALL_STEP_ID, - PROCESS_GRANOLA_CALL_SYSTEM_PROMPT, - PROCESS_GRANOLA_CALL_TOOL_PACKAGE_PINS, - PROCESS_GRANOLA_CALL_WORKFLOW_ID, - buildProcessGranolaCallWorkflow, - serializeProcessGranolaCallWorkflow, -} from "../src/index"; - -const INPUT = { - triggerAddress: "ins_dep000000000001@example.test", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 600000, -} as const; - -function processStep(definition: WorkflowDefinition): StepPrimitive { - const primitive = definition.steps[PROCESS_GRANOLA_CALL_STEP_ID]; - if (primitive === undefined || primitive.kind !== "step") { - throw new Error(`definition has no step primitive named ${PROCESS_GRANOLA_CALL_STEP_ID}`); - } - return primitive; -} - -test("the definition has exactly one step", () => { - const definition = buildProcessGranolaCallWorkflow(INPUT); - expect(definition.stepOrder).toEqual([PROCESS_GRANOLA_CALL_STEP_ID]); - expect(Object.keys(definition.steps)).toEqual([PROCESS_GRANOLA_CALL_STEP_ID]); -}); - -test("the step carries an explicit per-turn timeout", () => { - const definition = buildProcessGranolaCallWorkflow(INPUT); - expect(processStep(definition).timeout).toBe(INPUT.turnTimeoutMs); -}); - -test("the workflow is triggered by mail to the given deployment address", () => { - const definition = buildProcessGranolaCallWorkflow(INPUT); - expect(definition.id).toBe(PROCESS_GRANOLA_CALL_WORKFLOW_ID); - expect(definition.triggers).toEqual([{ type: "mail", to: INPUT.triggerAddress }]); -}); - -test("the agent instructs the five-section extraction, carries the preferences, and inlines no tools", () => { - const agent = processStep(buildProcessGranolaCallWorkflow(INPUT)).agent; - expect(agent.systemPrompt).toBe(PROCESS_GRANOLA_CALL_SYSTEM_PROMPT); - expect(agent.inference.sources).toEqual([...INPUT.inferencePreferences]); - expect(agent.toolFactories).toEqual([]); - for (const section of ["Participants", "Summary", "Pain points", "Decisions", "Action items"]) { - expect(PROCESS_GRANOLA_CALL_SYSTEM_PROMPT).toContain(section); - } -}); - -test("the agent pins @corbits/granola-tools by name and version", () => { - const agent = processStep(buildProcessGranolaCallWorkflow(INPUT)).agent; - expect(agent.toolPackagePins).toEqual([...PROCESS_GRANOLA_CALL_TOOL_PACKAGE_PINS]); - expect(PROCESS_GRANOLA_CALL_TOOL_PACKAGE_PINS).toEqual([ - { name: "@corbits/granola-tools", version: "0.0.5" }, - ]); -}); - -test("the system prompt commits to an honest failure instead of a fabricated document", () => { - const lowered = PROCESS_GRANOLA_CALL_SYSTEM_PROMPT.toLowerCase(); - expect(lowered).toContain("do not fabricate call notes"); - expect(lowered).toContain("never a fabricated document"); -}); - -test("the system prompt names the finalize tool for both the notes and no-data cases", () => { - expect(PROCESS_GRANOLA_CALL_SYSTEM_PROMPT).toContain(PROCESS_GRANOLA_CALL_FINALIZE_TOOL_NAME); - expect(PROCESS_GRANOLA_CALL_SYSTEM_PROMPT).toContain('status: "notes"'); - expect(PROCESS_GRANOLA_CALL_SYSTEM_PROMPT).toContain('status: "no-data"'); -}); - -test("the no-data path still teaches the human what to check next, not a bare failure", () => { - const lowered = PROCESS_GRANOLA_CALL_SYSTEM_PROMPT.toLowerCase(); - expect(lowered).toContain("granola connector"); - expect(lowered).toContain("next steps"); -}); - -test("the definition binds @corbits/granola-tools' declared handle to a tenant-owned granola credential", () => { - const definition = buildProcessGranolaCallWorkflow(INPUT); - expect(definition.credentialBindings).toEqual([...PROCESS_GRANOLA_CALL_CREDENTIAL_BINDINGS]); - expect(PROCESS_GRANOLA_CALL_CREDENTIAL_BINDINGS).toEqual([ - { - package: "@corbits/granola-tools", - handle: "granola", - provider: "granola", - locator: "tenant", - }, - ]); -}); - -test("the definition survives the workflow-asset JSON round-trip", () => { - const definition = buildProcessGranolaCallWorkflow(INPUT); - const revived: unknown = JSON.parse(serializeProcessGranolaCallWorkflow(definition)); - expect(revived).toEqual(definition); -}); - -test("serialization fails loud on a function-valued field, naming its path", () => { - const poisoned = { - id: PROCESS_GRANOLA_CALL_WORKFLOW_ID, - triggers: [{ type: "manual" }], - stepOrder: [PROCESS_GRANOLA_CALL_STEP_ID], - steps: { - [PROCESS_GRANOLA_CALL_STEP_ID]: { - kind: "step", - id: PROCESS_GRANOLA_CALL_STEP_ID, - drainBehavior: "cancel", - agent: { - id: PROCESS_GRANOLA_CALL_STEP_ID, - systemPrompt: PROCESS_GRANOLA_CALL_SYSTEM_PROMPT, - toolFactories: [() => []], - capabilities: [], - inference: { sources: [] }, - }, - }, - }, - } as unknown as WorkflowDefinition; - expect(() => serializeProcessGranolaCallWorkflow(poisoned)).toThrow( - /steps\.process-granola-call\.agent\.toolFactories\[0\]/, - ); -}); - -test("an empty trigger address is rejected", () => { - expect(() => buildProcessGranolaCallWorkflow({ ...INPUT, triggerAddress: "" })).toThrow( - /triggerAddress/, - ); -}); - -test("a non-positive or fractional turn timeout is rejected", () => { - expect(() => buildProcessGranolaCallWorkflow({ ...INPUT, turnTimeoutMs: 0 })).toThrow( - /turnTimeoutMs/, - ); - expect(() => buildProcessGranolaCallWorkflow({ ...INPUT, turnTimeoutMs: 0.5 })).toThrow( - /turnTimeoutMs/, - ); -}); diff --git a/workflows/process-granola-call/tsconfig.json b/workflows/process-granola-call/tsconfig.json deleted file mode 100644 index a8c2b9966d..0000000000 --- a/workflows/process-granola-call/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src", "test"], - "exclude": [] -} diff --git a/workflows/process-granola-call/tsconfig.src.json b/workflows/process-granola-call/tsconfig.src.json deleted file mode 100644 index fc8724305f..0000000000 --- a/workflows/process-granola-call/tsconfig.src.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } -} diff --git a/workflows/reddit-opportunity-scanner/LICENSE b/workflows/reddit-opportunity-scanner/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/workflows/reddit-opportunity-scanner/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/workflows/reddit-opportunity-scanner/README.md b/workflows/reddit-opportunity-scanner/README.md deleted file mode 100644 index 0872d99098..0000000000 --- a/workflows/reddit-opportunity-scanner/README.md +++ /dev/null @@ -1,179 +0,0 @@ -# @corbits/reddit-opportunity-scanner-workflow - -Turns one target website into a short list of scored Reddit -opportunities — posts worth engaging with for outreach or content ideas -— with a search-plan review and one human approval before anything is -finalized (CL-5994, ported from `gtm-workbench`'s -`workflows/reddit-opportunity-scanner`, child of CL-5987). - -## What it does - -One step, one agent, matching every other definition in this catalog. -The system prompt commits it to: - -- **Intake**: a target website URL, named by the sender. Asks for one - if none is given; never scans a site the sender did not name. -- **Scraping**: `firecrawl_scrape` (when available) fetches the site's - content. No Firecrawl tool package exists in this workspace yet (see - "Current limits" below) — this is out of scope for CL-5994, which owns - the Reddit tooling only. -- **Analysis**: infers what the business sells and a candidate list of - Reddit search keywords and subreddits. -- **Search-plan review**: presents the candidates and a business summary - to the sender, and asks them to edit or approve the list. Wait for an - answer; nothing is searched before this. -- **Searching**: `reddit_subreddit_search` (or `reddit_search` for a - keyword with no specific subreddit) per approved pair, via - `@corbits/reddit-tools`. A failed search is reported plainly and - skipped, never fabricated. If nothing is reachable, or nothing is - found, the run does not just reply and stop: it calls - `reddit_opportunity_scanner_report_no_results` - (`./src/finalize-tool.ts`) once, persisting one honest teaching - artifact — what was searched (or that nothing was reachable), which - connector is missing if that's why, and what the sender should do - next — then delivers the same account as its reply. This call needs - no approval: nothing was selected, so there is nothing to confirm. -- **Ranking**: scores every result 1 (weak fit) to 5 (explicit buying - signal or urgent pain), with a short engagement brief per opportunity. -- **Opportunity selection**: presents the ranked opportunities and asks - which to keep. Wait for an answer. -- **Finalizing**: exactly one call to `reddit_opportunity_scanner_finalize` - (`./src/finalize-tool.ts`) with every selected opportunity, gated - behind a single human approval. - -## Gate consolidation - -`gtm-workbench`'s original implementation suspended for a human **twice** -in sequence: a search-plan review and an opportunity-selection review. -That shape came from the OG's step-graph architecture (`awaitSignal` -steps), which had no other way to pause for input — neither was an -independent approval decision. - -Both are ordinary questions a chat-native agent asks in the course of one -conversation — matching the same consolidation -`@corbits/collateral-generation-workflow` (CL-5996) applied to its own -multi-gate OG. This port keeps the same conversation shape but -**consolidates to one real approval gate**: finalizing the opportunities -the sender selected, all at once, in a single -`reddit_opportunity_scanner_finalize` call. - -**Known limit versus the OG**: the OG's selection step was a dedicated -edit UI (`reviewList`) letting the sender pick any subset of scored -opportunities. This port's platform approval gate is binary — approve or -deny the exact set the sender named in conversation — so "selection" is -approve-all-named/deny-all, not per-item editing at the approval step -itself. The per-item pick still happens, just as the preceding -conversational turn rather than at the gate. - -## Approval mechanics - -`reddit_opportunity_scanner_finalize` is declared `approval: "ask"` -(`@intx/agent`'s `ToolDeclaration`), the platform's native tool-approval -gate — identical mechanics to `@corbits/pain-point-collateral-workflow`'s -and `@corbits/collateral-generation-workflow`'s finalize tools. Calling -it suspends the run; a human approves or rejects it from the inbox; on -approval the tool actually runs, on rejection it never runs and the -model gets a synthetic "denied by approver" error, which the system -prompt turns into a calm terminal reply. See `pain-point-collateral`'s -README for the full suspend/resume account, which applies unchanged -here. - -`reddit_opportunity_scanner_report_no_results` (the no-data teaching -artifact, see "What it does" above) carries no approval mark and runs -immediately: a no-data run has nothing the sender selected to confirm, -only an honest account of what the run attempted. - -## Library persistence - -Both tools persist through the sanctioned workflow-artifacts HTTP -surface (`@corbits/artifacts-hub`'s `createWorkflowArtifactRoutes`, -CL-6000) via `createWorkflowArtifact` (`./src/artifact-client.ts`, -duplicated rather than imported from `@corbits/artifact-tools` — see -that file's header for why this installable-data package never imports -another `@corbits/*` package). `reddit_opportunity_scanner_finalize` -persists each selected opportunity sequentially and, on a partial -failure, honestly names how many already persisted rather than losing -or silently claiming the whole batch failed. -`reddit_opportunity_scanner_report_no_results` persists exactly one -artifact. A successful call returns the persisted artifact's id/version -so the delivery pipeline attaches it to the reply as a Library file-part -chip (`packages/chat/src/artifact-delivery.ts`); a failed call surfaces -as an honest `isError: true` result, never a fabricated success. - -**Teaching-artifact kind**: a persisted opportunity's `kind` is fixed at -`"reddit-opportunity-scan"` (`ARTIFACT_KIND`); the no-results teaching -artifact's `kind` is fixed at `"status-note"` (`NO_RESULTS_ARTIFACT_KIND`) -— each is a tool-owned constant, not something the model supplies. -`"status-note"` is the one teaching-artifact kind shared by every -workflow in this catalog, so the Library's kind badge always reads -"Status note" for a no-data run, regardless of which workflow made it — -never the same kind a real opportunity uses. - -## Current limits (read before deploying) - -Two real gaps stand between this definition and a fully live deploy: - -1. **No Firecrawl tool package.** CL-5994's dependency survey found no - corbits/Interchange integration for site scraping. Building one is a - separate, un-scoped piece of work — this port names `firecrawl_scrape` - in its prompt as the tool it would use, and commits to an honest "no - way to scrape" reply until that package exists and is pinned. -2. **No tool-package pin** (CL-5999). No production workflow builder in - this repo threads a caller-supplied `toolPackagePins` onto a built - definition yet. Until it lands, this definition ships with - `tools: []`; `@corbits/reddit-tools` exists, is tested, and is ready - to wire in the moment pinning is built. In practice this means every - search is unreachable on a real deploy today, so the no-data teaching - artifact (naming `scrapecreators`, the connector `@corbits/reddit-tools` - needs) is the path most real runs will hit until pinning lands. - -Neither gap is specific to this workflow; both are pre-existing platform -limits this port surfaces rather than works around. - -## Usage - -```ts -import { - buildRedditOpportunityScannerWorkflow, - serializeRedditOpportunityScannerWorkflow, -} from "@corbits/reddit-opportunity-scanner-workflow"; - -const definition = buildRedditOpportunityScannerWorkflow({ - triggerAddress: "reddit-opportunity-scanner@tenant.example", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 300000, -}); - -const json = serializeRedditOpportunityScannerWorkflow(definition); -``` - -Pin `@corbits/reddit-tools` on the deployment for the agent to reach -real Reddit search results — without a pin, every search call is simply -absent and the run honestly reports Reddit as not reachable (see that -package's README for its credential requirement). - -See [`workflows/README.md`](../README.md#status-note) for what -registration/automatable/seeded mean — this one is `automatable: false` -because its mid-run approval gate is a poor fit for unattended -scheduling, per its original outcome checklist. - -## Deploy from the npm registry - -Once published, deploy this workflow definition to a tenant by name and -version pin: - -``` -POST /api/tenants/:id/workflows/deployments -{ - "source": { "kind": "registry", "registry": "npm" }, - "entry": "./src/index.ts", - "pin": "@corbits/reddit-opportunity-scanner-workflow@0.0.1", - "sourceOfferingIds": ["", ...], - "defaultSourceOfferingId": "" -} -``` - -`entry` is the package's `interchange.workflow` module path; `pin` is -`"@corbits/reddit-opportunity-scanner-workflow@0.0.1"` or a semver range on the same name. The hub -installs, probes, gates, and freezes the definition from the registry -tarball before creating the deployment. diff --git a/workflows/reddit-opportunity-scanner/package.json b/workflows/reddit-opportunity-scanner/package.json deleted file mode 100644 index 9c09f5cb2d..0000000000 --- a/workflows/reddit-opportunity-scanner/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@corbits/reddit-opportunity-scanner-workflow", - "version": "0.0.1", - "description": "Turns one target website into a short list of scored Reddit opportunities, with a search-plan review and one human approval before anything is finalized", - "license": "LGPL-2.1-or-later", - "repository": { - "type": "git", - "url": "https://github.com/corbitsdev/workbench.git", - "directory": "workflows/reddit-opportunity-scanner" - }, - "files": [ - "src", - "LICENSE", - "README.md" - ], - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "arktype": "catalog:" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/workflow": "0.3.0" - }, - "corbits": { - "workflow": { - "assetName": "reddit-opportunity-scanner", - "displayName": "Reddit opportunity scanner", - "automatable": false - } - }, - "interchange": { - "workflow": "./src/index.ts" - } -} diff --git a/workflows/reddit-opportunity-scanner/src/artifact-client.test.ts b/workflows/reddit-opportunity-scanner/src/artifact-client.test.ts deleted file mode 100644 index ba52a441e2..0000000000 --- a/workflows/reddit-opportunity-scanner/src/artifact-client.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { expect, test } from "bun:test"; - -import { createWorkflowArtifact } from "./artifact-client"; - -const CONFIG = { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - runAddress: "run_1@workflow", -}; - -test("posts to the workflow-artifacts endpoint and returns id/version", async () => { - const captured: { url: string; init: RequestInit | undefined } = { - url: "", - init: undefined, - }; - const fetchImpl = (async (input: URL | string, init?: RequestInit) => { - captured.url = String(input); - captured.init = init; - return new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { - status: 201, - }); - }) as unknown as typeof fetch; - - const result = await createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { - title: "Founder asking for an onboarding tool", - kind: "reddit-opportunity-scan", - content: "hello", - }, - ); - - expect(result).toEqual({ id: "art_1", version: 1 }); - expect(captured.url).toBe("https://hub.example.com/api/workflow-artifacts/artifacts"); - const headers = captured.init?.headers as Record; - expect(headers["authorization"]).toBe("Bearer sc-token"); - expect(headers["x-workflow-run-address"]).toBe("run_1@workflow"); -}); - -test("throws on a non-ok HTTP response", async () => { - const fetchImpl = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/500/); -}); - -test("throws when the response doesn't match the expected shape", async () => { - const fetchImpl = (async () => - new Response(JSON.stringify({ data: {} }), { - status: 201, - })) as unknown as typeof fetch; - - await expect( - createWorkflowArtifact( - { ...CONFIG, fetchImpl }, - { title: "Notes", kind: "text", content: "hello" }, - ), - ).rejects.toThrow(/did not match the expected shape/); -}); diff --git a/workflows/reddit-opportunity-scanner/src/artifact-client.ts b/workflows/reddit-opportunity-scanner/src/artifact-client.ts deleted file mode 100644 index 3576aae0c3..0000000000 --- a/workflows/reddit-opportunity-scanner/src/artifact-client.ts +++ /dev/null @@ -1,66 +0,0 @@ -// A minimal client for the sanctioned workflow-artifacts HTTP surface -// (`@corbits/artifacts-hub`'s `createWorkflowArtifactRoutes`, CL-6000). -// Deliberately duplicated from `@corbits/artifact-tools`' `client.ts` -// rather than imported: this package ships as installable data on the -// native workflow contract (see `./index.ts`'s header comment and -// `test/boundary.test.ts`), so its shipped sources import only `@intx/*`, -// `arktype`, and relative specifiers — never another `@corbits/*` -// package. Authenticates with the sidecar's own bearer token plus the -// run's own mailbox address (both already reach a workflow-process -// child's tool env), never a database handle. -// -// Same client `pain-point-collateral` and `collateral-generation` each -// carry their own copy of, per that convention: a shared `@corbits/*` -// import would violate the boundary test above. -import { type } from "arktype"; - -export interface WorkflowArtifactClientConfig { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly runAddress: string; - /** Override for tests; defaults to the global `fetch`. */ - readonly fetchImpl?: typeof fetch; -} - -export type CreateWorkflowArtifactInput = { - readonly title: string; - readonly kind: string; - readonly content: string; -}; - -export type CreatedWorkflowArtifact = { - readonly id: string; - readonly version: number; -}; - -const CreatedWorkflowArtifactResponse = type({ - data: { id: "string", version: "number" }, -}); - -/** Persists one artifact. Throws on any transport, HTTP, or shape failure. */ -export async function createWorkflowArtifact( - config: WorkflowArtifactClientConfig, - input: CreateWorkflowArtifactInput, -): Promise { - const doFetch = config.fetchImpl ?? fetch; - const response = await doFetch(`${config.hubArtifactsUrl}/api/workflow-artifacts/artifacts`, { - method: "POST", - headers: { - authorization: `Bearer ${config.sidecarToken}`, - "x-workflow-run-address": config.runAddress, - "content-type": "application/json", - }, - body: JSON.stringify(input), - }); - if (!response.ok) { - throw new Error(`Workflow artifact create failed: ${response.status} ${response.statusText}`); - } - const body: unknown = await response.json(); - const parsed = CreatedWorkflowArtifactResponse(body); - if (parsed instanceof type.errors) { - throw new Error( - `Workflow artifact create response did not match the expected shape: ${parsed.summary}`, - ); - } - return parsed.data; -} diff --git a/workflows/reddit-opportunity-scanner/src/finalize-tool.test.ts b/workflows/reddit-opportunity-scanner/src/finalize-tool.test.ts deleted file mode 100644 index e06149cdb5..0000000000 --- a/workflows/reddit-opportunity-scanner/src/finalize-tool.test.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { expect, test } from "bun:test"; - -import { - REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL, - REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME, - REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_TOOL_NAME, - buildArtifactPayloads, - buildNoResultsArtifactPayload, - type WorkflowArtifactEnv, -} from "./finalize-tool"; - -test("the finalize call is approval-gated; the no-results report is not", () => { - expect(REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL.definitions).toEqual([ - { name: REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME, approval: "ask" }, - { name: REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_TOOL_NAME }, - ]); -}); - -test("the tool is namespaced under this workflow package, not a shared one", () => { - expect(REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL.id).toBe( - "@corbits/workflow-reddit-opportunity-scanner/finalize", - ); -}); - -test("requires the sanctioned workflow-artifacts env keys", () => { - expect(REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL.requires).toEqual([ - "hubArtifactsUrl", - "sidecarToken", - "address", - ]); -}); - -test("buildArtifactPayloads fixes every opportunity's kind and folds context into the body", () => { - const payloads = buildArtifactPayloads({ - opportunities: [ - { - title: "Founder asking for an onboarding tool", - subreddit: "startups", - url: "https://reddit.com/r/startups/comments/abc123/onboarding", - score: 4, - whyItMatters: "Explicit buying signal, actively comparing tools", - content: "Reply with a plain comparison of options, no pitching.", - }, - ], - }); - expect(payloads).toEqual([ - { - title: "Founder asking for an onboarding tool", - kind: "reddit-opportunity-scan", - content: - "Subreddit: r/startups\n" + - "Score: 4/5\n" + - "URL: https://reddit.com/r/startups/comments/abc123/onboarding\n" + - "\n" + - "Why it matters: Explicit buying signal, actively comparing tools\n" + - "\n" + - "Reply with a plain comparison of options, no pitching.", - }, - ]); -}); - -test("buildNoResultsArtifactPayload honestly reports the attempt, missing connector, and next step", () => { - const payload = buildNoResultsArtifactPayload({ - targetUrl: "https://example.com", - attemptedSearches: ['keyword "onboarding tool" in r/startups: unreachable'], - missingConnectors: ["scrapecreators"], - nextSteps: "Connect the ScrapeCreators connector, then re-run this scan.", - }); - expect(payload.kind).toBe("status-note"); - expect(payload.title).toContain("https://example.com"); - expect(payload.content).toContain('keyword "onboarding tool" in r/startups: unreachable'); - expect(payload.content).toContain("scrapecreators"); - expect(payload.content).toContain("Connect the ScrapeCreators connector, then re-run this scan."); -}); - -test("buildNoResultsArtifactPayload is honest when no search was ever reachable", () => { - const payload = buildNoResultsArtifactPayload({ - targetUrl: "https://example.com", - attemptedSearches: [], - missingConnectors: ["scrapecreators"], - nextSteps: "Connect the ScrapeCreators connector, then re-run this scan.", - }); - expect(payload.content).toMatch(/no searches were reachable/i); -}); - -function testEnv(): WorkflowArtifactEnv { - return { - hubArtifactsUrl: "https://hub.example.com", - sidecarToken: "sc-token", - address: "run_1@workflow", - } as unknown as WorkflowArtifactEnv; -} - -test("finalize persists every selected opportunity as a recognized batch result", async () => { - let call = 0; - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => { - call += 1; - return new Response(JSON.stringify({ data: { id: `art_${call}`, version: 1 } }), { - status: 201, - }); - }) as unknown as typeof fetch; - - try { - const bundle = REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME, - arguments: { - opportunities: [ - { - title: "Founder asking for an onboarding tool", - subreddit: "startups", - url: "https://reddit.com/r/startups/comments/abc123/onboarding", - score: 4, - whyItMatters: "Explicit buying signal", - content: "Reply with a plain comparison of options.", - }, - { - title: "Complaint about slow onboarding", - subreddit: "smallbusiness", - url: "https://reddit.com/r/smallbusiness/comments/def456/slow", - score: 3, - whyItMatters: "Named pain point, no vendor mentioned yet", - content: "Reply with empathy and one concrete tip.", - }, - ], - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content as string) as { - artifacts: { id: string; title: string; persisted: boolean }[]; - }; - expect(parsed.artifacts).toHaveLength(2); - expect(parsed.artifacts.map((a) => a.id)).toEqual(["art_1", "art_2"]); - for (const artifact of parsed.artifacts) { - expect(artifact.persisted).toBe(true); - } - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("finalize reports a partial failure honestly, naming how many opportunities already persisted", async () => { - let call = 0; - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => { - call += 1; - if (call === 1) { - return new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), { status: 201 }); - } - return new Response("nope", { status: 500 }); - }) as unknown as typeof fetch; - - try { - const bundle = REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME, - arguments: { - opportunities: [ - { - title: "Founder asking for an onboarding tool", - subreddit: "startups", - url: "https://reddit.com/r/startups/comments/abc123/onboarding", - score: 4, - whyItMatters: "Explicit buying signal", - content: "Reply with a plain comparison of options.", - }, - { - title: "Complaint about slow onboarding", - subreddit: "smallbusiness", - url: "https://reddit.com/r/smallbusiness/comments/def456/slow", - score: 3, - whyItMatters: "Named pain point, no vendor mentioned yet", - content: "Reply with empathy and one concrete tip.", - }, - ], - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(true); - expect(result.content).toContain("persisting 1 of 2"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("finalize rejects an empty opportunities array without throwing", async () => { - const bundle = REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME, - arguments: { opportunities: [] }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("at least one selected opportunity"); -}); - -test("finalize rejects malformed arguments without throwing", async () => { - const bundle = REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME, - arguments: {}, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); - -test("finalize rejects a score outside 1-5 without throwing", async () => { - const bundle = REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME, - arguments: { - opportunities: [ - { - title: "Off-scale score", - subreddit: "startups", - url: "https://reddit.com/r/startups/comments/xyz/off-scale", - score: 9, - whyItMatters: "n/a", - content: "n/a", - }, - ], - }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); - -test("the no-results report persists one honest teaching artifact on real invocation", async () => { - globalThis.fetch = (async () => - new Response(JSON.stringify({ data: { id: "art_teach", version: 1 } }), { - status: 201, - })) as unknown as typeof fetch; - const originalFetch = globalThis.fetch; - - try { - const bundle = REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_TOOL_NAME, - arguments: { - targetUrl: "https://example.com", - attemptedSearches: ['keyword "onboarding tool" in r/startups: unreachable'], - missingConnectors: ["scrapecreators"], - nextSteps: "Connect the ScrapeCreators connector, then re-run this scan.", - }, - }, - new AbortController().signal, - ); - - expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content as string) as { - id: string; - title: string; - kind: string; - persisted: boolean; - }; - expect(parsed.persisted).toBe(true); - expect(parsed.id).toBe("art_teach"); - expect(parsed.kind).toBe("status-note"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("the no-results report surfaces a failed persist honestly", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => new Response("nope", { status: 500 })) as unknown as typeof fetch; - - try { - const bundle = REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_TOOL_NAME, - arguments: { - targetUrl: "https://example.com", - attemptedSearches: [], - missingConnectors: ["scrapecreators"], - nextSteps: "Connect the ScrapeCreators connector.", - }, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Failed to persist"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("the no-results report rejects malformed arguments without throwing", async () => { - const bundle = REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_TOOL_NAME, - arguments: {}, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid arguments"); -}); - -test("run rejects a call for an unrecognized tool name without throwing", async () => { - const bundle = REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL(testEnv()); - const result = await bundle.run( - { - id: "call_1", - name: "not_a_real_tool", - arguments: {}, - }, - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toContain("not_a_real_tool"); -}); diff --git a/workflows/reddit-opportunity-scanner/src/finalize-tool.ts b/workflows/reddit-opportunity-scanner/src/finalize-tool.ts deleted file mode 100644 index c610d3ae3c..0000000000 --- a/workflows/reddit-opportunity-scanner/src/finalize-tool.ts +++ /dev/null @@ -1,350 +0,0 @@ -// The two pieces of this workflow that reach the Library engine: finalizing -// the set of Reddit opportunities the sender picked to keep, and — on an -// honest no-data run — reporting what was attempted so the sender isn't -// left with silence. Kept inside the workflow package rather than a shared -// tool package — same "workflow-specific logic lives in the definition" -// convention `pain-point-collateral`'s and `collateral-generation`'s -// finalize-tool.ts files established — neither is a reusable integration -// on its own. -// -// Gate consolidation (CL-5994): the OG `gtm-workbench` implementation -// suspended for a human twice — once to edit/approve the candidate -// keyword+subreddit search plan, and once to pick which ranked -// opportunities to keep. The first of those is an ordinary conversational -// turn here (present the plan, wait for the sender's edits or approval — -// see `./index.ts`'s system prompt), the same consolidation -// `collateral-generation` (CL-5996) applied to its own multi-gate OG. The -// opportunity pick still happens in conversation too, but this port keeps -// exactly one real platform approval gate on top of it: finalizing the -// opportunities the sender actually chose to keep, all at once, as one -// call to `reddit_opportunity_scanner_finalize` — so a run can never -// silently persist something the sender never confirmed. -// -// Approval mechanics: `reddit_opportunity_scanner_finalize` is declared -// `approval: "ask"`, the platform's native tool-approval gate — identical -// mechanics to `pain-point-collateral`'s and `collateral-generation`'s -// finalize tools (see that package's `finalize-tool.ts` header for the -// full suspend/resume account, which applies unchanged here). -// `reddit_opportunity_scanner_report_no_results` carries no approval mark: -// a no-data run has no sender-selected content to confirm, only an honest -// account of what the workflow attempted, so gating it behind a human -// decision would just add friction with nothing to decide. -// -// This is one `defineTool` factory declaring two tool names (the shape -// `ToolDeclaration`/`ToolBundle` both support — see -// `@intx/agent/src/tool.ts`) rather than two separate exports, -// because both share the same persistence dependency -// (`./artifact-client.ts`) and env requirements. -// -// Persistence (CL-6000): `run` below calls `createWorkflowArtifact` -// (`./artifact-client.ts`, duplicated from `@corbits/artifact-tools`' -// client rather than imported — see that file's header for why this -// installable-data package never imports another `@corbits/*` package) -// against the sanctioned workflow-artifacts HTTP surface — authenticated -// with the sidecar's own bearer token and this run's own mailbox -// address, both already present on `env`, never a database handle. -// `reddit_opportunity_scanner_finalize` persists each opportunity -// sequentially and stops at the first failure, honestly naming how many -// already persisted (the same partial-failure convention -// `collateral-generation`'s finalize tool uses for its own batch). -// `reddit_opportunity_scanner_report_no_results` persists exactly one -// artifact. A successful call returns the persisted artifact's id/version -// (single object, or `{ artifacts: [...] }` for the batch) so the delivery -// pipeline can reference it with a file part -// (`packages/chat/src/artifact-delivery.ts`); a failed call surfaces as an -// honest `isError: true` result rather than a fabricated success. - -import { type } from "arktype"; -import { defineTool, type BaseEnv } from "@intx/agent"; -import { createWorkflowArtifact } from "./artifact-client"; - -export const REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME = "reddit_opportunity_scanner_finalize"; - -export const REDDIT_OPPORTUNITY_SCANNER_FINALIZE_DESCRIPTION = - "Finalizes the set of human-selected Reddit opportunities from one run, pending a single human approval, and persists each as a Library artifact."; - -export const REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_TOOL_NAME = - "reddit_opportunity_scanner_report_no_results"; - -export const REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_DESCRIPTION = - "Persists one honest teaching artifact for a run that found no opportunities: what was searched, which connector (if any) is missing, and what the sender should do next. Not approval-gated — nothing was selected, so there is nothing for a human to confirm."; - -const ARTIFACT_KIND = "reddit-opportunity-scan"; -// "status-note" is the one teaching-artifact kind shared by every -// workflow in this catalog (see each workflow's README, "Teaching- -// artifact kind" section) — a run's Library kind badge always reads -// "Status note" for a no-data run, regardless of which workflow made it. -const NO_RESULTS_ARTIFACT_KIND = "status-note"; - -const RedditOpportunity = type({ - /** Short, human-facing title for the opportunity. */ - title: "string > 0", - /** Subreddit name, without "r/", e.g. "startups". */ - subreddit: "string > 0", - /** Permalink to the Reddit post. */ - url: "string > 0", - /** 1 (weak fit) to 5 (strong buying signal or urgent pain). */ - score: "1 <= number <= 5", - /** Why this post is worth engaging with. */ - whyItMatters: "string > 0", - /** The drafted engagement brief: suggested angle, talking points, etc. */ - content: "string > 0", -}); - -const FinalizeArgs = type({ - opportunities: RedditOpportunity.array(), -}); - -export type FinalizeArgs = typeof FinalizeArgs.infer; -export type RedditOpportunity = typeof RedditOpportunity.infer; - -/** - * `attemptedSearches` and `missingConnectors` are free-text/id lists the - * model fills in from what it actually tried this run — never invented. - * `missingConnectors` may be empty: a run can reach zero results with - * every connector connected and every search reachable. - */ -const NoResultsReportArgs = type({ - /** The target site URL the sender named for this run. */ - targetUrl: "string > 0", - /** One line per keyword/subreddit pair actually attempted, or empty. */ - attemptedSearches: "string[]", - /** Connector ids (e.g. "scrapecreators") this run could not reach, if any. */ - missingConnectors: "string[]", - /** Plain-language advice for what the sender should do next. */ - nextSteps: "string > 0", -}); - -export type NoResultsReportArgs = typeof NoResultsReportArgs.infer; - -export type ArtifactPayload = { - title: string; - kind: string; - content: string; -}; - -/** - * The payload `@corbits/artifacts`' `artifact_create` tool expects - * (`{ title, kind, content }`) for one opportunity — `kind` is fixed at - * `"reddit-opportunity-scan"` so every persisted opportunity from this - * workflow is recognizable as one, regardless of subreddit or score. - */ -export function buildArtifactPayloads(args: FinalizeArgs): readonly ArtifactPayload[] { - return args.opportunities.map((opportunity) => ({ - title: opportunity.title, - kind: ARTIFACT_KIND, - content: [ - `Subreddit: r/${opportunity.subreddit}`, - `Score: ${opportunity.score}/5`, - `URL: ${opportunity.url}`, - "", - `Why it matters: ${opportunity.whyItMatters}`, - "", - opportunity.content, - ].join("\n"), - })); -} - -/** - * The payload for a no-data run's teaching artifact: what was searched - * (or that nothing was reachable), which connector is missing, and what - * the sender should do next. Every line is drawn from `args` — nothing - * here is invented sample data. - */ -export function buildNoResultsArtifactPayload(args: NoResultsReportArgs): ArtifactPayload { - const searchesSection = - args.attemptedSearches.length > 0 - ? ["Searches attempted:", ...args.attemptedSearches.map((search) => `- ${search}`)].join("\n") - : "No searches were reachable — this run could not reach Reddit before this point."; - const connectorsSection = - args.missingConnectors.length > 0 - ? `Missing connector(s): ${args.missingConnectors.join(", ")}` - : "No connector is missing; every attempted search was reachable and simply found nothing."; - return { - title: `Reddit opportunity scan: no results for ${args.targetUrl}`, - kind: NO_RESULTS_ARTIFACT_KIND, - content: [ - `Target: ${args.targetUrl}`, - "", - searchesSection, - "", - connectorsSection, - "", - `Next steps: ${args.nextSteps}`, - ].join("\n"), - }; -} - -/** - * The env this tool needs beyond `BaseEnv`: the sanctioned - * workflow-artifacts credential trio, populated for every workflow step - * (`apps/sidecar/src/workflow-substrate-factory/step-env.ts`) — the same - * three keys `@corbits/artifact-tools`' read-side bundle declares. - */ -export interface WorkflowArtifactEnv extends BaseEnv { - readonly hubArtifactsUrl: string; - readonly sidecarToken: string; - readonly address: string; -} - -function artifactClientConfig(env: WorkflowArtifactEnv) { - return { - hubArtifactsUrl: env.hubArtifactsUrl, - sidecarToken: env.sidecarToken, - runAddress: env.address, - }; -} - -/** - * `defineTool`'s env-DI factory shape. Needs the sanctioned - * workflow-artifacts credential trio beyond `BaseEnv`. - */ -export const REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL = defineTool({ - id: "@corbits/workflow-reddit-opportunity-scanner/finalize", - requires: ["hubArtifactsUrl", "sidecarToken", "address"], - definitions: [ - { name: REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME, approval: "ask" }, - { name: REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_TOOL_NAME }, - ], - factory: (env) => ({ - definitions: [ - { - name: REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME, - description: REDDIT_OPPORTUNITY_SCANNER_FINALIZE_DESCRIPTION, - inputSchema: { - type: "object", - properties: { - opportunities: { - type: "array", - items: { - type: "object", - properties: { - title: { type: "string" }, - subreddit: { type: "string" }, - url: { type: "string" }, - score: { type: "number" }, - whyItMatters: { type: "string" }, - content: { type: "string" }, - }, - required: ["title", "subreddit", "url", "score", "whyItMatters", "content"], - }, - }, - }, - required: ["opportunities"], - }, - }, - { - name: REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_TOOL_NAME, - description: REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_DESCRIPTION, - inputSchema: { - type: "object", - properties: { - targetUrl: { type: "string" }, - attemptedSearches: { - type: "array", - items: { type: "string" }, - }, - missingConnectors: { - type: "array", - items: { type: "string" }, - }, - nextSteps: { type: "string" }, - }, - required: ["targetUrl", "attemptedSearches", "missingConnectors", "nextSteps"], - }, - }, - ], - run: async (call) => { - if (call.name === REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME) { - const parsed = FinalizeArgs(call.arguments); - if (parsed instanceof type.errors) { - return { - callId: call.id, - isError: true, - content: `Invalid arguments for ${REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME}: ${parsed.summary}`, - }; - } - if (parsed.opportunities.length === 0) { - return { - callId: call.id, - isError: true, - content: `${REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME} requires at least one selected opportunity`, - }; - } - const artifacts = buildArtifactPayloads(parsed); - const persisted: { - id: string; - version: number; - title: string; - kind: string; - persisted: true; - }[] = []; - for (const artifact of artifacts) { - try { - const created = await createWorkflowArtifact(artifactClientConfig(env), artifact); - persisted.push({ - id: created.id, - version: created.version, - title: artifact.title, - kind: artifact.kind, - persisted: true, - }); - } catch (err) { - return { - callId: call.id, - isError: true, - content: `Failed to persist "${artifact.title}" as a Library artifact after persisting ${persisted.length} of ${artifacts.length} opportunit${artifacts.length === 1 ? "y" : "ies"}: ${ - err instanceof Error ? err.message : String(err) - }`, - }; - } - } - return { - callId: call.id, - isError: false, - content: JSON.stringify({ artifacts: persisted }), - }; - } - - if (call.name === REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_TOOL_NAME) { - const parsed = NoResultsReportArgs(call.arguments); - if (parsed instanceof type.errors) { - return { - callId: call.id, - isError: true, - content: `Invalid arguments for ${REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_TOOL_NAME}: ${parsed.summary}`, - }; - } - const artifact = buildNoResultsArtifactPayload(parsed); - try { - const created = await createWorkflowArtifact(artifactClientConfig(env), artifact); - return { - callId: call.id, - isError: false, - content: JSON.stringify({ - id: created.id, - version: created.version, - title: artifact.title, - kind: artifact.kind, - persisted: true, - }), - }; - } catch (err) { - return { - callId: call.id, - isError: true, - content: `Failed to persist "${artifact.title}" as a Library artifact: ${ - err instanceof Error ? err.message : String(err) - }`, - }; - } - } - - return { - callId: call.id, - isError: true, - content: `Unknown tool: ${call.name}`, - }; - }, - }), -}); diff --git a/workflows/reddit-opportunity-scanner/src/index.ts b/workflows/reddit-opportunity-scanner/src/index.ts deleted file mode 100644 index 5f94f5597f..0000000000 --- a/workflows/reddit-opportunity-scanner/src/index.ts +++ /dev/null @@ -1,263 +0,0 @@ -// The reddit-opportunity-scanner workflow (CL-5994, ported from -// `gtm-workbench`'s `workflows/reddit-opportunity-scanner`, child of -// CL-5987): turns one target website into a short list of scored Reddit -// opportunities worth engaging with, with a search-plan review and one -// human approval before anything is persisted. -// -// One step, one agent — matching the shape every other definition in -// this catalog commits to (`step`/`defineAgent`, mail-triggered, tools -// arrive as packages on the deploy, never inlined here). All -// workflow-specific logic — how the intake URL becomes a business -// summary, what counts as a good keyword/subreddit pair, how results get -// ranked into scored opportunities, the exact wording of an honest -// "nothing to scan" reply — lives in this definition's own system prompt -// and its one workflow-local tool (`./finalize-tool.ts`), not a separate -// tool package. The one genuinely reusable piece, searching Reddit, is -// `reddit_search` / `reddit_subreddit_search` from `@corbits/reddit-tools` -// (via ScrapeCreators — see that package's README for why Reddit's own -// API is not used). -// -// Gate consolidation (same convention `collateral-generation`, CL-5996, -// established): the OG `gtm-workbench` implementation suspended for a -// human twice — a search-plan review (edit/approve the candidate -// keyword+subreddit list) and an opportunity-selection review (pick which -// scored opportunities to keep). Neither is, on its own, the platform's -// approval-suspend primitive: they are ordinary questions a chat-native -// agent asks in the course of one conversation. This port asks both in -// the same conversation (see the system prompt below) and keeps exactly -// one real approval gate: finalizing the opportunities the sender chose -// to keep, all at once, in a single -// `reddit_opportunity_scanner_finalize` call. See `./finalize-tool.ts`'s -// header for the full account, including the known "selection is -// approve-all-proposed/deny-all, not per-item editing" limit this -// consolidation carries versus the OG's dedicated selection UI. -// -// Site scraping (Firecrawl) is out of scope for this port: CL-5994's -// dependency survey found no corbits/Interchange Firecrawl tool package, -// and building one is a separate concern from the Reddit tooling this -// ticket owns. The prompt below names `firecrawl_scrape` as the tool this -// workflow would use once that package exists, and commits to an honest -// "no way to scrape" reply in the meantime — the same shape as every -// other honestly-degraded source in this catalog. -// -// Known platform gap (see `finalize-tool.ts`'s header for the full -// account, and this package's README for the reader-facing version): no -// production workflow builder in this repo threads a caller-supplied -// `toolPackagePins` onto a built definition yet (CL-5999, -// `docs/AGENTS-PAGE.md`), the same gap `@corbits/morning-brief-workflow` -// (CL-5993), `@corbits/pain-point-collateral-workflow` (CL-5995), and -// `@corbits/collateral-generation-workflow` (CL-5996) document. Until -// that lands, this definition's step ships with `tools: []`, matching -// every other definition in this catalog — its system prompt commits it -// to saying plainly when it has no way to scrape a site, search Reddit, -// or finalize opportunities, rather than inventing scraped content, -// search results, or an approval that never happened. -// -// This package is installable data. It imports only published platform -// packages, and nothing imports it statically: a host publishes the -// serialized definition as a workflow asset and deploys it through the -// platform's deploy machinery; the execution host materializes it at -// runtime from the deploy alone. - -import { defineAgent } from "@intx/agent"; -import type { InferencePreference } from "@intx/agent"; -import { defineWorkflow, step } from "@intx/workflow"; -import type { WorkflowDefinition } from "@intx/workflow"; - -import { - REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME, - REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_TOOL_NAME, -} from "./finalize-tool"; - -export const REDDIT_OPPORTUNITY_SCANNER_WORKFLOW_ID = "wf_reddit_opportunity_scanner"; -export const REDDIT_OPPORTUNITY_SCANNER_STEP_ID = "reddit-opportunity-scanner"; - -export const REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT = [ - "You turn one target website into a short list of scored Reddit " + - "opportunities — posts worth engaging with for outreach or content " + - "ideas — with a search-plan review and exactly one human approval " + - "before anything is finalized.", - - "Intake: the sender's message names one target website URL. If no URL " + - "is given, ask for one and wait for the answer. Never guess a URL " + - "or scan a site the sender did not name.", - - "Scraping: use firecrawl_scrape (when available) to fetch the site's " + - "content. If firecrawl_scrape is not available, or the fetch fails, " + - "say so plainly in one sentence and stop — never fabricate what the " + - "site sells.", - - "Analysis: from the scraped content, infer what the business sells, " + - "its likely customers, and a candidate list of Reddit search " + - "keywords and subreddits worth searching for opportunities.", - - "Search-plan review: present the candidate keywords, subreddits, and a " + - "one-paragraph summary of what the business sells to the sender, and " + - "ask them to edit or approve the list before anything is searched. " + - "Wait for their answer; never search Reddit before this is approved.", - - "Searching: for each approved subreddit, call reddit_subreddit_search " + - "with the matching keyword; for any approved keyword with no " + - "specific subreddit, call reddit_search instead. A search call that " + - "errors means that pair is not reachable right now — say so plainly " + - "and continue with the others; never fail the whole run because one " + - "search failed. If every search is unreachable, or none return any " + - "results, call " + - REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_TOOL_NAME + - " exactly once with an honest account: the target URL, one line per " + - "keyword/subreddit pair actually attempted (or an empty list if none " + - "were reachable), the connector id this run could not reach if that " + - 'is why nothing came back (e.g. "scrapecreators", the connector ' + - "reddit_search and reddit_subreddit_search need — leave this empty " + - "if every search WAS reachable and simply found nothing), and a " + - "plain next step for the sender (connect the missing connector, or " + - "try a different target URL). This call needs no approval — nothing " + - "was selected, so there is nothing to confirm. After it returns, " + - "give the sender the same honest account as your reply; never " + - "invent posts to fill the gap, and never leave the sender with a " + - "reply and nothing else.", - - "Ranking: score every result found from 1 (weak fit, off-topic, or " + - "outdated) to 5 (an explicit buying signal or urgent, specific pain " + - "point), and draft a short engagement brief for each — why it " + - "matters and a suggested angle for replying. Keep only opportunities " + - "that are a genuine fit; never pad the list with weak or off-topic " + - "results just to have more to show.", - - "Opportunity selection: present the ranked opportunities to the " + - "sender — title, subreddit, score, and why it matters for each — " + - "and ask which to keep. Wait for their answer.", - - `Finalizing: call ${REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME} ` + - "exactly once with the full set of opportunities the sender chose " + - "to keep. This call requires a human's approval before it " + - "completes. Never call it more than once per run, with an empty " + - "set, or with an opportunity the sender did not select.", - - "If the call succeeds, present the finalized opportunities as your " + - "reply — each title, subreddit, score, and brief, clearly formatted " + - "— with no commentary about the approval mechanism itself. If the " + - "call is denied, reply with one calm, plain sentence that the " + - "opportunities were not approved and no action was taken; never " + - "present a denial as an error, and never apologize as if something " + - "broke.", -].join("\n\n"); - -/** - * Everything the definition needs that is per-deployment data. The - * trigger address names a specific deployment's inbox — for this - * workflow, the address a human messages to start a run — so a - * definition built here is per-deployment by construction. - */ -export interface RedditOpportunityScannerWorkflowInput { - /** The deployment's mail address; each inbound mail is one run. */ - readonly triggerAddress: string; - /** Provider/model preferences, in order; resolved at deploy time. */ - readonly inferencePreferences: readonly InferencePreference[]; - /** Per-turn timeout in milliseconds, enforced on the single step. */ - readonly turnTimeoutMs: number; -} - -/** - * Builds the reddit-opportunity-scanner definition. Exactly one step, - * matching the shape every other definition in this repo commits to. - * - * The step always sets an explicit `timeout` — the singular `agent:` - * shorthand sets none, and a wedged inference call (or one parked on an - * unresolved approval past a sane bound) would otherwise hang a run - * forever. Tools are never inlined on the definition: they arrive as - * packages on the deploy, keeping the definition pure data. - */ -export function buildRedditOpportunityScannerWorkflow( - input: RedditOpportunityScannerWorkflowInput, -): WorkflowDefinition { - if (input.triggerAddress === "") { - throw new Error("buildRedditOpportunityScannerWorkflow requires a non-empty triggerAddress"); - } - if (!Number.isInteger(input.turnTimeoutMs) || input.turnTimeoutMs <= 0) { - throw new Error( - "buildRedditOpportunityScannerWorkflow requires turnTimeoutMs to be a positive integer", - ); - } - return defineWorkflow({ - id: REDDIT_OPPORTUNITY_SCANNER_WORKFLOW_ID, - trigger: { type: "mail", to: input.triggerAddress }, - steps: { - [REDDIT_OPPORTUNITY_SCANNER_STEP_ID]: step({ - agent: defineAgent({ - id: REDDIT_OPPORTUNITY_SCANNER_STEP_ID, - description: - "Scans a target website's likely Reddit audience for " + - "engagement-worthy opportunities and finalizes only the " + - "ones a human selects and approves", - systemPrompt: REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT, - tools: [], - capabilities: [], - inference: { sources: input.inferencePreferences }, - }), - timeout: input.turnTimeoutMs, - }), - }, - }); -} - -/** - * Serializes a definition to the JSON a workflow asset carries. The - * definition must survive the asset round-trip byte-faithfully, so - * anything JSON would silently drop or mangle — functions, undefined, - * symbols, bigints, non-finite numbers, class instances — is a loud - * error naming the offending path instead of a corrupted asset. - */ -export function serializeRedditOpportunityScannerWorkflow(definition: WorkflowDefinition): string { - assertJsonPortable(definition, "definition"); - return JSON.stringify(definition); -} - -function assertJsonPortable(value: unknown, path: string): void { - if (value === null) return; - switch (typeof value) { - case "string": - case "boolean": - return; - case "number": - if (!Number.isFinite(value)) { - throw new Error(`${path} is a non-finite number; JSON drops it`); - } - return; - case "object": - break; - default: - throw new Error( - `${path} is a ${typeof value}, which does not survive JSON ` + "serialization", - ); - } - if (Array.isArray(value)) { - value.forEach((element, index) => { - assertJsonPortable(element, `${path}[${index}]`); - }); - return; - } - const proto: unknown = Object.getPrototypeOf(value); - if (proto !== Object.prototype && proto !== null) { - throw new Error(`${path} is a non-plain object; JSON would flatten it lossily`); - } - for (const [key, entry] of Object.entries(value)) { - assertJsonPortable(entry, `${path}.${key}`); - } -} - -export { - REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL, - REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME, - REDDIT_OPPORTUNITY_SCANNER_FINALIZE_DESCRIPTION, - REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_TOOL_NAME, - REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_DESCRIPTION, - buildArtifactPayloads, - buildNoResultsArtifactPayload, -} from "./finalize-tool"; -export type { - ArtifactPayload, - FinalizeArgs, - NoResultsReportArgs, - RedditOpportunity, -} from "./finalize-tool"; diff --git a/workflows/reddit-opportunity-scanner/test/boundary.test.ts b/workflows/reddit-opportunity-scanner/test/boundary.test.ts deleted file mode 100644 index efdaee7c44..0000000000 --- a/workflows/reddit-opportunity-scanner/test/boundary.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -// This package is installable data on the native workflow contract: -// its shipped sources import only published platform packages, so the -// source tree plus the package manifest deploys on any Interchange -// instance without a wrapper contract. Tool packages this workflow -// expects at deploy time (@corbits/reddit-tools) are pinned by the -// deployer, never imported here — see src/index.ts's header comment. - -import { readFile, readdir } from "node:fs/promises"; -import path from "node:path"; -import { expect, test } from "bun:test"; - -const ALLOWED_IMPORT_PREFIXES = ["@intx/", "arktype", "./", "../"]; - -async function listFiles(dir: string): Promise { - const entries = await readdir(dir, { withFileTypes: true }); - const files: string[] = []; - for (const entry of entries) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...(await listFiles(full))); - } else { - files.push(full); - } - } - return files; -} - -async function shippedFiles(): Promise { - const packageRoot = path.join(import.meta.dir, ".."); - return [ - ...(await listFiles(path.join(packageRoot, "src"))), - path.join(packageRoot, "package.json"), - ]; -} - -test("shipped sources import only published platform packages", async () => { - const importPattern = /from\s+"([^"]+)"/g; - const violations: string[] = []; - for (const file of await shippedFiles()) { - if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue; - const content = await readFile(file, "utf8"); - for (const match of content.matchAll(importPattern)) { - const specifier = match[1] ?? ""; - const allowed = ALLOWED_IMPORT_PREFIXES.some((prefix) => specifier.startsWith(prefix)); - if (!allowed) { - violations.push(`${path.basename(file)}: ${specifier}`); - } - } - } - expect(violations).toEqual([]); -}); diff --git a/workflows/reddit-opportunity-scanner/test/definition.test.ts b/workflows/reddit-opportunity-scanner/test/definition.test.ts deleted file mode 100644 index f0c06b6740..0000000000 --- a/workflows/reddit-opportunity-scanner/test/definition.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -// Tests for this package's own contract: the shape our factory -// commits to, its serialization guarantees, and its boundary. The -// platform's own normalization and validation are its business, not -// re-proven here. - -import { expect, test } from "bun:test"; -import type { StepPrimitive, WorkflowDefinition } from "@intx/workflow"; - -import { - REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME, - REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_TOOL_NAME, - REDDIT_OPPORTUNITY_SCANNER_STEP_ID, - REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT, - REDDIT_OPPORTUNITY_SCANNER_WORKFLOW_ID, - buildRedditOpportunityScannerWorkflow, - serializeRedditOpportunityScannerWorkflow, -} from "../src/index"; - -const INPUT = { - triggerAddress: "ins_dep000000000000@example.test", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 300000, -} as const; - -function scannerStep(definition: WorkflowDefinition): StepPrimitive { - const primitive = definition.steps[REDDIT_OPPORTUNITY_SCANNER_STEP_ID]; - if (primitive === undefined || primitive.kind !== "step") { - throw new Error(`definition has no step primitive named ${REDDIT_OPPORTUNITY_SCANNER_STEP_ID}`); - } - return primitive; -} - -test("the definition has exactly one step", () => { - const definition = buildRedditOpportunityScannerWorkflow(INPUT); - expect(definition.stepOrder).toEqual([REDDIT_OPPORTUNITY_SCANNER_STEP_ID]); - expect(Object.keys(definition.steps)).toEqual([REDDIT_OPPORTUNITY_SCANNER_STEP_ID]); -}); - -test("the step carries an explicit per-turn timeout", () => { - const definition = buildRedditOpportunityScannerWorkflow(INPUT); - expect(scannerStep(definition).timeout).toBe(INPUT.turnTimeoutMs); -}); - -test("the workflow is triggered by mail to the given deployment address", () => { - const definition = buildRedditOpportunityScannerWorkflow(INPUT); - expect(definition.id).toBe(REDDIT_OPPORTUNITY_SCANNER_WORKFLOW_ID); - expect(definition.triggers).toEqual([{ type: "mail", to: INPUT.triggerAddress }]); -}); - -test("the agent carries the fixed prompt, the preferences, and inlines no tools", () => { - const agent = scannerStep(buildRedditOpportunityScannerWorkflow(INPUT)).agent; - expect(agent.systemPrompt).toBe(REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT); - expect(agent.inference.sources).toEqual([...INPUT.inferencePreferences]); - // Tools arrive as packages on the deploy, never inlined here: an - // inline factory is a function-valued field the asset cannot carry. - expect(agent.toolFactories).toEqual([]); -}); - -test("the system prompt names the exact approval-gated finalize tool", () => { - expect(REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT).toContain( - REDDIT_OPPORTUNITY_SCANNER_FINALIZE_TOOL_NAME, - ); -}); - -test("the system prompt names the real Reddit and scrape tools by their exact names", () => { - expect(REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT).toContain("firecrawl_scrape"); - expect(REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT).toContain("reddit_search"); - expect(REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT).toContain("reddit_subreddit_search"); -}); - -test("the system prompt commits to an honest no-scrape failure, not fabricated site content", () => { - expect(REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT).toMatch(/never fabricate what the site sells/i); -}); - -test("the system prompt requires the search plan to be reviewed before any search runs", () => { - expect(REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT).toMatch( - /never search reddit before this is approved/i, - ); -}); - -test("the system prompt names the honest no-data teaching-artifact tool and its missing connector", () => { - expect(REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT).toContain( - REDDIT_OPPORTUNITY_SCANNER_REPORT_NO_RESULTS_TOOL_NAME, - ); - expect(REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT).toContain("scrapecreators"); - expect(REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT).toMatch(/needs no approval/i); -}); - -test("the system prompt collapses opportunity selection into one approval, not a second gate", () => { - expect(REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT).toMatch( - /exactly once with the full set of opportunities the sender chose/i, - ); -}); - -test("the system prompt commits to a calm terminal reply on denial, not an error", () => { - expect(REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT).toMatch(/not approved/i); - expect(REDDIT_OPPORTUNITY_SCANNER_SYSTEM_PROMPT).toMatch(/never present a denial as an error/i); -}); - -test("the definition survives the workflow-asset JSON round-trip", () => { - const definition = buildRedditOpportunityScannerWorkflow(INPUT); - const revived: unknown = JSON.parse(serializeRedditOpportunityScannerWorkflow(definition)); - expect(revived).toEqual(definition); -}); - -test("an empty trigger address is rejected", () => { - expect(() => buildRedditOpportunityScannerWorkflow({ ...INPUT, triggerAddress: "" })).toThrow( - /triggerAddress/, - ); -}); - -test("a non-positive or fractional turn timeout is rejected", () => { - expect(() => buildRedditOpportunityScannerWorkflow({ ...INPUT, turnTimeoutMs: 0 })).toThrow( - /turnTimeoutMs/, - ); - expect(() => buildRedditOpportunityScannerWorkflow({ ...INPUT, turnTimeoutMs: 0.5 })).toThrow( - /turnTimeoutMs/, - ); -}); diff --git a/workflows/reddit-opportunity-scanner/tsconfig.json b/workflows/reddit-opportunity-scanner/tsconfig.json deleted file mode 100644 index a8c2b9966d..0000000000 --- a/workflows/reddit-opportunity-scanner/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src", "test"], - "exclude": [] -} diff --git a/workflows/reddit-opportunity-scanner/tsconfig.src.json b/workflows/reddit-opportunity-scanner/tsconfig.src.json deleted file mode 100644 index fc8724305f..0000000000 --- a/workflows/reddit-opportunity-scanner/tsconfig.src.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } -} diff --git a/workflows/workbench-digest/LICENSE b/workflows/workbench-digest/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/workflows/workbench-digest/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/workflows/workbench-digest/README.md b/workflows/workbench-digest/README.md deleted file mode 100644 index a187dde875..0000000000 --- a/workflows/workbench-digest/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# @corbits/workbench-digest-workflow - -A single mail-triggered step meant to be deployed against a workbench's -own timeline address. `@corbits/cron` delivers a tick mail on the -digest's schedule (daily at 09:00 UTC), and each tick relays a -deterministic summary line straight back into the workbench, mirroring -how a workbench host's reply becomes a workbench mail post (see -`packages/chat/src/workbench-workflow.ts` and `platform-adapter.ts`). - -## What it does - -One step, one agent, a system prompt that instructs relaying the -trigger's exact text with no additions, no commentary, no formatting of -its own. This package never computes the digest line itself, so its -output is exactly as deterministic as its input. - -## Cost profile - -**Pinned to `noop-inference` (default, zero cost):** deployed the same -way as `@corbits/heartbeat-workflow` — `inferencePreferences` pointed at -the hub's `noop-inference` endpoint (see -`packages/chat/src/noop-inference.ts`'s header comment). Every run resolves against a -constant, locally served SSE response, so running this on a tight -schedule costs nothing. The trade-off: `noop-inference` always replies -with empty text by design (see its header comment), so under this pin -no visible digest line is actually posted — the run still proves the -mail-trigger and workbench-mail-posting paths stay alive, just without -visible output. - -**Pinned to a real catalog model:** point `inferencePreferences` at any -configured model instead, and the relayed digest line is posted for -real, at that model's ordinary per-turn cost (a single short completion -per trigger — a few dozen tokens, not a real "reasoning" turn). - -## Usage - -```ts -import { - buildWorkbenchDigestWorkflow, - serializeWorkbenchDigestWorkflow, -} from "@corbits/workbench-digest-workflow"; - -const definition = buildWorkbenchDigestWorkflow({ - triggerAddress: "ins_dep000000000000@example.test", - inferencePreferences: [{ provider: "anthropic", model: "noop" }], - turnTimeoutMs: 60_000, -}); - -const json = serializeWorkbenchDigestWorkflow(definition); -``` - -See [`workflows/README.md`](../README.md#status-note) for what -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. - -## Deploy from the npm registry - -Once published, deploy this workflow definition to a tenant by name and -version pin: - -``` -POST /api/tenants/:id/workflows/deployments -{ - "source": { "kind": "registry", "registry": "npm" }, - "entry": "./src/index.ts", - "pin": "@corbits/workbench-digest-workflow@0.0.1", - "sourceOfferingIds": ["", ...], - "defaultSourceOfferingId": "" -} -``` - -`entry` is the package's `interchange.workflow` module path; `pin` is -`"@corbits/workbench-digest-workflow@0.0.1"` or a semver range on the same name. The hub -installs, probes, gates, and freezes the definition from the registry -tarball before creating the deployment. diff --git a/workflows/workbench-digest/package.json b/workflows/workbench-digest/package.json deleted file mode 100644 index 4902a5ffdb..0000000000 --- a/workflows/workbench-digest/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "@corbits/workbench-digest-workflow", - "version": "0.0.1", - "description": "Minimal schedule-triggered workflow that posts a deterministic summary line, exercising the workbench-mail path at zero inference cost", - "license": "LGPL-2.1-or-later", - "repository": { - "type": "git", - "url": "https://github.com/corbitsdev/workbench.git", - "directory": "workflows/workbench-digest" - }, - "files": [ - "src", - "LICENSE", - "README.md" - ], - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/workflow": "0.3.0" - }, - "corbits": { - "workflow": { - "assetName": "workbench-digest", - "displayName": "Workbench digest", - "automatable": true - } - }, - "interchange": { - "workflow": "./src/index.ts" - } -} diff --git a/workflows/workbench-digest/src/index.ts b/workflows/workbench-digest/src/index.ts deleted file mode 100644 index 6a95fe3f01..0000000000 --- a/workflows/workbench-digest/src/index.ts +++ /dev/null @@ -1,134 +0,0 @@ -// The workbench-digest workflow: a single-step definition meant to be -// deployed against a workbench's own timeline address so its reply -// posts a deterministic summary line — the trigger's own message count -// and timestamp — back into the workbench, the same way a workbench -// host's reply becomes a workbench mail post (see -// `packages/chat/src/workbench-workflow.ts` and `platform-adapter.ts`'s -// `connectorReplyContent` handling). Fired by mail to the deployment's -// own trigger address: `@corbits/cron` delivers a tick mail on the -// digest's schedule, so this definition never declares `type: -// "schedule"` itself. This definition's only job is to relay an -// already-deterministic digest line back out verbatim, so nothing about -// the reply's content is left to the model. -// -// No agent-free step primitive runs on the deployed host today (see -// `@corbits/heartbeat-workflow`'s own header comment for why), so this -// step is unavoidably an agent step. CL-8160 stopped pinning a -// deployer-supplied synthetic noop `ModelSource` here: a deployer now -// deploys this workflow against the tenant's own real, resolved catalog -// offering like every -// other workflow (`resolveRealSourceOfferingIds`), so a trigger posts -// an actual, human-visible digest line at that model's ordinary -// per-turn cost rather than a constant empty reply. - -import { defineAgent } from "@intx/agent"; -import type { InferencePreference } from "@intx/agent"; -import { defineWorkflow, step } from "@intx/workflow"; -import type { WorkflowDefinition } from "@intx/workflow"; - -export const WORKBENCH_DIGEST_WORKFLOW_ID = "wf_workbench_digest"; -export const WORKBENCH_DIGEST_STEP_ID = "workbench-digest"; - -export const WORKBENCH_DIGEST_SYSTEM_PROMPT = - "You post a single deterministic summary line into a workbench. The " + - "message you receive is already the exact summary line to post — " + - "reply with its exact text: nothing added, nothing removed, no " + - "commentary, no formatting of your own."; - -/** - * Everything the definition needs that is per-deployment data. The - * trigger address names a specific deployment's inbox, so a definition - * built here is per-deployment by construction. - */ -export interface WorkbenchDigestWorkflowInput { - /** The deployment's mail address; each inbound mail is one run. */ - readonly triggerAddress: string; - /** Provider/model preferences, in order; resolved at deploy time. */ - readonly inferencePreferences: readonly InferencePreference[]; - /** Per-turn timeout in milliseconds, enforced on the single step. */ - readonly turnTimeoutMs: number; -} - -/** - * Builds the workbench-digest definition. Exactly one step, matching the - * shape every other definition in this repo commits to. - * - * The step always sets an explicit `timeout` — the singular `agent:` - * shorthand sets none, and a wedged inference call would then hang a - * run forever. Tools are never inlined on the definition: they arrive - * as packages on the deploy, keeping the definition pure data. - */ -export function buildWorkbenchDigestWorkflow( - input: WorkbenchDigestWorkflowInput, -): WorkflowDefinition { - if (input.triggerAddress === "") { - throw new Error("buildWorkbenchDigestWorkflow requires a non-empty triggerAddress"); - } - if (!Number.isInteger(input.turnTimeoutMs) || input.turnTimeoutMs <= 0) { - throw new Error("buildWorkbenchDigestWorkflow requires turnTimeoutMs to be a positive integer"); - } - return defineWorkflow({ - id: WORKBENCH_DIGEST_WORKFLOW_ID, - trigger: { type: "mail", to: input.triggerAddress }, - steps: { - "workbench-digest": step({ - agent: defineAgent({ - id: WORKBENCH_DIGEST_STEP_ID, - description: - "Relays an already-computed deterministic summary line " + - "back into the workbench it is deployed against", - systemPrompt: WORKBENCH_DIGEST_SYSTEM_PROMPT, - tools: [], - capabilities: [], - inference: { sources: input.inferencePreferences }, - }), - timeout: input.turnTimeoutMs, - }), - }, - }); -} - -/** - * Serializes a definition to the JSON a workflow asset carries. The - * definition must survive the asset round-trip byte-faithfully, so - * anything JSON would silently drop or mangle — functions, undefined, - * symbols, bigints, non-finite numbers, class instances — is a loud - * error naming the offending path instead of a corrupted asset. - */ -export function serializeWorkbenchDigestWorkflow(definition: WorkflowDefinition): string { - assertJsonPortable(definition, "definition"); - return JSON.stringify(definition); -} - -function assertJsonPortable(value: unknown, path: string): void { - if (value === null) return; - switch (typeof value) { - case "string": - case "boolean": - return; - case "number": - if (!Number.isFinite(value)) { - throw new Error(`${path} is a non-finite number; JSON drops it`); - } - return; - case "object": - break; - default: - throw new Error( - `${path} is a ${typeof value}, which does not survive JSON ` + "serialization", - ); - } - if (Array.isArray(value)) { - value.forEach((element, index) => { - assertJsonPortable(element, `${path}[${index}]`); - }); - return; - } - const proto: unknown = Object.getPrototypeOf(value); - if (proto !== Object.prototype && proto !== null) { - throw new Error(`${path} is a non-plain object; JSON would flatten it lossily`); - } - for (const [key, entry] of Object.entries(value)) { - assertJsonPortable(entry, `${path}.${key}`); - } -} diff --git a/workflows/workbench-digest/test/boundary.test.ts b/workflows/workbench-digest/test/boundary.test.ts deleted file mode 100644 index 7cd3621b2f..0000000000 --- a/workflows/workbench-digest/test/boundary.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -// This package is installable data on the native workflow contract: -// its shipped sources import only published platform packages, so the -// source tree plus the package manifest deploys on any Interchange -// instance without a wrapper contract. - -import { readFile, readdir } from "node:fs/promises"; -import path from "node:path"; -import { expect, test } from "bun:test"; - -const ALLOWED_IMPORT_PREFIXES = ["@intx/", "./", "../"]; - -async function listFiles(dir: string): Promise { - const entries = await readdir(dir, { withFileTypes: true }); - const files: string[] = []; - for (const entry of entries) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...(await listFiles(full))); - } else { - files.push(full); - } - } - return files; -} - -async function shippedFiles(): Promise { - const packageRoot = path.join(import.meta.dir, ".."); - return [ - ...(await listFiles(path.join(packageRoot, "src"))), - path.join(packageRoot, "package.json"), - ]; -} - -test("shipped sources import only published platform packages", async () => { - const importPattern = /from\s+"([^"]+)"/g; - const violations: string[] = []; - for (const file of await shippedFiles()) { - if (!file.endsWith(".ts")) continue; - const content = await readFile(file, "utf8"); - for (const match of content.matchAll(importPattern)) { - const specifier = match[1] ?? ""; - const allowed = ALLOWED_IMPORT_PREFIXES.some((prefix) => specifier.startsWith(prefix)); - if (!allowed) { - violations.push(`${path.basename(file)}: ${specifier}`); - } - } - } - expect(violations).toEqual([]); -}); diff --git a/workflows/workbench-digest/test/definition.test.ts b/workflows/workbench-digest/test/definition.test.ts deleted file mode 100644 index 48b6de6c1e..0000000000 --- a/workflows/workbench-digest/test/definition.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -// Tests for this package's own contract: the shape our factory -// commits to, its serialization guarantees, and its boundary. The -// platform's own normalization and validation are its business, not -// re-proven here. - -import { expect, test } from "bun:test"; -import type { StepPrimitive, WorkflowDefinition } from "@intx/workflow"; - -import { - WORKBENCH_DIGEST_STEP_ID, - WORKBENCH_DIGEST_SYSTEM_PROMPT, - WORKBENCH_DIGEST_WORKFLOW_ID, - buildWorkbenchDigestWorkflow, - serializeWorkbenchDigestWorkflow, -} from "../src/index"; - -const INPUT = { - triggerAddress: "ins_dep000000000000@example.test", - inferencePreferences: [{ provider: "anthropic", model: "claude-test" }], - turnTimeoutMs: 60000, -} as const; - -function digestStep(definition: WorkflowDefinition): StepPrimitive { - const primitive = definition.steps[WORKBENCH_DIGEST_STEP_ID]; - if (primitive === undefined || primitive.kind !== "step") { - throw new Error(`definition has no step primitive named ${WORKBENCH_DIGEST_STEP_ID}`); - } - return primitive; -} - -test("the definition has exactly one step", () => { - const definition = buildWorkbenchDigestWorkflow(INPUT); - expect(definition.stepOrder).toEqual([WORKBENCH_DIGEST_STEP_ID]); - expect(Object.keys(definition.steps)).toEqual([WORKBENCH_DIGEST_STEP_ID]); -}); - -test("the step carries an explicit per-turn timeout", () => { - const definition = buildWorkbenchDigestWorkflow(INPUT); - expect(digestStep(definition).timeout).toBe(INPUT.turnTimeoutMs); -}); - -test("the workflow is triggered by mail to the given deployment address", () => { - const definition = buildWorkbenchDigestWorkflow(INPUT); - expect(definition.id).toBe(WORKBENCH_DIGEST_WORKFLOW_ID); - expect(definition.triggers).toEqual([{ type: "mail", to: INPUT.triggerAddress }]); -}); - -test("the agent instructs relaying the exact summary line, carries the preferences, and inlines no tools", () => { - const agent = digestStep(buildWorkbenchDigestWorkflow(INPUT)).agent; - expect(agent.systemPrompt).toBe(WORKBENCH_DIGEST_SYSTEM_PROMPT); - expect(agent.inference.sources).toEqual([...INPUT.inferencePreferences]); - // Tools arrive as packages on the deploy, never inlined here: an - // inline factory is a function-valued field the asset cannot carry. - expect(agent.toolFactories).toEqual([]); -}); - -test("the definition survives the workflow-asset JSON round-trip", () => { - const definition = buildWorkbenchDigestWorkflow(INPUT); - const revived: unknown = JSON.parse(serializeWorkbenchDigestWorkflow(definition)); - expect(revived).toEqual(definition); -}); - -test("serialization fails loud on a function-valued field, naming its path", () => { - const poisoned = { - id: WORKBENCH_DIGEST_WORKFLOW_ID, - triggers: [{ type: "manual" }], - stepOrder: [WORKBENCH_DIGEST_STEP_ID], - steps: { - "workbench-digest": { - kind: "step", - id: WORKBENCH_DIGEST_STEP_ID, - drainBehavior: "cancel", - agent: { - id: WORKBENCH_DIGEST_STEP_ID, - systemPrompt: WORKBENCH_DIGEST_SYSTEM_PROMPT, - toolFactories: [() => []], - capabilities: [], - inference: { sources: [] }, - }, - }, - }, - } as unknown as WorkflowDefinition; - expect(() => serializeWorkbenchDigestWorkflow(poisoned)).toThrow( - /steps\.workbench-digest\.agent\.toolFactories\[0\]/, - ); -}); - -test("an empty trigger address is rejected", () => { - expect(() => buildWorkbenchDigestWorkflow({ ...INPUT, triggerAddress: "" })).toThrow( - /triggerAddress/, - ); -}); - -test("a non-positive or fractional turn timeout is rejected", () => { - expect(() => buildWorkbenchDigestWorkflow({ ...INPUT, turnTimeoutMs: 0 })).toThrow( - /turnTimeoutMs/, - ); - expect(() => buildWorkbenchDigestWorkflow({ ...INPUT, turnTimeoutMs: 0.5 })).toThrow( - /turnTimeoutMs/, - ); -}); diff --git a/workflows/workbench-digest/tsconfig.json b/workflows/workbench-digest/tsconfig.json deleted file mode 100644 index a8c2b9966d..0000000000 --- a/workflows/workbench-digest/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src", "test"], - "exclude": [] -} diff --git a/workflows/workbench-digest/tsconfig.src.json b/workflows/workbench-digest/tsconfig.src.json deleted file mode 100644 index fc8724305f..0000000000 --- a/workflows/workbench-digest/tsconfig.src.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } -} From 95d5a73cad7d36c6a0c171a41b15168ddf378a5b Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 15:42:41 -0700 Subject: [PATCH 2/2] workflows trim: drop hub deps on deleted workflow packages --- scripts/e2e/smoke-webhook.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/scripts/e2e/smoke-webhook.test.ts b/scripts/e2e/smoke-webhook.test.ts index 057b87f9fb..2894585847 100644 --- a/scripts/e2e/smoke-webhook.test.ts +++ b/scripts/e2e/smoke-webhook.test.ts @@ -23,10 +23,7 @@ import { import { describe, expect, test } from "bun:test"; import { resetSchema, setupDatabase } from "../db-setup.ts"; -import { - buildEchoWorkflow, - serializeEchoWorkflow, -} from "../../workflows/echo/src/index.ts"; +import { buildEchoWorkflow, serializeEchoWorkflow } from "../../workflows/echo/src/index.ts"; import { ensureNoopCatalogOffering, startNoopInferenceServer } from "./noop-inference-server.ts"; import { publishCorbitsToolsRegistry } from "../../packages/tool-registry-publish/src/publish.ts"; import { createHubAPI } from "../../packages/hub-api-client/src/index.ts";