From ea51f347ac85e1116aa55bf2d810b2f873f58973 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Fri, 11 Sep 2026 22:04:19 +0200 Subject: [PATCH 1/3] feat(surface,sdk): helpers runtime fanout via codegen for 46 relayfile providers (#340) Generate 50 namespaces, with 40 runtime clients and explicit refusal for unavailable upstream clients. Full acceptance remains blocked by the upstream gaps documented in docs/HELPERS-RUNTIME.md. Capture typechecks, full test runs, and the passing isolated retry of two watch-mode timeouts. Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82 Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82 Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82 Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82 Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82 --- docs/HELPERS-RUNTIME.md | 86 +++ evidence/slice-S/README.md | 143 ++++ evidence/slice-S/codegen.txt | 2 + evidence/slice-S/provider-inventory.txt | 16 + .../sdk-tests-before-catalog-fallback.txt | 275 +++++++ evidence/slice-S/sdk-tests-final.txt | 293 ++++++++ evidence/slice-S/sdk-tests.txt | 513 ++++++++++++++ evidence/slice-S/sdk-watch-retry.txt | 19 + evidence/slice-S/surface-regressions.txt | 5 + evidence/slice-S/surface-tests.txt | 18 + packages/sdk/src/authored-flow-error.ts | 2 + packages/sdk/src/authored-flow-executor.ts | 24 +- packages/sdk/src/authored-helper-effect.ts | 143 ++++ packages/sdk/src/authored-slack-effect.ts | 95 --- packages/sdk/src/cli/run.ts | 6 +- packages/sdk/src/failure-kinds.ts | 2 + packages/sdk/src/helper-preflight.ts | 34 + packages/sdk/src/helper-storage.ts | 23 + packages/sdk/src/helper-writeback.ts | 83 +++ packages/sdk/src/preflight.ts | 19 +- packages/sdk/src/slack-preflight.ts | 18 +- packages/sdk/src/slack-writeback.ts | 71 +- packages/sdk/tests/authored-helpers.test.ts | 190 +++++ packages/sdk/tests/helpers-fanout.test.ts | 112 +++ packages/sdk/tests/preflight.test.ts | 2 + packages/sdk/tsconfig.tests.json | 2 + packages/surface/package-lock.json | 668 +++++++++++++++++- packages/surface/src/effect-transport.ts | 59 ++ packages/surface/src/flow.ts | 14 +- packages/surface/src/helper-clients.ts | 30 + packages/surface/src/helpers/airtable.ts | 9 + packages/surface/src/helpers/asana.ts | 10 + packages/surface/src/helpers/azure-blob.ts | 10 + packages/surface/src/helpers/box.ts | 10 + packages/surface/src/helpers/calendly.ts | 10 + packages/surface/src/helpers/clickup.ts | 10 + packages/surface/src/helpers/clients.ts | 48 ++ packages/surface/src/helpers/cloudflare.ts | 10 + packages/surface/src/helpers/confluence.ts | 10 + packages/surface/src/helpers/daytona.ts | 10 + packages/surface/src/helpers/docker-hub.ts | 9 + packages/surface/src/helpers/dropbox.ts | 10 + packages/surface/src/helpers/fathom.ts | 9 + packages/surface/src/helpers/gcp.ts | 9 + packages/surface/src/helpers/gcs.ts | 10 + packages/surface/src/helpers/github.ts | 10 + packages/surface/src/helpers/gitlab.ts | 10 + packages/surface/src/helpers/gmail.ts | 10 + .../surface/src/helpers/google-calendar.ts | 10 + packages/surface/src/helpers/google-drive.ts | 10 + packages/surface/src/helpers/granola.ts | 10 + packages/surface/src/helpers/hubspot.ts | 10 + packages/surface/src/helpers/index.ts | 203 +++++- packages/surface/src/helpers/intercom.ts | 10 + packages/surface/src/helpers/jira.ts | 10 + packages/surface/src/helpers/linear.ts | 10 + packages/surface/src/helpers/mailgun.ts | 10 + packages/surface/src/helpers/mixpanel.ts | 10 + packages/surface/src/helpers/neon.ts | 9 + packages/surface/src/helpers/notion.ts | 10 + packages/surface/src/helpers/onedrive.ts | 10 + packages/surface/src/helpers/pipedrive.ts | 10 + packages/surface/src/helpers/postgres.ts | 10 + packages/surface/src/helpers/posthog.ts | 9 + packages/surface/src/helpers/providers.ts | 305 ++++++++ packages/surface/src/helpers/ramp.ts | 12 + packages/surface/src/helpers/recall.ts | 10 + packages/surface/src/helpers/reddit.ts | 10 + packages/surface/src/helpers/redis.ts | 10 + packages/surface/src/helpers/s3.ts | 10 + packages/surface/src/helpers/salesforce.ts | 10 + packages/surface/src/helpers/segment.ts | 9 + packages/surface/src/helpers/sendgrid.ts | 10 + packages/surface/src/helpers/sharepoint.ts | 10 + packages/surface/src/helpers/shopify.ts | 9 + packages/surface/src/helpers/shortcut.ts | 10 + packages/surface/src/helpers/stripe.ts | 10 + packages/surface/src/helpers/teams.ts | 10 + packages/surface/src/helpers/telegram.ts | 10 + .../surface/src/helpers/webhook-server.ts | 9 + packages/surface/src/helpers/x.ts | 9 + packages/surface/src/helpers/zendesk.ts | 10 + packages/surface/src/runtime.ts | 5 + .../surface/tests/fixtures/helpers.flow.ts | 26 + .../surface/tests/helpers.snapshot.test.ts | 2 +- scripts/generate-helpers.mjs | 97 ++- 86 files changed, 3893 insertions(+), 242 deletions(-) create mode 100644 docs/HELPERS-RUNTIME.md create mode 100644 evidence/slice-S/README.md create mode 100644 evidence/slice-S/codegen.txt create mode 100644 evidence/slice-S/provider-inventory.txt create mode 100644 evidence/slice-S/sdk-tests-before-catalog-fallback.txt create mode 100644 evidence/slice-S/sdk-tests-final.txt create mode 100644 evidence/slice-S/sdk-tests.txt create mode 100644 evidence/slice-S/sdk-watch-retry.txt create mode 100644 evidence/slice-S/surface-regressions.txt create mode 100644 evidence/slice-S/surface-tests.txt create mode 100644 packages/sdk/src/authored-helper-effect.ts delete mode 100644 packages/sdk/src/authored-slack-effect.ts create mode 100644 packages/sdk/src/helper-preflight.ts create mode 100644 packages/sdk/src/helper-storage.ts create mode 100644 packages/sdk/src/helper-writeback.ts create mode 100644 packages/sdk/tests/authored-helpers.test.ts create mode 100644 packages/sdk/tests/helpers-fanout.test.ts create mode 100644 packages/surface/src/effect-transport.ts create mode 100644 packages/surface/src/helper-clients.ts create mode 100644 packages/surface/src/helpers/airtable.ts create mode 100644 packages/surface/src/helpers/asana.ts create mode 100644 packages/surface/src/helpers/azure-blob.ts create mode 100644 packages/surface/src/helpers/box.ts create mode 100644 packages/surface/src/helpers/calendly.ts create mode 100644 packages/surface/src/helpers/clickup.ts create mode 100644 packages/surface/src/helpers/clients.ts create mode 100644 packages/surface/src/helpers/cloudflare.ts create mode 100644 packages/surface/src/helpers/confluence.ts create mode 100644 packages/surface/src/helpers/daytona.ts create mode 100644 packages/surface/src/helpers/docker-hub.ts create mode 100644 packages/surface/src/helpers/dropbox.ts create mode 100644 packages/surface/src/helpers/fathom.ts create mode 100644 packages/surface/src/helpers/gcp.ts create mode 100644 packages/surface/src/helpers/gcs.ts create mode 100644 packages/surface/src/helpers/github.ts create mode 100644 packages/surface/src/helpers/gitlab.ts create mode 100644 packages/surface/src/helpers/gmail.ts create mode 100644 packages/surface/src/helpers/google-calendar.ts create mode 100644 packages/surface/src/helpers/google-drive.ts create mode 100644 packages/surface/src/helpers/granola.ts create mode 100644 packages/surface/src/helpers/hubspot.ts create mode 100644 packages/surface/src/helpers/intercom.ts create mode 100644 packages/surface/src/helpers/jira.ts create mode 100644 packages/surface/src/helpers/linear.ts create mode 100644 packages/surface/src/helpers/mailgun.ts create mode 100644 packages/surface/src/helpers/mixpanel.ts create mode 100644 packages/surface/src/helpers/neon.ts create mode 100644 packages/surface/src/helpers/notion.ts create mode 100644 packages/surface/src/helpers/onedrive.ts create mode 100644 packages/surface/src/helpers/pipedrive.ts create mode 100644 packages/surface/src/helpers/postgres.ts create mode 100644 packages/surface/src/helpers/posthog.ts create mode 100644 packages/surface/src/helpers/providers.ts create mode 100644 packages/surface/src/helpers/ramp.ts create mode 100644 packages/surface/src/helpers/recall.ts create mode 100644 packages/surface/src/helpers/reddit.ts create mode 100644 packages/surface/src/helpers/redis.ts create mode 100644 packages/surface/src/helpers/s3.ts create mode 100644 packages/surface/src/helpers/salesforce.ts create mode 100644 packages/surface/src/helpers/segment.ts create mode 100644 packages/surface/src/helpers/sendgrid.ts create mode 100644 packages/surface/src/helpers/sharepoint.ts create mode 100644 packages/surface/src/helpers/shopify.ts create mode 100644 packages/surface/src/helpers/shortcut.ts create mode 100644 packages/surface/src/helpers/stripe.ts create mode 100644 packages/surface/src/helpers/teams.ts create mode 100644 packages/surface/src/helpers/telegram.ts create mode 100644 packages/surface/src/helpers/webhook-server.ts create mode 100644 packages/surface/src/helpers/x.ts create mode 100644 packages/surface/src/helpers/zendesk.ts create mode 100644 packages/surface/tests/fixtures/helpers.flow.ts diff --git a/docs/HELPERS-RUNTIME.md b/docs/HELPERS-RUNTIME.md new file mode 100644 index 000000000..0f7aceffa --- /dev/null +++ b/docs/HELPERS-RUNTIME.md @@ -0,0 +1,86 @@ +# Generated provider helpers + +`Ctx` exposes the relayfile helper clients as lazy journaled steps. For example: + +```ts +await f.github.createIssue({ repo: 'owner/repo', title: 'Investigate', body: 'Details' }); +await f.linear.createIssue({ teamId: 'team-id', title: 'Investigate' }); +await f.stripe.createInvoice({ customer: 'cus_123' }); +await f.asana.tasks.write({}, { name: 'Investigate' }); +await f.googleDrive.files.write({}, { name: 'Report' }); +``` + +Provider names with hyphens become camelCase namespaces. Resource names retain +upstream spelling (`f.github['issue-comments'].write(...)`). The upstream +resource clients expose `write`, `read`, `list`, and a pure `path` resolver. +All asynchronous methods become `Step`s; `path` performs no I/O. + +A provider directory must exist under the first configured nonempty mount root: +`RELAYFILE_MOUNT_PATH`, `WORKSPACE_ROOT`, `WORKFORCE_SANDBOX_ROOT`, +`RELAYFILE_MOUNT_ROOT`, or `RELAYFILE_ROOT`. Direct provider tokens are not a +transport. Explicit declarations such as `tools: { github: true }` and direct +body references are checked before the body runs; dynamic aliases are checked +when their steps execute. + +`RELAYFLOWS__MOCK=1` enables mock delivery for available providers. +Uppercase provider names and replace hyphens with underscores, for example +`RELAYFLOWS_GOOGLE_DRIVE_MOCK=1`. Mock writes are captured under +`/mock-writeback//`; mock reads return an empty object and +mock lists return an empty array. These are test responses, not provider data. + +Writes use `(run id, step id)` as the writeback idempotency key. Collection +paths receive stable draft filenames; item paths remain canonical. Delivery +must be confirmed before the receipt is persisted and the journal effect is +confirmed. Resume recovers the receipt, and repeats only an unconfirmed effect +using its original key. Provider failures complete with `worker_error`; +journal and receipt-storage failures fail closed. + +## Regeneration + +```sh +npm run gen --prefix packages/surface +node scripts/generate-helpers.mjs --adapters-dir /path/to/relayfile-adapters +``` + +The pinned published catalog and helper clients provide reproducible API types +and runtime factories. `--adapters-dir` additionally walks every adapter package +to discover providers that have not published writeback clients. Generated +files include factories, namespace types, a runtime client registry, and +preflight metadata. An unpublished client is marked unavailable rather than +assigned invented writeback paths. + +## Upstream gaps in slice S + +The supplied provider list contains 48 names including the five original +providers. The pinned catalog also includes GitLab and Ramp, so regeneration +currently emits 50 namespaces. Forty have runtime clients. These ten are +explicitly unavailable, including in mock mode: + +- airtable +- docker-hub +- fathom +- gcp +- neon +- posthog +- segment +- shopify +- webhook-server +- x + +The supplied checkout's catalog lists all except webhook-server under +`ADAPTERS_WITHOUT_WRITEBACK_PATHS`. Webhook-server is infrastructure, not a +writeback provider. These require upstream client/catalog work before slice S +can meet its full provider acceptance bar. + +Stripe has an adapter writeback route for invoice creation but no exported +helper client. The invoice convenience wrapper submits that existing route +through the shared transport and consumes `created()` from relay-helpers. + +`f.notion.appendBlock({ pageId, block })` is **mock-only**. The supplied Notion +adapter supports page content replacement and comments, but no append-block +writeback route. Real mount execution is refused before starting the effect; +mock execution can test types and journal lowering. This is an outstanding +acceptance gap, not live append-block support. Existing catalog Notion +resources continue to use the upstream client. + +No triggers or direct-token transports are added by this slice. diff --git a/evidence/slice-S/README.md b/evidence/slice-S/README.md new file mode 100644 index 000000000..769dcfcdb --- /dev/null +++ b/evidence/slice-S/README.md @@ -0,0 +1,143 @@ +# Slice S verification + +Run from `/Users/khaliqgant/fl-slice-S` on branch +`feat/spec-S-helpers-runtime-fanout`. No push or external provider write was +performed. Local mount tests simulate delivery receipts; provider writes in +kernel integration tests use mock mode. + +## Scope and outstanding acceptance + +See [HELPERS-RUNTIME.md](../../docs/HELPERS-RUNTIME.md) for the API and upstream +limitations. The generated registry covers 50 discovered namespaces, 40 with +runtime clients. Ten namespaces have no upstream client and are explicitly +unavailable. Notion appendBlock is mock-only because the adapter has no such +writeback route. This commit must not be treated as closing #340's full live +provider acceptance bar. + +The final catalog factory adjustment is also covered by the full final run. +The earlier successful run is retained in +[sdk-tests-before-catalog-fallback.txt](sdk-tests-before-catalog-fallback.txt). + +Registry inventory command: + +```sh +node --input-type=module - <<'JS' +import { helperProviders } from './packages/surface/dist/runtime.js'; +console.log(JSON.stringify({ + namespaces: helperProviders.length, + runtimeClients: helperProviders.filter(p => p.supported).length, + unavailable: helperProviders.filter(p => !p.supported).map(p => p.provider), +}, null, 2)); +JS +``` + +Literal output: [provider-inventory.txt](provider-inventory.txt). + +## Local package resolution + +The SDK's `node_modules/@relayflows/surface` points to this worktree's +`packages/surface`, not the published 2.0.8 package. Dependencies were installed +with `npm install --ignore-scripts --prefix packages/surface` and +`npm install --ignore-scripts --prefix packages/sdk`, then the local surface +was built and linked. The surface lockfile repair is included in the commit. + +For a fresh worktree, CI's equivalent local-package setup is: + +```sh +npm ci --ignore-scripts --prefix packages/surface +npm run build --prefix packages/surface +npm ci --ignore-scripts --prefix packages/sdk +npm install ./packages/surface --prefix packages/sdk --no-save --ignore-scripts +``` + +## Commands and literal output + +Surface typecheck: + +```sh +npm run typecheck --prefix packages/surface +``` + +Captured output: + +```text +> @relayflows/surface@2.0.8 typecheck +> tsc --noEmit +``` + +Surface tests and typed smoke fixture: + +```sh +npm test --prefix packages/surface +``` + +Full captured output: [surface-tests.txt](surface-tests.txt). + +Surface regression typechecks and codegen drift guard: + +```sh +npm run typecheck:regressions --prefix packages/surface +``` + +Full captured output: [surface-regressions.txt](surface-regressions.txt). + +Discovery from the supplied adapter checkout, followed by byte comparison with +regeneration from the pinned published inputs: + +```sh +node scripts/generate-helpers.mjs --adapters-dir /Users/khaliqgant/Projects/AgentWorkforce/relayfile-adapters +node packages/surface/scripts/check-generated-helpers.mjs +``` + +Full captured output: [codegen.txt](codegen.txt). + +Full SDK check, including kernel build, SDK and test typechecks, build, and all +SDK tests: + +```sh +PATH=/Users/khaliqgant/.cargo/bin:$PATH RUSTUP_TOOLCHAIN=stable RELAYFLOWD_BIN=/Users/khaliqgant/.relayflows-toolchain/target/2945819964/debug/relayflowd VITEST_MAX_FORKS=2 VITEST_MIN_FORKS=1 npm test --prefix packages/sdk +``` + +Full captured output: [sdk-tests-final.txt](sdk-tests-final.txt). Exit status: 1: two watch-mode tests hit their unchanged five-second timeout under concurrent load. All typecheck/build phases completed successfully. + +Literal final test output: + +```text + Test Files 1 failed | 70 passed | 1 skipped (72) + Tests 2 failed | 1314 passed | 3 skipped (1319) +``` + +Both timeout failures were rerun as part of the entire watch suite, in isolation: + +```sh +cd /Users/khaliqgant/fl-slice-S/packages/sdk +npm exec -- vitest run tests/cli-watch.test.ts --maxWorkers=1 --minWorkers=1 +``` + +Literal output: [sdk-watch-retry.txt](sdk-watch-retry.txt). Exit status: 0. + +```text + Test Files 1 passed (1) + Tests 10 passed (10) +``` + +No assertions, timeouts, or test selection within that suite were changed. +The final tree therefore has passing evidence for every enabled test across +the full run and isolated retry; the final full invocation was not green. + +The skipped tests are the existing opt-in real CLI adapter suite. The new +helper tests and the existing Slack crash/resume tests ran. + +The explicit Rust toolchain bypasses a stale mise shim; RELAYFLOWD_BIN points +all test consumers at this worktree's freshly built daemon. The fork limits +reduce contention without changing test selection or timeout assertions. + +The earlier full run is retained at [sdk-tests.txt](sdk-tests.txt): + +```sh +PATH=/Users/khaliqgant/.cargo/bin:$PATH RUSTUP_TOOLCHAIN=stable npm test --prefix packages/sdk +``` + +It exposed the memory.recall/provider-name collision and missing refusal-kind +coverage, which were repaired, as well as missing daemon paths and timeout +failures. Its output is not passing evidence. diff --git a/evidence/slice-S/codegen.txt b/evidence/slice-S/codegen.txt new file mode 100644 index 000000000..4ecaa2368 --- /dev/null +++ b/evidence/slice-S/codegen.txt @@ -0,0 +1,2 @@ +Generated 50 provider helpers (10 without upstream writeback clients) +HELPERS_GENERATED_OK airtable.ts, asana.ts, azure-blob.ts, box.ts, calendly.ts, clickup.ts, clients.ts, cloudflare.ts, confluence.ts, daytona.ts, docker-hub.ts, dropbox.ts, fathom.ts, gcp.ts, gcs.ts, github.ts, gitlab.ts, gmail.ts, google-calendar.ts, google-drive.ts, granola.ts, hubspot.ts, index.ts, intercom.ts, jira.ts, linear.ts, mailgun.ts, mixpanel.ts, neon.ts, notion.ts, onedrive.ts, pipedrive.ts, postgres.ts, posthog.ts, providers.ts, ramp.ts, recall.ts, reddit.ts, redis.ts, s3.ts, salesforce.ts, segment.ts, sendgrid.ts, sharepoint.ts, shopify.ts, shortcut.ts, slack.ts, stripe.ts, teams.ts, telegram.ts, webhook-server.ts, x.ts, zendesk.ts diff --git a/evidence/slice-S/provider-inventory.txt b/evidence/slice-S/provider-inventory.txt new file mode 100644 index 000000000..3c4ec5cbd --- /dev/null +++ b/evidence/slice-S/provider-inventory.txt @@ -0,0 +1,16 @@ +{ + "namespaces": 50, + "runtimeClients": 40, + "unavailable": [ + "airtable", + "docker-hub", + "fathom", + "gcp", + "neon", + "posthog", + "segment", + "shopify", + "webhook-server", + "x" + ] +} diff --git a/evidence/slice-S/sdk-tests-before-catalog-fallback.txt b/evidence/slice-S/sdk-tests-before-catalog-fallback.txt new file mode 100644 index 000000000..e21cfce46 --- /dev/null +++ b/evidence/slice-S/sdk-tests-before-catalog-fallback.txt @@ -0,0 +1,275 @@ + +> @relayflows/sdk@2.0.8 test +> sh scripts/test.sh + + +> @relayflows/sdk@2.0.8 test:prep +> ( cd ../../kernel && sh ../ops/cargo.sh build ) && ( [ ! -d ../../testdata/preflight ] || find ../../testdata/preflight -name '*-cli' -type f -exec chmod +x {} + ) + + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.50s + +> @relayflows/sdk@2.0.8 typecheck +> tsc --noEmit && tsc -p tsconfig.type-tests.json + + +> @relayflows/sdk@2.0.8 build +> tsc && node scripts/make-cli-executable.mjs + + +> @relayflows/sdk@2.0.8 typecheck:tests +> tsc -p tsconfig.tests.json + + + RUN v2.1.9 /Users/khaliqgant/fl-slice-S/packages/sdk + +stdout | tests/live-kernel.test.ts +LIVE_KERNEL relayflowd=/Users/khaliqgant/.relayflows-toolchain/target/2945819964/debug/relayflowd +LIVE_KERNEL flows=/Users/khaliqgant/fl-slice-S/packages/sdk/dist/cli.js + + ✓ tests/cli.test.ts (63 tests) 13625ms + ✓ flows check CLI > binds a checked relative wrapper to the flow directory for worker execution 1177ms + ✓ flows check CLI > uses the raw Claude adapter model flag instead of accepting auth status as model proof 302ms + ✓ flows check CLI > uses Codex login status and reports a rejected model as unavailable, not unauthenticated 311ms + ✓ flows check CLI > refuses a typo model before probing or contacting relayflowd 975ms + ✓ flows check CLI > maps every input refusal path to its declared kind without raw exceptions 1965ms + ✓ flows run/resume CLI over the journal protocol > parses run options, submits the kernel dialect, and exits 0 on success 701ms + ✓ flows run/resume CLI over the journal protocol > exits 1 and emits the declared completionReason for a failed run 640ms + ✓ flows run/resume CLI over the journal protocol > exits 3 and names the parked llm step 633ms + ✓ flows run/resume CLI over the journal protocol > reports a needs_human agent step as parked for human recovery 600ms + ✓ flows run/resume CLI over the journal protocol > classifies a typed hello refusal as a protocol error, not an unreachable daemon 539ms + ✓ flows run/resume CLI over the journal protocol > follows a dispatched worker step instead of reporting a protocol error 653ms + ✓ flows run/resume CLI over the journal protocol > bounds a worker wait by its lease and reports what it is waiting for 616ms + ✓ flows run/resume CLI over the journal protocol > resumes a parked run from snapshot step types without reading journal sequence one 525ms + ✓ flows run/resume CLI over the journal protocol > maps only run_not_found resumes to exit 2 1570ms + ✓ tests/observer-link.test.ts (39 tests) 370ms + ✓ tests/preflight.test.ts (27 tests) 143ms + ✓ tests/daemon-lifecycle.test.ts (42 tests) 83ms + ✓ tests/authored-flow.test.ts (25 tests) 846ms + ✓ tests/worker-cli.test.ts (13 tests) 20932ms + ✓ custom wrapper execution identity > refuses a wrapper symlink retarget before delivering private values 381ms + ✓ custom wrapper execution identity > bounds wrapper execution after acknowledgement 307ms + ✓ custom wrapper execution identity > refuses a duplicate execute protocol frame 328ms + ✓ custom wrapper execution bounds are reader-owned > resolves when a conforming wrapper leaks a stdio pipe to a background helper 1803ms + ✓ custom wrapper execution bounds are reader-owned > resolves when the leaked helper inherits stderr only 1819ms + ✓ custom wrapper execution bounds are reader-owned > resolves when a wrapper leaks a stdio pipe and exits before identifying 3257ms + ✓ custom wrapper execution bounds are reader-owned > journals a completionReason at the default bound when a wrapper leaks a stdio pipe 11259ms + ✓ custom wrapper execution bounds are reader-owned > accepts the same over-8KiB payload whether or not it coalesces with the execute token 499ms + ✓ tests/mcp.test.ts (30 tests) 10469ms + ✓ MCP preflight and transports > flows check refuses an undeclared server with exit 2 and no daemon 813ms + ✓ MCP preflight and transports > flows check reports a refusing server and leaves no PID 930ms + ✓ MCP preflight and transports > kills a SIGTERM-resistant silent child after a parent-owned handshake deadline 1312ms + ✓ MCP preflight and transports > reaps a SIGTERM-resistant descendant with inherit stdio before cleanup finishes 1295ms + ✓ MCP preflight and transports > reaps a SIGTERM-resistant descendant with ignore stdio before cleanup finishes 2062ms + ✓ MCP preflight and transports > reports malformed connection configuration as config_invalid 840ms + ✓ authored MCP effects against the real kernel > journals one MCP receipt per call with args, result, stable logical key, and a confirmed effect 663ms + ✓ authored MCP effects against the real kernel > reports a dropped tool connection as a failed CLI run 1196ms + ✓ authored MCP effects against the real kernel > journals drop/echo failure as worker_error with diagnostic mcp_disconnected 328ms + ✓ tests/validate.test.ts (68 tests) 27ms + ✓ tests/journal-client.test.ts (14 tests) 105ms + ✓ tests/tick-source.test.ts (33 tests) 91ms + ✓ tests/verb-field-lint.test.ts (78 tests) 510ms + ✓ tests/cloud-run.test.ts (47 tests) 237ms + ✓ tests/authored-flow-lifecycle-executor.test.ts (27 tests) 675ms + ✓ tests/cli-replay.test.ts (36 tests) 2709ms + ✓ flows replay > --json is byte-identical across two CLI invocations (diff) 2121ms +stdout | tests/live-kernel.test.ts > built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI +LIVE_ANALYZER ready: claude -p --model claude-haiku-4-5-20251001 round-trip OK + + ✓ tests/tick-runner.test.ts (22 tests) 5119ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms fractional as an invocation error 802ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms exponent notation as an invocation error 848ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms hex as an invocation error 1211ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms trailing text as an invocation error 745ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms empty as an invocation error 700ms + ✓ CLI argument parsing refuses coercion rather than accepting it > accepts an exact integer and proceeds past parsing 713ms +stdout | tests/live-kernel.test.ts > built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI +LIVE_ANALYZER analysis: {"reasoning":"This story directly demonstrates a practical AI agent implementation performing autonomous software development tasks—opening and reviewing pull requests—which is a core application of AI agents and automation in development workflows.","relevance_score":9,"story_title":"Show HN: an agent that opens and reviews its own pull requests [wake-nonce-7f3a91c4]"} + +stdout | tests/live-kernel.test.ts > surface resume after a real daemon kill > resumes a three-step run with each successful completion exactly once +LIVE_KERNEL kill -9 pid=59295 run=01M290DZP9HM8B2MXH72ZW18F1 while step=two state=Running + + ✓ tests/stop-process-group.test.ts (6 tests) 7736ms + ✓ every stop reaches the process group, not just the direct child > exits the run after an execution-timeout stop 927ms + ✓ every stop reaches the process group, not just the direct child > exits the run after a protocol terminate stop 709ms + ✓ every stop reaches the process group, not just the direct child > kills a SIGTERM-deaf grandchild after a protocol terminate stop 2050ms + ✓ every stop reaches the process group, not just the direct child > kills a SIGTERM-deaf grandchild after an execution-timeout stop 2179ms + ✓ every stop reaches the process group, not just the direct child > holds the loop open long enough for the escalation to run 1389ms + ✓ every stop reaches the process group, not just the direct child > terminate() forces a group that outlives SIGTERM 479ms + ✓ tests/live-kernel.test.ts (31 tests) 83164ms + ✓ built flows CLI against live relayflowd > twenty-six-step reuses 25 durable completions after editing the failed final step 6446ms + ✓ built flows CLI against live relayflowd > runs rung (a), parks rung (b), and keeps JSON report-shaped 9141ms + ✓ built flows CLI against live relayflowd > allows a deterministic run to exceed the bounded request timeout 33450ms + ✓ built flows CLI against live relayflowd > follows a live worker dispatch through flows run 1801ms + ✓ built flows CLI against live relayflowd > runs an agent CLI end to end through the SDK worker 369ms + ✓ built flows CLI against live relayflowd > f.agent lowers to a real agent step and dispatches through a live worker 698ms + ✓ built flows CLI against live relayflowd > f.agent's default flowPath anchors on cwd, not cwd's parent 761ms + ✓ built flows CLI against live relayflowd > can always get a parked run to a late-attaching worker 5680ms + ✓ built flows CLI against live relayflowd > reports a real manual-recovery NeedsHuman state as parked 2454ms + ✓ built flows CLI against live relayflowd > runs hn-monitor analyze-story end-to-end via a stub agent CLI (gate 2 clause 2 demo) 438ms + ✓ built flows CLI against live relayflowd > hn-monitor analyze-story FAILS verification when the CLI omits required schema fields 390ms + ✓ built flows CLI against live relayflowd > agent step preserves the CliResult wrapper as output when the CLI emits non-JSON text 302ms + ✓ built flows CLI against live relayflowd > AgentWorker exposes wake_context to the CLI via RELAYFLOW_WAKE_CONTEXT env var (real analyzer prerequisite) 371ms + ✓ built flows CLI against live relayflowd > AgentWorker passes a declared model to an identified wrapper as RELAYFLOW_MODEL 603ms + ✓ built flows CLI against live relayflowd > AgentWorker refuses a nonconforming journal-submitted wrapper before exposing RELAYFLOW_MODEL 390ms + ✓ built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI 10337ms + ✓ built flows CLI against live relayflowd > preflights before journaling and names an unreachable socket 2501ms + ✓ built flows CLI against live relayflowd > starts exactly one daemon when two runs race for one empty data dir 1360ms + ✓ surface resume after a real daemon kill > resumes a three-step run with each successful completion exactly once 3014ms + ✓ a relayflow can be scheduled: tick source against live relayflowd > a tick spawns a real run whose step reports the SCHEDULED instant 471ms + ✓ tests/gate-contract.test.ts (20 tests) 493ms + ✓ tests/cli-hn-monitor.test.ts (16 tests) 142ms + ✓ tests/authored-flow-slack.test.ts (6 tests) 3818ms + ✓ authored Slack helper effects > journals exactly one effect with the authored params and typed receipt, without network 353ms + ✓ authored Slack helper effects > replays after SIGKILL before confirm with the same token and one successful completion 1232ms + ✓ authored Slack helper effects > replays after SIGKILL before complete with the same token and one successful completion 1204ms + ✓ authored Slack helper effects > writes two files for two calls and supports dm, reply, and react 992ms + ✓ tests/bundle.test.ts (21 tests) 16282ms + ✓ immutable bundles > returns exit 2 naming a byte-flipped payload and refuses to reuse corruption 921ms + ✓ immutable bundles > builds and verifies the canonical YAML fixture through the compiled CLI 2446ms + ✓ immutable bundles > emits the ephemeral warning on CLI stderr and uses the default output directory 1710ms + ✓ immutable bundles > refuses build-provable CLI resolution errors without environment probes 813ms + ✓ immutable bundles > builds a standalone TS fixture twice with identical executable hashes 5288ms + ✓ immutable bundles > refuses to label installed dependency drift with lockfile pins 812ms + ✓ immutable bundles > refuses invalid CLI arguments %j 787ms + ✓ immutable bundles > refuses invalid CLI arguments "--out" 797ms + ✓ immutable bundles > refuses invalid CLI arguments "--verify" 766ms + ✓ immutable bundles > refuses invalid CLI arguments "--verify" 757ms + ✓ immutable bundles > refuses invalid CLI arguments "--out" 797ms + ✓ tests/daemon-lifecycle-live.test.ts (9 tests) 18407ms + ✓ flows run against a data dir with no daemon (§6 test 7) > cold start spawns exactly one daemon, the run succeeds, and the daemon outlives the CLI 1512ms + ✓ flows run against a data dir with no daemon (§6 test 7) > polls, bounded, for a daemon that holds the lock before it binds 2187ms + ✓ flows run against a data dir with no daemon (§6 test 7) > attaches to a serving daemon that has not published a connection file 1607ms + ✓ flows run against a data dir with no daemon (§6 test 7) > a second run attaches to the daemon the first one started, spawning nothing 3468ms + ✓ flows run against a data dir with no daemon (§6 test 7) > detects a stale connection file left by a hard kill and starts a fresh daemon 3192ms + ✓ concurrent invocations against one empty data dir (§6 test 15) > ends with exactly one daemon owning the socket, and both runs succeed 1994ms + ✓ refusals from a spawn that cannot produce a daemon > names relayflowd_not_found rather than falling through to PATH 1398ms + ✓ refusals from a spawn that cannot produce a daemon > names daemon_start_failed and quotes the daemon log when startup dies 1541ms + ✓ refusals from a spawn that cannot produce a daemon > refuses a daemon speaking another protocol version instead of binding over it 1504ms + ✓ tests/authored-helpers.test.ts (6 tests) 11265ms + ✓ lowers the named acceptance helpers and Slack to confirmed journal effects 1031ms + ✓ runs every available provider through the real kernel and resumes completed effects without a second write 7285ms + ✓ replays after SIGKILL before confirm with the same token and one successful completion 1224ms + ✓ replays after SIGKILL before complete with the same token and one successful completion 1142ms + ✓ journals provider failure with worker_error and never confirms the effect 572ms + ✓ tests/backlog-picker.test.ts (14 tests) 191ms + ✓ tests/backlog-picker-flow.test.ts (6 tests) 1239ms + ✓ backlog-picker flow > emit-package describes the entry select-entry chose, even if the backlog changes between them 308ms + ✓ backlog-picker flow > does not emit a stale entry left by a previous run 626ms + ✓ tests/flow-executor-chain.test.ts (12 tests) 16027ms + ✓ flow executor LLM and output-binding chain > runs f.llm -> f.agent -> f.run with schema-verified journal output and the exact allowed model 1775ms + ✓ flow executor LLM and output-binding chain > fails invalid LLM output before the next step: not JSON 1027ms + ✓ flow executor LLM and output-binding chain > fails invalid LLM output before the next step: {"message":7} 1107ms + ✓ flow executor LLM and output-binding chain > retains tagged-template text output 882ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: null 1058ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: [1,2] 1043ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: "hello" 1053ms + ✓ flow executor LLM and output-binding chain > runs the authored LLM path through the built flows CLI 2283ms + ✓ flow executor LLM and output-binding chain > passes a declarative verified value through an agent into a deterministic artifact 1419ms + ✓ flow executor LLM and output-binding chain > journals a missing optional field as a failure before the consuming command executes 1080ms + ✓ flow executor LLM and output-binding chain > flows run consumes YAML bindings and resume reuses the original journal output 3226ms + ✓ tests/authored-flow-operation.test.ts (23 tests) 656ms + ✓ completes the gate in linear time over a body with 30000 ordinary awaits 485ms + ✓ tests/direct-input.test.ts (4 tests) 13192ms + ✓ direct .flow.ts input through the built CLI and live runtime > executes inline and file JSON input through relayflowd 4688ms + ✓ direct .flow.ts input through the built CLI and live runtime > refuses missing and malformed input before contacting relayflowd 5518ms + ✓ direct .flow.ts input through the built CLI and live runtime > does not run the authored body before daemon availability 1517ms + ✓ direct .flow.ts input through the built CLI and live runtime > refuses oversized file input before contacting relayflowd 1466ms + ✓ tests/work-package-consumer.test.ts (13 tests) 529ms + ✓ the Garden join: picker output feeds the consumer > accepts a package the picker actually emits, and refuses one lacking a definition of done 516ms + ✓ tests/spec-parity.test.ts (31 tests) 947ms + ✓ tests/helpers-fanout.test.ts (95 tests) 848ms + ✓ tests/cli-watch.test.ts (10 tests) 30440ms + ✓ flows check --watch > rechecks syntax errors, clears once, and returns the last refusal on Ctrl-C 2713ms + ✓ flows check --watch > streams JSON lines without ANSI, recovers after atomic saves, and exits zero after repair 3606ms + ✓ flows check --watch > coalesces 20 concurrent saves into at most two rechecks 3247ms + ✓ flows check --watch > watches transitive relative use imports, cycles, and nearest config changes 4749ms + ✓ flows check --watch > refreshes the import graph and notices missing imports being created 4715ms + ✓ flows check --watch > reloads authored TypeScript instead of reusing the first imported definition 2892ms + ✓ flows check --watch > detects a nearer config appearing and falls back after it is deleted 3930ms + ✓ flows check --watch > keeps watching after the target is deleted and recreated 3779ms + ✓ flows check --watch > queues changes during a slow check without overlapping checks 801ms + ✓ tests/worker-lease.test.ts (7 tests) 31ms + ✓ tests/typed-output.test.ts (14 tests) 554ms + ✓ tests/webhook-live.test.ts (3 tests) 7522ms + ✓ flows serve-webhook writes JSON before the daemon starts, then journals and archives exactly once 2205ms + ✓ replays a dropped file after SIGKILL before spawn 1004ms + ✓ resumes the same journal after SIGKILL after spawn and before acknowledgement 4311ms + ✓ tests/effect-channel.test.ts (5 tests) 676ms + ✓ tests/mcp-lifecycle.test.ts (4 tests) 28ms + ✓ tests/model-selection.test.ts (10 tests) 45ms + ✓ tests/json-schema-bound.test.ts (71 tests) 7492ms + ✓ JSON Schema termination bound > walks a deep schema with an explicit stack rather than recursion 6380ms + ✓ tests/relayflowd-path.test.ts (10 tests) 11ms + ✓ tests/f-memory.test.ts (7 tests) 2782ms + ✓ reads the seeded local SQLite database with cloud unused and fallback disabled 551ms + ✓ cannot widen script scope using a raw project option or another flow name 427ms + ✓ attaches read helpers without journaling read steps 396ms + ✓ refuses missing, corrupt, and directory database paths before body or journal activity 361ms + ✓ detects direct memory use before running earlier body effects 369ms + ✓ maps an unavailable provider probe to memory_unreachable 323ms + ✓ fails closed for deferred writes, agent scope, disabled script memory, and reads after done 353ms + ✓ tests/webhook.test.ts (6 tests) 171ms + ✓ tests/yaml-local-agent-live.test.ts (7 tests) 8993ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked step CLI and model and journals done 1348ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked named CLI and model and journals done 1330ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked flow CLI and model and journals done 1363ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked project CLI and model and journals done 1325ms + ✓ YAML --local-agent through the built CLI and real daemon > still parks without --local-agent 1150ms + ✓ YAML --local-agent through the built CLI and real daemon > reports the agent process failure 1323ms + ✓ YAML --local-agent through the built CLI and real daemon > preserves declared workspace surfaces that the local worker cannot pin 1153ms + ✓ tests/local-dev-ux.test.ts (8 tests) 50ms + ✓ tests/dependency-validation.test.ts (6 tests) 1465ms + ✓ dependency validation > accepts a valid 10,000-step reverse chain through every direct public boundary 824ms + ✓ dependency validation > bounds the author-facing path for a 10,000-step cycle 544ms + ✓ tests/input-binding.test.ts (12 tests) 529ms + ✓ tests/deterministic-llm.test.ts (5 tests) 170ms + ✓ tests/bin.test.ts (7 tests) 4695ms + ✓ built flows binary > refuses through a symlink to the built artifact 778ms + ✓ built flows binary > refuses through a symlinked directory component 808ms + ✓ built flows binary > classifies a signal-terminated auth probe as probe_failed 815ms + ✓ built flows binary > classifies an unavailable PATH resolver as probe_failed 700ms + ✓ built flows binary > does not describe a present non-executable CLI as missing 793ms + ✓ built flows binary > runs one auth probe for three steps sharing a flow CLI 798ms + ✓ tests/scope-compiler.test.ts (25 tests) 28ms + ✓ tests/budget-attribution.test.ts (4 tests) 11ms + ✓ tests/hn-poller.test.ts (6 tests) 10ms + ✓ tests/dir-watcher-poller.test.ts (6 tests) 13ms + ✓ tests/hello-deterministic.test.ts (5 tests) 37ms + ✓ tests/budget-preflight.test.ts (16 tests) 20ms + ✓ tests/work-package-validator.test.ts (7 tests) 14ms + ✓ tests/classify-outcome.test.ts (2 tests) 2233ms + ✓ classifyOutcome > gives up and reports when a running run never becomes classifiable 2072ms + ↓ tests/real-cli-adapters.test.ts (3 tests | 3 skipped) + ✓ tests/model-pricing.test.ts (8 tests) 8ms + ✓ tests/authored-use-loader.test.ts (5 tests) 117ms + ✓ tests/memoization.test.ts (57 tests) 1170ms + ✓ refuses invalid reuse invocation "run" 1068ms + ✓ tests/parse-json-output.test.ts (7 tests) 8ms + ✓ tests/cli-adapter.test.ts (3 tests) 11ms + ✓ tests/journal-client-completion.test.ts (4 tests) 116ms + ✓ tests/worker-cli-abort.test.ts (2 tests) 2951ms + ✓ stops claude and its process group when lease ownership is lost 1466ms + ✓ stops wrapper.mjs and its process group when lease ownership is lost 1483ms + ✓ tests/direct-run-failure.test.ts (6 tests) 14ms + ✓ tests/budget-authored-live.test.ts (2 tests) 670ms + ✓ authored budgets through the live kernel > retains the first completion and journals refusal of the next authored step 326ms + ✓ authored budgets through the live kernel > permits done after the last valid step crosses the limit 343ms + ✓ tests/slack-writeback.test.ts (1 test) 275ms + ✓ tests/local-agent-live.test.ts (5 tests) 45923ms + ✓ built CLI local agent against a real daemon > dispatches through the wrapper and keeps --json stdout report-shaped 1917ms + ✓ built CLI local agent against a real daemon > runs beyond the initial 30-second lease without a second invocation 36937ms + ✓ built CLI local agent against a real daemon > renders actual agent completion in text output 3824ms + ✓ built CLI local agent against a real daemon > returns a failed run when the agent process fails 1753ms + ✓ built CLI local agent against a real daemon > refuses a workspace it cannot pin before invoking the agent 1489ms + ✓ tests/placement.test.ts (54 tests) 41ms + ✓ tests/memory.test.ts (18 tests) 10ms + ✓ tests/cli-progress-wait.test.ts (2 tests) 1229ms + ✓ run starts the wait clock on its first observed lease 683ms + ✓ resume starts the wait clock on its first observed lease 545ms + ✓ tests/worker-platform.test.ts (1 test) 8ms + + Test Files 71 passed | 1 skipped (72) + Tests 1315 passed | 3 skipped (1318) + Start at 21:50:45 + Duration 214.13s (transform 3.28s, setup 0ms, collect 45.07s, tests 351.45s, environment 24ms, prepare 8.01s) + diff --git a/evidence/slice-S/sdk-tests-final.txt b/evidence/slice-S/sdk-tests-final.txt new file mode 100644 index 000000000..c0ff6f569 --- /dev/null +++ b/evidence/slice-S/sdk-tests-final.txt @@ -0,0 +1,293 @@ + +> @relayflows/sdk@2.0.8 test +> sh scripts/test.sh + + +> @relayflows/sdk@2.0.8 test:prep +> ( cd ../../kernel && sh ../ops/cargo.sh build ) && ( [ ! -d ../../testdata/preflight ] || find ../../testdata/preflight -name '*-cli' -type f -exec chmod +x {} + ) + + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.64s + +> @relayflows/sdk@2.0.8 typecheck +> tsc --noEmit && tsc -p tsconfig.type-tests.json + + +> @relayflows/sdk@2.0.8 build +> tsc && node scripts/make-cli-executable.mjs + + +> @relayflows/sdk@2.0.8 typecheck:tests +> tsc -p tsconfig.tests.json + + + RUN v2.1.9 /Users/khaliqgant/fl-slice-S/packages/sdk + +stdout | tests/live-kernel.test.ts +LIVE_KERNEL relayflowd=/Users/khaliqgant/.relayflows-toolchain/target/2945819964/debug/relayflowd +LIVE_KERNEL flows=/Users/khaliqgant/fl-slice-S/packages/sdk/dist/cli.js + + ✓ tests/cli.test.ts (63 tests) 12160ms + ✓ flows check CLI > binds a checked relative wrapper to the flow directory for worker execution 1247ms + ✓ flows check CLI > uses the raw Claude adapter model flag instead of accepting auth status as model proof 333ms + ✓ flows check CLI > refuses a typo model before probing or contacting relayflowd 667ms + ✓ flows check CLI > maps every input refusal path to its declared kind without raw exceptions 1598ms + ✓ flows run/resume CLI over the journal protocol > parses run options, submits the kernel dialect, and exits 0 on success 651ms + ✓ flows run/resume CLI over the journal protocol > exits 1 and emits the declared completionReason for a failed run 484ms + ✓ flows run/resume CLI over the journal protocol > exits 3 and names the parked llm step 657ms + ✓ flows run/resume CLI over the journal protocol > reports a needs_human agent step as parked for human recovery 644ms + ✓ flows run/resume CLI over the journal protocol > follows a dispatched worker step instead of reporting a protocol error 548ms + ✓ flows run/resume CLI over the journal protocol > bounds a worker wait by its lease and reports what it is waiting for 550ms + ✓ flows run/resume CLI over the journal protocol > resumes a parked run from snapshot step types without reading journal sequence one 459ms + ✓ flows run/resume CLI over the journal protocol > maps only run_not_found resumes to exit 2 1517ms + ✓ tests/observer-link.test.ts (39 tests) 332ms + ✓ tests/preflight.test.ts (27 tests) 144ms + ✓ tests/daemon-lifecycle.test.ts (42 tests) 84ms + ✓ tests/authored-flow.test.ts (25 tests) 840ms + ✓ tests/worker-cli.test.ts (13 tests) 22490ms + ✓ custom wrapper execution identity > passes an explicit safe environment at identification and execution 383ms + ✓ custom wrapper execution identity > refuses a wrapper symlink retarget before delivering private values 365ms + ✓ custom wrapper execution identity > bounds wrapper execution after acknowledgement 419ms + ✓ custom wrapper execution identity > bounds captured wrapper output 433ms + ✓ custom wrapper execution identity > refuses a duplicate execute protocol frame 435ms + ✓ custom wrapper execution bounds are reader-owned > resolves when a conforming wrapper leaks a stdio pipe to a background helper 1954ms + ✓ custom wrapper execution bounds are reader-owned > resolves when the leaked helper inherits stderr only 1929ms + ✓ custom wrapper execution bounds are reader-owned > resolves when a wrapper leaks a stdio pipe and exits before identifying 3257ms + ✓ custom wrapper execution bounds are reader-owned > journals a completionReason at the default bound when a wrapper leaks a stdio pipe 11263ms + ✓ custom wrapper execution bounds are reader-owned > accepts an execute token and an over-8KiB payload flushed in one write 391ms + ✓ custom wrapper execution bounds are reader-owned > accepts the same over-8KiB payload whether or not it coalesces with the execute token 865ms + ✓ custom wrapper execution bounds are reader-owned > still bounds an un-terminated handshake buffer and names the bound 375ms + ✓ delivers the journaled memory pack to the real wrapper and excludes its charge from completion usage 418ms + ✓ tests/mcp.test.ts (30 tests) 10785ms + ✓ MCP preflight and transports > flows check refuses an undeclared server with exit 2 and no daemon 878ms + ✓ MCP preflight and transports > flows check reports a refusing server and leaves no PID 994ms + ✓ MCP preflight and transports > kills a SIGTERM-resistant silent child after a parent-owned handshake deadline 1316ms + ✓ MCP preflight and transports > reaps a SIGTERM-resistant descendant with inherit stdio before cleanup finishes 1327ms + ✓ MCP preflight and transports > reaps a SIGTERM-resistant descendant with ignore stdio before cleanup finishes 2063ms + ✓ MCP preflight and transports > reports malformed connection configuration as config_invalid 864ms + ✓ authored MCP effects against the real kernel > journals one MCP receipt per call with args, result, stable logical key, and a confirmed effect 680ms + ✓ authored MCP effects against the real kernel > reports a dropped tool connection as a failed CLI run 1078ms + ✓ authored MCP effects against the real kernel > journals drop/echo failure as worker_error with diagnostic mcp_disconnected 387ms + ✓ tests/validate.test.ts (68 tests) 50ms + ✓ tests/journal-client.test.ts (14 tests) 120ms + ✓ tests/tick-source.test.ts (33 tests) 58ms + ✓ tests/verb-field-lint.test.ts (78 tests) 812ms + ✓ tests/cloud-run.test.ts (47 tests) 203ms + ✓ tests/authored-flow-lifecycle-executor.test.ts (27 tests) 703ms + ✓ tests/cli-replay.test.ts (36 tests) 2245ms + ✓ flows replay > --json is byte-identical across two CLI invocations (diff) 1629ms +stdout | tests/live-kernel.test.ts > built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI +LIVE_ANALYZER ready: claude -p --model claude-haiku-4-5-20251001 round-trip OK + + ✓ tests/tick-runner.test.ts (22 tests) 5740ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms fractional as an invocation error 987ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms exponent notation as an invocation error 782ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms hex as an invocation error 792ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms trailing text as an invocation error 888ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms empty as an invocation error 1153ms + ✓ CLI argument parsing refuses coercion rather than accepting it > accepts an exact integer and proceeds past parsing 819ms +stdout | tests/live-kernel.test.ts > built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI +LIVE_ANALYZER analysis: {"reasoning":"This story is directly relevant to AI agents and automation, describing an agent system that autonomously performs software development tasks (opening and reviewing pull requests), which is a core application of agentic AI systems.","relevance_score":10,"story_title":"Show HN: an agent that opens and reviews its own pull requests [wake-nonce-7f3a91c4]"} + + ✓ tests/stop-process-group.test.ts (6 tests) 7833ms + ✓ every stop reaches the process group, not just the direct child > exits the run after an execution-timeout stop 968ms + ✓ every stop reaches the process group, not just the direct child > exits the run after a protocol terminate stop 776ms + ✓ every stop reaches the process group, not just the direct child > kills a SIGTERM-deaf grandchild after a protocol terminate stop 2006ms + ✓ every stop reaches the process group, not just the direct child > kills a SIGTERM-deaf grandchild after an execution-timeout stop 2203ms + ✓ every stop reaches the process group, not just the direct child > holds the loop open long enough for the escalation to run 1393ms + ✓ every stop reaches the process group, not just the direct child > terminate() forces a group that outlives SIGTERM 487ms + ✓ tests/gate-contract.test.ts (20 tests) 385ms +stdout | tests/live-kernel.test.ts > surface resume after a real daemon kill > resumes a three-step run with each successful completion exactly once +LIVE_KERNEL kill -9 pid=88815 run=01M290SXJQ9EFTBB2HFXT2C6HT while step=two state=Running + + ✓ tests/cli-hn-monitor.test.ts (16 tests) 155ms + ✓ tests/live-kernel.test.ts (31 tests) 85264ms + ✓ built flows CLI against live relayflowd > twenty-six-step reuses 25 durable completions after editing the failed final step 6153ms + ✓ built flows CLI against live relayflowd > runs rung (a), parks rung (b), and keeps JSON report-shaped 8140ms + ✓ built flows CLI against live relayflowd > allows a deterministic run to exceed the bounded request timeout 33383ms + ✓ built flows CLI against live relayflowd > follows a live worker dispatch through flows run 1687ms + ✓ built flows CLI against live relayflowd > runs an agent CLI end to end through the SDK worker 492ms + ✓ built flows CLI against live relayflowd > f.agent lowers to a real agent step and dispatches through a live worker 848ms + ✓ built flows CLI against live relayflowd > f.agent's default flowPath anchors on cwd, not cwd's parent 835ms + ✓ built flows CLI against live relayflowd > can always get a parked run to a late-attaching worker 5712ms + ✓ built flows CLI against live relayflowd > reports a real manual-recovery NeedsHuman state as parked 1701ms + ✓ built flows CLI against live relayflowd > runs hn-monitor analyze-story end-to-end via a stub agent CLI (gate 2 clause 2 demo) 401ms + ✓ built flows CLI against live relayflowd > hn-monitor analyze-story FAILS verification when the CLI omits required schema fields 388ms + ✓ built flows CLI against live relayflowd > AgentWorker exposes wake_context to the CLI via RELAYFLOW_WAKE_CONTEXT env var (real analyzer prerequisite) 422ms + ✓ built flows CLI against live relayflowd > AgentWorker passes a declared model to an identified wrapper as RELAYFLOW_MODEL 814ms + ✓ built flows CLI against live relayflowd > AgentWorker refuses a nonconforming journal-submitted wrapper before exposing RELAYFLOW_MODEL 581ms + ✓ built flows CLI against live relayflowd > AgentWorker executes the raw claude adapter with its real model flag 446ms + ✓ built flows CLI against live relayflowd > AgentWorker executes the raw codex adapter with its real model flag 402ms + ✓ built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI 11818ms + ✓ built flows CLI against live relayflowd > preflights before journaling and names an unreachable socket 3033ms + ✓ built flows CLI against live relayflowd > starts exactly one daemon when two runs race for one empty data dir 2186ms + ✓ surface resume after a real daemon kill > resumes a three-step run with each successful completion exactly once 3305ms + ✓ a relayflow can be scheduled: tick source against live relayflowd > a tick spawns a real run whose step reports the SCHEDULED instant 385ms + ✓ tests/authored-flow-slack.test.ts (6 tests) 4011ms + ✓ authored Slack helper effects > journals exactly one effect with the authored params and typed receipt, without network 385ms + ✓ authored Slack helper effects > replays after SIGKILL before confirm with the same token and one successful completion 1378ms + ✓ authored Slack helper effects > replays after SIGKILL before complete with the same token and one successful completion 1216ms + ✓ authored Slack helper effects > writes two files for two calls and supports dm, reply, and react 990ms + ✓ tests/bundle.test.ts (21 tests) 15537ms + ✓ immutable bundles > returns exit 2 naming a byte-flipped payload and refuses to reuse corruption 792ms + ✓ immutable bundles > builds and verifies the canonical YAML fixture through the compiled CLI 2598ms + ✓ immutable bundles > emits the ephemeral warning on CLI stderr and uses the default output directory 1737ms + ✓ immutable bundles > preserves quoted asset words and executable permissions 346ms + ✓ immutable bundles > refuses build-provable CLI resolution errors without environment probes 745ms + ✓ immutable bundles > builds a standalone TS fixture twice with identical executable hashes 4762ms + ✓ immutable bundles > refuses to label installed dependency drift with lockfile pins 701ms + ✓ immutable bundles > refuses invalid CLI arguments %j 710ms + ✓ immutable bundles > refuses invalid CLI arguments "--out" 772ms + ✓ immutable bundles > refuses invalid CLI arguments "--verify" 688ms + ✓ immutable bundles > refuses invalid CLI arguments "--verify" 744ms + ✓ immutable bundles > refuses invalid CLI arguments "--out" 704ms + ✓ tests/daemon-lifecycle-live.test.ts (9 tests) 17465ms + ✓ flows run against a data dir with no daemon (§6 test 7) > cold start spawns exactly one daemon, the run succeeds, and the daemon outlives the CLI 1853ms + ✓ flows run against a data dir with no daemon (§6 test 7) > polls, bounded, for a daemon that holds the lock before it binds 2114ms + ✓ flows run against a data dir with no daemon (§6 test 7) > attaches to a serving daemon that has not published a connection file 1366ms + ✓ flows run against a data dir with no daemon (§6 test 7) > a second run attaches to the daemon the first one started, spawning nothing 2949ms + ✓ flows run against a data dir with no daemon (§6 test 7) > detects a stale connection file left by a hard kill and starts a fresh daemon 3104ms + ✓ concurrent invocations against one empty data dir (§6 test 15) > ends with exactly one daemon owning the socket, and both runs succeed 1833ms + ✓ refusals from a spawn that cannot produce a daemon > names relayflowd_not_found rather than falling through to PATH 1358ms + ✓ refusals from a spawn that cannot produce a daemon > names daemon_start_failed and quotes the daemon log when startup dies 1426ms + ✓ refusals from a spawn that cannot produce a daemon > refuses a daemon speaking another protocol version instead of binding over it 1459ms + ✓ tests/authored-helpers.test.ts (6 tests) 11228ms + ✓ lowers the named acceptance helpers and Slack to confirmed journal effects 1060ms + ✓ runs every available provider through the real kernel and resumes completed effects without a second write 7221ms + ✓ replays after SIGKILL before confirm with the same token and one successful completion 1232ms + ✓ replays after SIGKILL before complete with the same token and one successful completion 1157ms + ✓ journals provider failure with worker_error and never confirms the effect 546ms + ✓ tests/backlog-picker.test.ts (14 tests) 184ms + ✓ tests/backlog-picker-flow.test.ts (6 tests) 969ms + ✓ backlog-picker flow > emit-package describes the entry select-entry chose, even if the backlog changes between them 359ms + ✓ backlog-picker flow > does not emit a stale entry left by a previous run 434ms + ✓ tests/flow-executor-chain.test.ts (12 tests) 14212ms + ✓ flow executor LLM and output-binding chain > runs f.llm -> f.agent -> f.run with schema-verified journal output and the exact allowed model 1724ms + ✓ flow executor LLM and output-binding chain > fails invalid LLM output before the next step: not JSON 995ms + ✓ flow executor LLM and output-binding chain > fails invalid LLM output before the next step: {"message":7} 1011ms + ✓ flow executor LLM and output-binding chain > retains tagged-template text output 818ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: null 809ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: [1,2] 721ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: "hello" 931ms + ✓ flow executor LLM and output-binding chain > runs the authored LLM path through the built flows CLI 1920ms + ✓ flow executor LLM and output-binding chain > passes a declarative verified value through an agent into a deterministic artifact 1065ms + ✓ flow executor LLM and output-binding chain > journals a missing optional field as a failure before the consuming command executes 844ms + ✓ flow executor LLM and output-binding chain > flows run consumes YAML bindings and resume reuses the original journal output 3318ms + ✓ tests/authored-flow-operation.test.ts (23 tests) 643ms + ✓ completes the gate in linear time over a body with 30000 ordinary awaits 473ms + ✓ tests/direct-input.test.ts (4 tests) 15281ms + ✓ direct .flow.ts input through the built CLI and live runtime > executes inline and file JSON input through relayflowd 4115ms + ✓ direct .flow.ts input through the built CLI and live runtime > refuses missing and malformed input before contacting relayflowd 7872ms + ✓ direct .flow.ts input through the built CLI and live runtime > does not run the authored body before daemon availability 1883ms + ✓ direct .flow.ts input through the built CLI and live runtime > refuses oversized file input before contacting relayflowd 1409ms + ✓ tests/work-package-consumer.test.ts (13 tests) 755ms + ✓ the Garden join: picker output feeds the consumer > accepts a package the picker actually emits, and refuses one lacking a definition of done 742ms + ❯ tests/cli-watch.test.ts (10 tests | 2 failed) 32431ms + ✓ flows check --watch > rechecks syntax errors, clears once, and returns the last refusal on Ctrl-C 1796ms + × flows check --watch > streams JSON lines without ANSI, recovers after atomic saves, and exits zero after repair 5163ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + ✓ flows check --watch > coalesces 20 concurrent saves into at most two rechecks 2982ms + ✓ flows check --watch > watches transitive relative use imports, cycles, and nearest config changes 3819ms + × flows check --watch > refreshes the import graph and notices missing imports being created 5143ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + ✓ flows check --watch > reloads authored TypeScript instead of reusing the first imported definition 5160ms + ✓ flows check --watch > detects a nearer config appearing and falls back after it is deleted 3870ms + ✓ flows check --watch > keeps watching after the target is deleted and recreated 3688ms + ✓ flows check --watch > queues changes during a slow check without overlapping checks 804ms + ✓ tests/spec-parity.test.ts (31 tests) 881ms + ✓ tests/helpers-fanout.test.ts (96 tests) 877ms + ✓ tests/worker-lease.test.ts (7 tests) 16ms + ✓ tests/typed-output.test.ts (14 tests) 535ms + ✓ tests/webhook-live.test.ts (3 tests) 7722ms + ✓ flows serve-webhook writes JSON before the daemon starts, then journals and archives exactly once 2284ms + ✓ replays a dropped file after SIGKILL before spawn 999ms + ✓ resumes the same journal after SIGKILL after spawn and before acknowledgement 4437ms + ✓ tests/effect-channel.test.ts (5 tests) 670ms + ✓ tests/mcp-lifecycle.test.ts (4 tests) 14ms + ✓ tests/model-selection.test.ts (10 tests) 39ms + ✓ tests/json-schema-bound.test.ts (71 tests) 7573ms + ✓ JSON Schema termination bound > walks a deep schema with an explicit stack rather than recursion 6347ms + ✓ tests/relayflowd-path.test.ts (10 tests) 11ms + ✓ tests/f-memory.test.ts (7 tests) 2782ms + ✓ reads the seeded local SQLite database with cloud unused and fallback disabled 561ms + ✓ cannot widen script scope using a raw project option or another flow name 397ms + ✓ attaches read helpers without journaling read steps 375ms + ✓ refuses missing, corrupt, and directory database paths before body or journal activity 363ms + ✓ detects direct memory use before running earlier body effects 348ms + ✓ maps an unavailable provider probe to memory_unreachable 359ms + ✓ fails closed for deferred writes, agent scope, disabled script memory, and reads after done 377ms + ✓ tests/webhook.test.ts (6 tests) 181ms + ✓ tests/yaml-local-agent-live.test.ts (7 tests) 9924ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked step CLI and model and journals done 1254ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked named CLI and model and journals done 1435ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked flow CLI and model and journals done 1398ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked project CLI and model and journals done 1681ms + ✓ YAML --local-agent through the built CLI and real daemon > still parks without --local-agent 1354ms + ✓ YAML --local-agent through the built CLI and real daemon > reports the agent process failure 1507ms + ✓ YAML --local-agent through the built CLI and real daemon > preserves declared workspace surfaces that the local worker cannot pin 1294ms + ✓ tests/local-dev-ux.test.ts (8 tests) 87ms + ✓ tests/dependency-validation.test.ts (6 tests) 1609ms + ✓ dependency validation > accepts a valid 10,000-step reverse chain through every direct public boundary 760ms + ✓ dependency validation > bounds the author-facing path for a 10,000-step cycle 767ms + ✓ tests/input-binding.test.ts (12 tests) 860ms + ✓ declarative output binding > compiles YAML selectors into durable input and implicit dependency edges 381ms + ✓ tests/deterministic-llm.test.ts (5 tests) 81ms + ✓ tests/bin.test.ts (7 tests) 3148ms + ✓ built flows binary > refuses through a symlink to the built artifact 516ms + ✓ built flows binary > refuses through a symlinked directory component 410ms + ✓ built flows binary > classifies a signal-terminated auth probe as probe_failed 378ms + ✓ built flows binary > classifies an unavailable PATH resolver as probe_failed 415ms + ✓ built flows binary > does not describe a present non-executable CLI as missing 503ms + ✓ built flows binary > runs one auth probe for three steps sharing a flow CLI 923ms + ✓ tests/scope-compiler.test.ts (25 tests) 38ms + ✓ tests/budget-attribution.test.ts (4 tests) 11ms + ✓ tests/hn-poller.test.ts (6 tests) 15ms + ✓ tests/dir-watcher-poller.test.ts (6 tests) 5ms + ✓ tests/hello-deterministic.test.ts (5 tests) 53ms + ✓ tests/budget-preflight.test.ts (16 tests) 26ms + ✓ tests/work-package-validator.test.ts (7 tests) 14ms + ✓ tests/classify-outcome.test.ts (2 tests) 2242ms + ✓ classifyOutcome > gives up and reports when a running run never becomes classifiable 2081ms + ↓ tests/real-cli-adapters.test.ts (3 tests | 3 skipped) + ✓ tests/model-pricing.test.ts (8 tests) 10ms + ✓ tests/authored-use-loader.test.ts (5 tests) 127ms + ✓ tests/memoization.test.ts (57 tests) 821ms + ✓ refuses invalid reuse invocation "run" 686ms + ✓ tests/parse-json-output.test.ts (7 tests) 8ms + ✓ tests/cli-adapter.test.ts (3 tests) 11ms + ✓ tests/journal-client-completion.test.ts (4 tests) 124ms + ✓ tests/worker-cli-abort.test.ts (2 tests) 2777ms + ✓ stops claude and its process group when lease ownership is lost 1397ms + ✓ stops wrapper.mjs and its process group when lease ownership is lost 1379ms + ✓ tests/direct-run-failure.test.ts (6 tests) 17ms + ✓ tests/budget-authored-live.test.ts (2 tests) 754ms + ✓ authored budgets through the live kernel > retains the first completion and journals refusal of the next authored step 365ms + ✓ authored budgets through the live kernel > permits done after the last valid step crosses the limit 387ms + ✓ tests/local-agent-live.test.ts (5 tests) 45262ms + ✓ built CLI local agent against a real daemon > dispatches through the wrapper and keeps --json stdout report-shaped 1983ms + ✓ built CLI local agent against a real daemon > runs beyond the initial 30-second lease without a second invocation 36925ms + ✓ built CLI local agent against a real daemon > renders actual agent completion in text output 2472ms + ✓ built CLI local agent against a real daemon > returns a failed run when the agent process fails 2334ms + ✓ built CLI local agent against a real daemon > refuses a workspace it cannot pin before invoking the agent 1545ms + ✓ tests/slack-writeback.test.ts (1 test) 271ms + ✓ tests/placement.test.ts (54 tests) 42ms + ✓ tests/memory.test.ts (18 tests) 19ms + ✓ tests/cli-progress-wait.test.ts (2 tests) 1368ms + ✓ run starts the wait clock on its first observed lease 866ms + ✓ resume starts the wait clock on its first observed lease 501ms + ✓ tests/worker-platform.test.ts (1 test) 7ms + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/cli-watch.test.ts > flows check --watch > streams JSON lines without ANSI, recovers after atomic saves, and exits zero after repair + FAIL tests/cli-watch.test.ts > flows check --watch > refreshes the import graph and notices missing imports being created +Error: Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ + + Test Files 1 failed | 70 passed | 1 skipped (72) + Tests 2 failed | 1314 passed | 3 skipped (1319) + Start at 21:57:14 + Duration 210.15s (transform 2.98s, setup 0ms, collect 31.33s, tests 354.15s, environment 24ms, prepare 9.76s) + diff --git a/evidence/slice-S/sdk-tests.txt b/evidence/slice-S/sdk-tests.txt new file mode 100644 index 000000000..b0fdf0163 --- /dev/null +++ b/evidence/slice-S/sdk-tests.txt @@ -0,0 +1,513 @@ + +> @relayflows/sdk@2.0.8 test +> sh scripts/test.sh + + +> @relayflows/sdk@2.0.8 test:prep +> ( cd ../../kernel && sh ../ops/cargo.sh build ) && ( [ ! -d ../../testdata/preflight ] || find ../../testdata/preflight -name '*-cli' -type f -exec chmod +x {} + ) + + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.68s + +> @relayflows/sdk@2.0.8 typecheck +> tsc --noEmit && tsc -p tsconfig.type-tests.json + + +> @relayflows/sdk@2.0.8 build +> tsc && node scripts/make-cli-executable.mjs + + +> @relayflows/sdk@2.0.8 typecheck:tests +> tsc -p tsconfig.tests.json + + + RUN v2.1.9 /Users/khaliqgant/fl-slice-S/packages/sdk + +stdout | tests/live-kernel.test.ts +LIVE_KERNEL relayflowd=/Users/khaliqgant/.relayflows-toolchain/target/2945819964/debug/relayflowd +LIVE_KERNEL flows=/Users/khaliqgant/fl-slice-S/packages/sdk/dist/cli.js + + ✓ tests/validate.test.ts (68 tests) 135ms + ❯ tests/preflight.test.ts (27 tests | 1 failed) 306ms + × preflight: CLI resolution and refusal predicates > reaches every declared refusal kind, with the converse held by the type 69ms + → expected Set{ …(17) } to deeply equal Set{ 'invalid_spec', …(18) } + ✓ tests/daemon-lifecycle.test.ts (42 tests) 231ms + ✓ tests/observer-link.test.ts (39 tests) 656ms + ✓ tests/authored-flow.test.ts (25 tests) 1041ms + ✓ tests/journal-client.test.ts (14 tests) 226ms + ✓ tests/tick-source.test.ts (33 tests) 208ms + ✓ tests/cloud-run.test.ts (47 tests) 412ms + ✓ tests/authored-flow-lifecycle-executor.test.ts (27 tests) 838ms + ✓ tests/verb-field-lint.test.ts (78 tests) 1530ms + ✓ closed per-verb step fields > preserves a valid v0.1.0 ladder with every declared per-verb field 361ms + ✓ tests/cli-hn-monitor.test.ts (16 tests) 170ms + ✓ tests/gate-contract.test.ts (20 tests) 666ms + ✓ tests/cli-replay.test.ts (36 tests) 4351ms + ✓ flows replay > --json is byte-identical across two CLI invocations (diff) 3296ms + ✓ tests/tick-runner.test.ts (22 tests) 9034ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms fractional as an invocation error 1557ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms exponent notation as an invocation error 1485ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms hex as an invocation error 1623ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms trailing text as an invocation error 1254ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms empty as an invocation error 1393ms + ✓ CLI argument parsing refuses coercion rather than accepting it > accepts an exact integer and proceeds past parsing 1087ms + ❯ tests/mcp.test.ts (30 tests | 4 skipped) 14281ms + ✓ MCP preflight and transports > flows check refuses an undeclared server with exit 2 and no daemon 2153ms + ✓ MCP preflight and transports > flows check reports a refusing server and leaves no PID 1711ms + ✓ MCP preflight and transports > kills a SIGTERM-resistant silent child after a parent-owned handshake deadline 1316ms + ✓ MCP preflight and transports > reaps a SIGTERM-resistant descendant with inherit stdio before cleanup finishes 1498ms + ✓ MCP preflight and transports > reaps a SIGTERM-resistant descendant with ignore stdio before cleanup finishes 2069ms + ✓ MCP preflight and transports > reports malformed connection configuration as config_invalid 1435ms + ✓ tests/stop-process-group.test.ts (6 tests) 9161ms + ✓ every stop reaches the process group, not just the direct child > exits the run after an execution-timeout stop 1283ms + ✓ every stop reaches the process group, not just the direct child > exits the run after a protocol terminate stop 930ms + ✓ every stop reaches the process group, not just the direct child > kills a SIGTERM-deaf grandchild after a protocol terminate stop 2505ms + ✓ every stop reaches the process group, not just the direct child > kills a SIGTERM-deaf grandchild after an execution-timeout stop 2366ms + ✓ every stop reaches the process group, not just the direct child > holds the loop open long enough for the escalation to run 1528ms + ✓ every stop reaches the process group, not just the direct child > terminate() forces a group that outlives SIGTERM 547ms + ✓ tests/authored-flow-slack.test.ts (6 tests) 5065ms + ✓ authored Slack helper effects > journals exactly one effect with the authored params and typed receipt, without network 420ms + ✓ authored Slack helper effects > replays after SIGKILL before confirm with the same token and one successful completion 1713ms + ✓ authored Slack helper effects > replays after SIGKILL before complete with the same token and one successful completion 1598ms + ✓ authored Slack helper effects > writes two files for two calls and supports dm, reply, and react 1276ms + ✓ tests/backlog-picker.test.ts (14 tests) 282ms + ✓ tests/backlog-picker-flow.test.ts (6 tests) 2288ms + ✓ backlog-picker flow > emit-package describes the entry select-entry chose, even if the backlog changes between them 649ms + ✓ backlog-picker flow > does not emit a stale entry left by a previous run 1119ms + ✓ backlog-picker canonical spec > keeps directories and extensionless paths, and rejects prose 505ms + ✓ tests/cli.test.ts (63 tests) 17725ms + ✓ flows check CLI > binds a checked relative wrapper to the flow directory for worker execution 1004ms + ✓ flows check CLI > uses Codex login status and reports a rejected model as unavailable, not unauthenticated 549ms + ✓ flows check CLI > refuses a typo model before probing or contacting relayflowd 745ms + ✓ flows check CLI > passes all three canonical ladder flows and prints their resolved CLI 748ms + ✓ flows check CLI > resolves a project CLI path relative to the flows.json that declares it 494ms + ✓ flows check CLI > maps every input refusal path to its declared kind without raw exceptions 2005ms + ✓ flows run/resume CLI over the journal protocol > parses run options, submits the kernel dialect, and exits 0 on success 620ms + ✓ flows run/resume CLI over the journal protocol > exits 1 and emits the declared completionReason for a failed run 937ms + ✓ flows run/resume CLI over the journal protocol > exits 3 and names the parked llm step 1156ms + ✓ flows run/resume CLI over the journal protocol > reports a needs_human agent step as parked for human recovery 659ms + ✓ flows run/resume CLI over the journal protocol > classifies a typed hello refusal as a protocol error, not an unreachable daemon 600ms + ✓ flows run/resume CLI over the journal protocol > follows a dispatched worker step instead of reporting a protocol error 973ms + ✓ flows run/resume CLI over the journal protocol > bounds a worker wait by its lease and reports what it is waiting for 738ms + ✓ flows run/resume CLI over the journal protocol > resumes a parked run from snapshot step types without reading journal sequence one 693ms + ✓ flows run/resume CLI over the journal protocol > maps only run_not_found resumes to exit 2 1901ms + ✓ tests/authored-flow-operation.test.ts (23 tests) 1554ms + ✓ completes the gate in linear time over a body with 30000 ordinary awaits 1329ms + ✓ tests/worker-cli.test.ts (13 tests) 25059ms + ✓ custom wrapper execution identity > passes an explicit safe environment at identification and execution 534ms + ✓ custom wrapper execution identity > refuses a wrapper symlink retarget before delivering private values 475ms + ✓ custom wrapper execution identity > bounds wrapper execution after acknowledgement 1367ms + ✓ custom wrapper execution identity > bounds captured wrapper output 552ms + ✓ custom wrapper execution identity > refuses a duplicate execute protocol frame 575ms + ✓ custom wrapper execution bounds are reader-owned > resolves when a conforming wrapper leaks a stdio pipe to a background helper 2062ms + ✓ custom wrapper execution bounds are reader-owned > resolves when the leaked helper inherits stderr only 2044ms + ✓ custom wrapper execution bounds are reader-owned > resolves when a wrapper leaks a stdio pipe and exits before identifying 3261ms + ✓ custom wrapper execution bounds are reader-owned > journals a completionReason at the default bound when a wrapper leaks a stdio pipe 11272ms + ✓ custom wrapper execution bounds are reader-owned > accepts an execute token and an over-8KiB payload flushed in one write 546ms + ✓ custom wrapper execution bounds are reader-owned > accepts the same over-8KiB payload whether or not it coalesces with the execute token 1230ms + ✓ custom wrapper execution bounds are reader-owned > still bounds an un-terminated handshake buffer and names the bound 523ms + ✓ delivers the journaled memory pack to the real wrapper and excludes its charge from completion usage 611ms + ✓ tests/work-package-consumer.test.ts (13 tests) 1179ms + ✓ the Garden join: picker output feeds the consumer > accepts a package the picker actually emits, and refuses one lacking a definition of done 1141ms + ✓ tests/spec-parity.test.ts (31 tests) 1947ms + ✓ tests/helpers-fanout.test.ts (94 tests) 2578ms + ✓ tests/worker-lease.test.ts (7 tests) 69ms + ✓ tests/authored-helpers.test.ts (6 tests) 17292ms + ✓ lowers the named acceptance helpers and Slack to confirmed journal effects 1598ms + ✓ runs every available provider through the real kernel and resumes completed effects without a second write 10020ms + ✓ replays after SIGKILL before confirm with the same token and one successful completion 3046ms + ✓ replays after SIGKILL before complete with the same token and one successful completion 2005ms + ✓ journals provider failure with worker_error and never confirms the effect 610ms + ✓ tests/typed-output.test.ts (14 tests) 1231ms + ✓ typed llm and agent outputs > compiles llm output sugar to the existing json_schema primitive 343ms + ✓ tests/effect-channel.test.ts (5 tests) 960ms + ✓ tests/mcp-lifecycle.test.ts (4 tests) 53ms + ✓ tests/model-selection.test.ts (10 tests) 119ms + ✓ tests/bundle.test.ts (21 tests) 31809ms + ✓ immutable bundles > returns exit 2 naming a byte-flipped payload and refuses to reuse corruption 1492ms + ✓ immutable bundles > builds and verifies the canonical YAML fixture through the compiled CLI 3516ms + ✓ immutable bundles > emits the ephemeral warning on CLI stderr and uses the default output directory 3478ms + ✓ immutable bundles > preserves quoted asset words and executable permissions 328ms + ✓ immutable bundles > refuses build-provable CLI resolution errors without environment probes 2096ms + ✓ immutable bundles > builds a standalone TS fixture twice with identical executable hashes 10113ms + ✓ immutable bundles > refuses to label installed dependency drift with lockfile pins 1650ms + ✓ immutable bundles > refuses invalid CLI arguments %j 1523ms + ✓ immutable bundles > refuses invalid CLI arguments "--out" 1449ms + ✓ immutable bundles > refuses invalid CLI arguments "--verify" 2186ms + ✓ immutable bundles > refuses invalid CLI arguments "--verify" 1543ms + ✓ immutable bundles > refuses invalid CLI arguments "--out" 1910ms + ✓ tests/daemon-lifecycle-live.test.ts (9 tests) 32061ms + ✓ flows run against a data dir with no daemon (§6 test 7) > cold start spawns exactly one daemon, the run succeeds, and the daemon outlives the CLI 2355ms + ✓ flows run against a data dir with no daemon (§6 test 7) > polls, bounded, for a daemon that holds the lock before it binds 2742ms + ✓ flows run against a data dir with no daemon (§6 test 7) > attaches to a serving daemon that has not published a connection file 2448ms + ✓ flows run against a data dir with no daemon (§6 test 7) > a second run attaches to the daemon the first one started, spawning nothing 6032ms + ✓ flows run against a data dir with no daemon (§6 test 7) > detects a stale connection file left by a hard kill and starts a fresh daemon 7211ms + ✓ concurrent invocations against one empty data dir (§6 test 15) > ends with exactly one daemon owning the socket, and both runs succeed 2908ms + ✓ refusals from a spawn that cannot produce a daemon > names relayflowd_not_found rather than falling through to PATH 2356ms + ✓ refusals from a spawn that cannot produce a daemon > names daemon_start_failed and quotes the daemon log when startup dies 3116ms + ✓ refusals from a spawn that cannot produce a daemon > refuses a daemon speaking another protocol version instead of binding over it 2887ms + ✓ tests/relayflowd-path.test.ts (10 tests) 15ms + ✓ tests/direct-input.test.ts (4 tests) 25404ms + ✓ direct .flow.ts input through the built CLI and live runtime > executes inline and file JSON input through relayflowd 9848ms + ✓ direct .flow.ts input through the built CLI and live runtime > refuses missing and malformed input before contacting relayflowd 10133ms + ✓ direct .flow.ts input through the built CLI and live runtime > does not run the authored body before daemon availability 2889ms + ✓ direct .flow.ts input through the built CLI and live runtime > refuses oversized file input before contacting relayflowd 2527ms + ✓ tests/webhook.test.ts (6 tests) 427ms + ❯ tests/f-memory.test.ts (7 tests | 3 failed) 4753ms + ✓ reads the seeded local SQLite database with cloud unused and fallback disabled 977ms + ✓ cannot widen script scope using a raw project option or another flow name 589ms + × attaches read helpers without journaling read steps 726ms + → helper_provider.mount_required: f.recall requires a relayfile recall mount; direct-token transport is not implemented. + ✓ refuses missing, corrupt, and directory database paths before body or journal activity 584ms + × detects direct memory use before running earlier body effects 635ms + → expected AuthoredFlowExecutionError: helper_provid… { …(3) } to match object { code: 'memory_unreachable' } +(3 matching properties omitted from actual) + ✓ maps an unavailable provider probe to memory_unreachable 687ms + × fails closed for deferred writes, agent scope, disabled script memory, and reads after done 552ms + → expected AuthoredFlowExecutionError: helper_provid… { …(3) } to match object { code: 'operation_after_completion' } +(3 matching properties omitted from actual) + ✓ tests/local-dev-ux.test.ts (8 tests) 129ms + ✓ tests/json-schema-bound.test.ts (71 tests) 14395ms + ✓ JSON Schema termination bound > walks a deep schema with an explicit stack rather than recursion 12244ms + ✓ tests/flow-executor-chain.test.ts (12 tests) 30965ms + ✓ flow executor LLM and output-binding chain > runs f.llm -> f.agent -> f.run with schema-verified journal output and the exact allowed model 3404ms + ✓ flow executor LLM and output-binding chain > fails invalid LLM output before the next step: not JSON 2282ms + ✓ flow executor LLM and output-binding chain > fails invalid LLM output before the next step: {"message":7} 2539ms + ✓ flow executor LLM and output-binding chain > retains tagged-template text output 2036ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: null 2417ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: [1,2] 1931ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: "hello" 1996ms + ✓ flow executor LLM and output-binding chain > refuses a model outside flows.json before a run or adapter execution 452ms + ✓ flow executor LLM and output-binding chain > runs the authored LLM path through the built flows CLI 4610ms + ✓ flow executor LLM and output-binding chain > passes a declarative verified value through an agent into a deterministic artifact 2725ms + ✓ flow executor LLM and output-binding chain > journals a missing optional field as a failure before the consuming command executes 1918ms + ✓ flow executor LLM and output-binding chain > flows run consumes YAML bindings and resume reuses the original journal output 4651ms + ✓ tests/deterministic-llm.test.ts (5 tests) 283ms + ✓ tests/input-binding.test.ts (12 tests) 1274ms + ✓ declarative output binding > compiles YAML selectors into durable input and implicit dependency edges 331ms + ✓ tests/scope-compiler.test.ts (25 tests) 51ms + ✓ tests/hn-poller.test.ts (6 tests) 31ms + ✓ tests/budget-attribution.test.ts (4 tests) 16ms + ✓ tests/dependency-validation.test.ts (6 tests) 2709ms + ✓ dependency validation > accepts a valid 10,000-step reverse chain through every direct public boundary 1593ms + ✓ dependency validation > bounds the author-facing path for a 10,000-step cycle 944ms + ✓ tests/dir-watcher-poller.test.ts (6 tests) 34ms + ✓ tests/hello-deterministic.test.ts (5 tests) 85ms + ✓ tests/work-package-validator.test.ts (7 tests) 15ms + ✓ tests/budget-preflight.test.ts (16 tests) 50ms + ✓ tests/model-pricing.test.ts (8 tests) 19ms + ↓ tests/real-cli-adapters.test.ts (3 tests | 3 skipped) + ✓ tests/authored-use-loader.test.ts (5 tests) 200ms + ✓ tests/parse-json-output.test.ts (7 tests) 11ms + ✓ tests/cli-adapter.test.ts (3 tests) 14ms + ✓ tests/classify-outcome.test.ts (2 tests) 2260ms + ✓ classifyOutcome > gives up and reports when a running run never becomes classifiable 2094ms + ✓ tests/memoization.test.ts (57 tests) 883ms + ✓ refuses invalid reuse invocation "run" 634ms + ✓ tests/journal-client-completion.test.ts (4 tests) 133ms + ✓ tests/bin.test.ts (7 tests) 8330ms + ✓ built flows binary > refuses through a symlink to the built artifact 1466ms + ✓ built flows binary > refuses through a symlinked directory component 1508ms + ✓ built flows binary > classifies a signal-terminated auth probe as probe_failed 1866ms + ✓ built flows binary > classifies an unavailable PATH resolver as probe_failed 910ms + ✓ built flows binary > does not describe a present non-executable CLI as missing 1100ms + ✓ built flows binary > runs one auth probe for three steps sharing a flow CLI 1469ms + ✓ tests/direct-run-failure.test.ts (6 tests) 45ms + ✓ tests/worker-cli-abort.test.ts (2 tests) 3045ms + ✓ stops claude and its process group when lease ownership is lost 1453ms + ✓ stops wrapper.mjs and its process group when lease ownership is lost 1590ms + ✓ tests/budget-authored-live.test.ts (2 tests) 1513ms + ✓ authored budgets through the live kernel > retains the first completion and journals refusal of the next authored step 765ms + ✓ authored budgets through the live kernel > permits done after the last valid step crosses the limit 745ms + ✓ tests/slack-writeback.test.ts (1 test) 291ms + ✓ tests/memory.test.ts (18 tests) 25ms + ✓ tests/placement.test.ts (54 tests) 57ms + ✓ tests/worker-platform.test.ts (1 test) 11ms + ✓ tests/cli-progress-wait.test.ts (2 tests) 1248ms + ✓ run starts the wait clock on its first observed lease 752ms + ✓ resume starts the wait clock on its first observed lease 494ms + ✓ tests/yaml-local-agent-live.test.ts (7 tests) 15889ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked step CLI and model and journals done 2363ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked named CLI and model and journals done 2667ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked flow CLI and model and journals done 2658ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked project CLI and model and journals done 1740ms + ✓ YAML --local-agent through the built CLI and real daemon > still parks without --local-agent 1829ms + ✓ YAML --local-agent through the built CLI and real daemon > reports the agent process failure 2848ms + ✓ YAML --local-agent through the built CLI and real daemon > preserves declared workspace surfaces that the local worker cannot pin 1781ms + ❯ tests/webhook-live.test.ts (3 tests | 3 failed) 35448ms + × flows serve-webhook writes JSON before the daemon starts, then journals and archives exactly once 11955ms + → webhook integration timed out: spawn /Users/khaliqgant/fl-slice-S/kernel/target/debug/relayflowd ENOENT + × replays a dropped file after SIGKILL before spawn 11895ms + → webhook integration timed out: spawn /Users/khaliqgant/fl-slice-S/kernel/target/debug/relayflowd ENOENT + × resumes the same journal after SIGKILL after spawn and before acknowledgement 11593ms + → webhook integration timed out: spawn /Users/khaliqgant/fl-slice-S/kernel/target/debug/relayflowd ENOENT + ❯ tests/cli-watch.test.ts (10 tests | 7 failed) 46106ms + × flows check --watch > rechecks syntax errors, clears once, and returns the last refusal on Ctrl-C 7383ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + × flows check --watch > streams JSON lines without ANSI, recovers after atomic saves, and exits zero after repair 5628ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + × flows check --watch > coalesces 20 concurrent saves into at most two rechecks 6078ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + × flows check --watch > watches transitive relative use imports, cycles, and nearest config changes 5633ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + × flows check --watch > refreshes the import graph and notices missing imports being created 5063ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + × flows check --watch > reloads authored TypeScript instead of reusing the first imported definition 5130ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + × flows check --watch > detects a nearer config appearing and falls back after it is deleted 5479ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + ✓ flows check --watch > keeps watching after the target is deleted and recreated 4895ms + ✓ flows check --watch > queues changes during a slow check without overlapping checks 799ms +stdout | tests/live-kernel.test.ts > built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI +LIVE_ANALYZER ready: claude -p --model claude-haiku-4-5-20251001 round-trip OK + +stdout | tests/live-kernel.test.ts > built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI +LIVE_ANALYZER analysis: {"reasoning":"This story is directly relevant to AI agents and automation as it describes an autonomous agent performing software development workflows including opening and reviewing pull requests, exemplifying practical agent-driven automation in development operations.","relevance_score":9,"story_title":"Show HN: an agent that opens and reviews its own pull requests [wake-nonce-7f3a91c4]"} + + ✓ tests/local-agent-live.test.ts (5 tests) 46375ms + ✓ built CLI local agent against a real daemon > dispatches through the wrapper and keeps --json stdout report-shaped 3445ms + ✓ built CLI local agent against a real daemon > runs beyond the initial 30-second lease without a second invocation 37410ms + ✓ built CLI local agent against a real daemon > renders actual agent completion in text output 2066ms + ✓ built CLI local agent against a real daemon > returns a failed run when the agent process fails 2027ms + ✓ built CLI local agent against a real daemon > refuses a workspace it cannot pin before invoking the agent 1423ms +stdout | tests/live-kernel.test.ts > surface resume after a real daemon kill > resumes a three-step run with each successful completion exactly once +LIVE_KERNEL kill -9 pid=44112 run=01M29084XKR3WMXDY5FDX3Z49H while step=two state=Running + + ❯ tests/live-kernel.test.ts (31 tests | 2 failed) 95283ms + × built flows CLI against live relayflowd > twenty-six-step reuses 25 durable completions after editing the failed final step 8531ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + ✓ built flows CLI against live relayflowd > runs rung (a), parks rung (b), and keeps JSON report-shaped 12969ms + ✓ built flows CLI against live relayflowd > allows a deterministic run to exceed the bounded request timeout 35120ms + ✓ built flows CLI against live relayflowd > follows a live worker dispatch through flows run 2669ms + ✓ built flows CLI against live relayflowd > runs an agent CLI end to end through the SDK worker 706ms + ✓ built flows CLI against live relayflowd > f.agent lowers to a real agent step and dispatches through a live worker 986ms + ✓ built flows CLI against live relayflowd > f.agent's default flowPath anchors on cwd, not cwd's parent 1028ms + ✓ built flows CLI against live relayflowd > can always get a parked run to a late-attaching worker 5683ms + ✓ built flows CLI against live relayflowd > reports a real manual-recovery NeedsHuman state as parked 1494ms + ✓ built flows CLI against live relayflowd > runs hn-monitor analyze-story end-to-end via a stub agent CLI (gate 2 clause 2 demo) 616ms + ✓ built flows CLI against live relayflowd > hn-monitor analyze-story FAILS verification when the CLI omits required schema fields 657ms + ✓ built flows CLI against live relayflowd > agent step preserves the CliResult wrapper as output when the CLI emits non-JSON text 528ms + ✓ built flows CLI against live relayflowd > AgentWorker exposes wake_context to the CLI via RELAYFLOW_WAKE_CONTEXT env var (real analyzer prerequisite) 588ms + ✓ built flows CLI against live relayflowd > AgentWorker leaves RELAYFLOW_WAKE_CONTEXT UNSET when the run has no wake_context (undefined-vs-null pin) 479ms + ✓ built flows CLI against live relayflowd > AgentWorker passes a declared model to an identified wrapper as RELAYFLOW_MODEL 795ms + ✓ built flows CLI against live relayflowd > AgentWorker refuses a nonconforming journal-submitted wrapper before exposing RELAYFLOW_MODEL 517ms + ✓ built flows CLI against live relayflowd > AgentWorker executes the raw claude adapter with its real model flag 350ms + ✓ built flows CLI against live relayflowd > AgentWorker executes the raw codex adapter with its real model flag 340ms + ✓ built flows CLI against live relayflowd > AgentWorker leaves RELAYFLOW_MODEL UNSET when the step declares no model 342ms + ✓ built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI 11871ms + ✓ built flows CLI against live relayflowd > preflights before journaling and names an unreachable socket 2788ms + × built flows CLI against live relayflowd > starts exactly one daemon when two runs race for one empty data dir 1531ms + → WARNING [unprovable_effects] Step "greet" command "echo" resolves, but its effects cannot be proven before execution. +WARNING [unprovable_effects] Step "shout" command "echo" resolves, but its effects cannot be proven before execution. +WARNING [editor_schema_missing] For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json +REFUSED [relayflowd_not_found] No relayflowd binary could be found. Install the runtime package for this host (@relayflows/runtime-darwin-arm64), or set RELAYFLOWD_BIN to a relayflowd executable. Tried: /Users/khaliqgant/fl-slice-S/packages/sdk/dist/relayflowd. +: expected 2 to be +0 // Object.is equality + ✓ surface resume after a real daemon kill > resumes a three-step run with each successful completion exactly once 2851ms + ✓ a relayflow can be scheduled: tick source against live relayflowd > a tick spawns a real run whose step reports the SCHEDULED instant 603ms + +⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/mcp.test.ts > authored MCP effects against the real kernel +Error: journal client: connect failed: connect ENOENT /var/folders/yv/nbp9l2c55wlbj1x0gml37s7c0000gn/T/relayflowd-edbd934b342d.sock + ❯ Socket.onError src/journal-client.ts:99:16 + 97| socket.removeAllListeners(); + 98| this.failAll(err); + 99| reject(new Error(`journal client: connect failed: ${err.messag… + | ^ + 100| }; + 101| socket.once('error', onError); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/17]⎯ + +⎯⎯⎯⎯⎯⎯ Failed Tests 16 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/cli-watch.test.ts > flows check --watch > rechecks syntax errors, clears once, and returns the last refusal on Ctrl-C + FAIL tests/cli-watch.test.ts > flows check --watch > streams JSON lines without ANSI, recovers after atomic saves, and exits zero after repair + FAIL tests/cli-watch.test.ts > flows check --watch > coalesces 20 concurrent saves into at most two rechecks + FAIL tests/cli-watch.test.ts > flows check --watch > watches transitive relative use imports, cycles, and nearest config changes + FAIL tests/cli-watch.test.ts > flows check --watch > refreshes the import graph and notices missing imports being created + FAIL tests/cli-watch.test.ts > flows check --watch > reloads authored TypeScript instead of reusing the first imported definition + FAIL tests/cli-watch.test.ts > flows check --watch > detects a nearer config appearing and falls back after it is deleted + FAIL tests/live-kernel.test.ts > built flows CLI against live relayflowd > twenty-six-step reuses 25 durable completions after editing the failed final step +Error: Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/17]⎯ + + FAIL tests/f-memory.test.ts > attaches read helpers without journaling read steps +AuthoredFlowExecutionError: helper_provider.mount_required: f.recall requires a relayfile recall mount; direct-token transport is not implemented. + ❯ Module.executeAuthoredFlow src/authored-flow-executor.ts:145:11 + 143| if (!helperPreflight.ok) { + 144| const diagnostic = helperPreflight.diagnostics[0]!; + 145| throw new AuthoredFlowExecutionError(diagnostic.kind as AuthoredFl… + | ^ + 146| } + 147| if (headerFields.length > 0) { + ❯ tests/f-memory.test.ts:62:24 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/17]⎯ + + FAIL tests/f-memory.test.ts > detects direct memory use before running earlier body effects +AssertionError: expected AuthoredFlowExecutionError: helper_provid… { …(3) } to match object { code: 'memory_unreachable' } +(3 matching properties omitted from actual) + +- Expected ++ Received + +- Object { +- "code": "memory_unreachable", ++ AuthoredFlowExecutionError { ++ "code": "helper_provider.mount_required", + } + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/17]⎯ + + FAIL tests/f-memory.test.ts > fails closed for deferred writes, agent scope, disabled script memory, and reads after done +AssertionError: expected AuthoredFlowExecutionError: helper_provid… { …(3) } to match object { code: 'operation_after_completion' } +(3 matching properties omitted from actual) + +- Expected ++ Received + +- Object { +- "code": "operation_after_completion", ++ AuthoredFlowExecutionError { ++ "code": "helper_provider.mount_required", + } + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/17]⎯ + + FAIL tests/live-kernel.test.ts > built flows CLI against live relayflowd > starts exactly one daemon when two runs race for one empty data dir +AssertionError: WARNING [unprovable_effects] Step "greet" command "echo" resolves, but its effects cannot be proven before execution. +WARNING [unprovable_effects] Step "shout" command "echo" resolves, but its effects cannot be proven before execution. +WARNING [editor_schema_missing] For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json +REFUSED [relayflowd_not_found] No relayflowd binary could be found. Install the runtime package for this host (@relayflows/runtime-darwin-arm64), or set RELAYFLOWD_BIN to a relayflowd executable. Tried: /Users/khaliqgant/fl-slice-S/packages/sdk/dist/relayflowd. +: expected 2 to be +0 // Object.is equality + +- Expected ++ Received + +- 0 ++ 2 + + ❯ tests/live-kernel.test.ts:1388:40 + 1386| ]); + 1387| + 1388| expect(first.status, first.stderr).toBe(0); + | ^ + 1389| expect(second.status, second.stderr).toBe(0); + 1390| expect(first.stdout).toContain('completionReason: success'); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/17]⎯ + + FAIL tests/preflight.test.ts > preflight: CLI resolution and refusal predicates > reaches every declared refusal kind, with the converse held by the type +AssertionError: expected Set{ …(17) } to deeply equal Set{ 'invalid_spec', …(18) } + +- Expected ++ Received + + Set { + "budget_missing_price", + "budget_syntax_invalid", + "cli_missing", + "cli_unauthenticated", + "cli_unresolved", + "cli_unsupported", + "command_missing", +- "helper_provider.mount_required", +- "helper_provider.unsupported", + "helper_slack.credential_missing", + "helper_slack.mount_required", + "invalid_spec", + "mcp_undeclared_server", + "mcp_unreachable", + "memory_unreachable", + "model_unavailable", + "model_unknown", + "no_executor", + "probe_failed", + } + + ❯ tests/preflight.test.ts:398:35 + 396| refusalKinds.push(memoryRefusal!.kind); + 397| expect(JSON.stringify(memoryRefusal)).not.toContain('raw secret'); + 398| expect(new Set(refusalKinds)).toEqual(new Set(PREFLIGHT_FAILURE_KI… + | ^ + 399| expect(JSON.stringify(scenarios)).not.toContain('raw secret'); + 400| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[7/17]⎯ + + FAIL tests/webhook-live.test.ts > flows serve-webhook writes JSON before the daemon starts, then journals and archives exactly once +Error: webhook integration timed out: spawn /Users/khaliqgant/fl-slice-S/kernel/target/debug/relayflowd ENOENT + ❯ until tests/webhook-live.test.ts:36:9 + 34| const deadline = Date.now() + 10_000; + 35| while (Date.now() < deadline) { if (await predicate()) return; await… + 36| throw new Error(`webhook integration timed out: ${detail()}`); + | ^ + 37| } + 38| async function daemon(dir: string): Promise { + ❯ daemon tests/webhook-live.test.ts:40:3 + ❯ tests/webhook-live.test.ts:82:3 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[8/17]⎯ + + FAIL tests/webhook-live.test.ts > replays a dropped file after SIGKILL before spawn +Error: webhook integration timed out: spawn /Users/khaliqgant/fl-slice-S/kernel/target/debug/relayflowd ENOENT + ❯ until tests/webhook-live.test.ts:36:9 + 34| const deadline = Date.now() + 10_000; + 35| while (Date.now() < deadline) { if (await predicate()) return; await… + 36| throw new Error(`webhook integration timed out: ${detail()}`); + | ^ + 37| } + 38| async function daemon(dir: string): Promise { + ❯ daemon tests/webhook-live.test.ts:40:3 + ❯ tests/webhook-live.test.ts:98:17 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[9/17]⎯ + + FAIL tests/webhook-live.test.ts > resumes the same journal after SIGKILL after spawn and before acknowledgement +Error: webhook integration timed out: spawn /Users/khaliqgant/fl-slice-S/kernel/target/debug/relayflowd ENOENT + ❯ until tests/webhook-live.test.ts:36:9 + 34| const deadline = Date.now() + 10_000; + 35| while (Date.now() < deadline) { if (await predicate()) return; await… + 36| throw new Error(`webhook integration timed out: ${detail()}`); + | ^ + 37| } + 38| async function daemon(dir: string): Promise { + ❯ daemon tests/webhook-live.test.ts:40:3 + ❯ tests/webhook-live.test.ts:111:17 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[10/17]⎯ + +⎯⎯⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯⎯ + +Vitest caught 1 unhandled error during the test run. +This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected. + +⎯⎯⎯⎯⎯ Uncaught Exception ⎯⎯⎯⎯⎯ +Error: spawn /Users/khaliqgant/fl-slice-S/kernel/target/release/relayflowd ENOENT + ❯ Process.ChildProcess._handle.onexit node:internal/child_process:286:19 + ❯ onErrorNT node:internal/child_process:524:16 + ❯ processTicksAndRejections node:internal/process/task_queues:90:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ +Serialized Error: { errno: -2, code: 'ENOENT', syscall: 'spawn /Users/khaliqgant/fl-slice-S/kernel/target/release/relayflowd', path: '/Users/khaliqgant/fl-slice-S/kernel/target/release/relayflowd', spawnargs: [ '--data-dir', '/var/folders/yv/nbp9l2c55wlbj1x0gml37s7c0000gn/T/flows-mcp-daemon-l2Zu58', 'serve' ] } +This error originated in "tests/mcp.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. +The latest test that might've caused the error is "authored MCP effects against the real kernel". It might mean one of the following: +- The error was thrown, while Vitest was running this test. +- If the error occurred after the test had been completed, this was the last documented test before it was thrown. +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ + + Test Files 6 failed | 65 passed | 1 skipped (72) + Tests 16 failed | 1294 passed | 7 skipped (1317) + Errors 1 error + Start at 21:47:18 + Duration 101.03s (transform 9.25s, setup 0ms, collect 84.94s, tests 522.34s, environment 44ms, prepare 17.39s) + diff --git a/evidence/slice-S/sdk-watch-retry.txt b/evidence/slice-S/sdk-watch-retry.txt new file mode 100644 index 000000000..720aadd44 --- /dev/null +++ b/evidence/slice-S/sdk-watch-retry.txt @@ -0,0 +1,19 @@ + + RUN v2.1.9 /Users/khaliqgant/fl-slice-S/packages/sdk + + ✓ tests/cli-watch.test.ts (10 tests) 28592ms + ✓ flows check --watch > rechecks syntax errors, clears once, and returns the last refusal on Ctrl-C 2560ms + ✓ flows check --watch > streams JSON lines without ANSI, recovers after atomic saves, and exits zero after repair 3579ms + ✓ flows check --watch > coalesces 20 concurrent saves into at most two rechecks 2996ms + ✓ flows check --watch > watches transitive relative use imports, cycles, and nearest config changes 4517ms + ✓ flows check --watch > refreshes the import graph and notices missing imports being created 4436ms + ✓ flows check --watch > reloads authored TypeScript instead of reusing the first imported definition 2660ms + ✓ flows check --watch > detects a nearer config appearing and falls back after it is deleted 3600ms + ✓ flows check --watch > keeps watching after the target is deleted and recreated 3442ms + ✓ flows check --watch > queues changes during a slow check without overlapping checks 796ms + + Test Files 1 passed (1) + Tests 10 passed (10) + Start at 22:02:45 + Duration 31.19s (transform 1.07s, setup 0ms, collect 2.14s, tests 28.59s, environment 0ms, prepare 127ms) + diff --git a/evidence/slice-S/surface-regressions.txt b/evidence/slice-S/surface-regressions.txt new file mode 100644 index 000000000..a8f1eede4 --- /dev/null +++ b/evidence/slice-S/surface-regressions.txt @@ -0,0 +1,5 @@ + +> @relayflows/surface@2.0.8 typecheck:regressions +> tsc -p ../../regressions/tsconfig.json && tsc -p tsconfig.test.json && node scripts/check-generated-helpers.mjs + +HELPERS_GENERATED_OK airtable.ts, asana.ts, azure-blob.ts, box.ts, calendly.ts, clickup.ts, clients.ts, cloudflare.ts, confluence.ts, daytona.ts, docker-hub.ts, dropbox.ts, fathom.ts, gcp.ts, gcs.ts, github.ts, gitlab.ts, gmail.ts, google-calendar.ts, google-drive.ts, granola.ts, hubspot.ts, index.ts, intercom.ts, jira.ts, linear.ts, mailgun.ts, mixpanel.ts, neon.ts, notion.ts, onedrive.ts, pipedrive.ts, postgres.ts, posthog.ts, providers.ts, ramp.ts, recall.ts, reddit.ts, redis.ts, s3.ts, salesforce.ts, segment.ts, sendgrid.ts, sharepoint.ts, shopify.ts, shortcut.ts, slack.ts, stripe.ts, teams.ts, telegram.ts, webhook-server.ts, x.ts, zendesk.ts diff --git a/evidence/slice-S/surface-tests.txt b/evidence/slice-S/surface-tests.txt new file mode 100644 index 000000000..ae23729b3 --- /dev/null +++ b/evidence/slice-S/surface-tests.txt @@ -0,0 +1,18 @@ + +> @relayflows/surface@2.0.8 test +> bun run build && tsc -p tsconfig.test.json && vitest run + +$ tsc + + RUN v2.1.9 /Users/khaliqgant/fl-slice-S/packages/surface + + ✓ tests/triggers.test.ts (4 tests) 21ms + ✓ tests/flow.test.ts (20 tests) 26ms + ✓ tests/helpers.snapshot.test.ts (1 test) 994ms + ✓ regenerates helpers byte-identically from the pinned adapter 993ms + + Test Files 3 passed (3) + Tests 25 passed (25) + Start at 21:56:57 + Duration 2.09s (transform 474ms, setup 0ms, collect 1.41s, tests 1.04s, environment 1ms, prepare 747ms) + diff --git a/packages/sdk/src/authored-flow-error.ts b/packages/sdk/src/authored-flow-error.ts index 664f0d071..7f38eef25 100644 --- a/packages/sdk/src/authored-flow-error.ts +++ b/packages/sdk/src/authored-flow-error.ts @@ -4,6 +4,8 @@ import type { } from './protocol.js'; export type AuthoredFlowExecutionErrorCode = + | 'helper_provider.mount_required' + | 'helper_provider.unsupported' | 'helper_slack.credential_missing' | 'helper_slack.mount_required' | 'budget_syntax_invalid' diff --git a/packages/sdk/src/authored-flow-executor.ts b/packages/sdk/src/authored-flow-executor.ts index 9a4cfc034..f04b868b8 100644 --- a/packages/sdk/src/authored-flow-executor.ts +++ b/packages/sdk/src/authored-flow-executor.ts @@ -2,7 +2,7 @@ import { pluginHelpers } from './plugin-loader.js'; import { runPluginEffect } from './authored-plugin-effect.js'; import { randomUUID } from 'node:crypto'; import { dirname } from 'node:path'; -import { assertSlackCredentials, runSlackEffect } from './authored-slack-effect.js'; +import { runHelperEffect } from './authored-helper-effect.js'; import { checkSlackHelpers } from './slack-preflight.js'; import { snapshotJsonValue } from './json-value.js'; import type { SlackCall } from './slack-writeback.js'; @@ -21,7 +21,7 @@ import { type RunCompletionReason as SurfaceRunCompletionReason, type Step, } from '@relayflows/surface'; -import type { FlowHandle } from '@relayflows/surface/runtime'; +import { createHelpers, helperProviders, type HelperCall, type FlowHandle } from '@relayflows/surface/runtime'; import { join } from 'node:path'; import { observeStep, type ProgressEvent } from './progress.js'; import { parseStepTimeout } from './compile.js'; @@ -139,10 +139,13 @@ export async function executeAuthoredFlow( }; const definition = getDefinition(handle); const headerFields = Object.keys(definition.header).filter(key => key !== 'tools' && key !== 'budget' && key !== 'memory'); - if (definition.header.tools && Object.keys(definition.header.tools).some(key => !['slack', 'mcp'].includes(key))) headerFields.push('tools'); + if (definition.header.tools && Object.keys(definition.header.tools).some(key => !['mcp', ...helperProviders.map(p => p.namespace)].includes(key))) headerFields.push('tools'); if (definition.header.tools?.relayfile !== undefined) headerFields.push('tools.relayfile'); const helperPreflight = checkSlackHelpers(definition); - if (!helperPreflight.ok) assertSlackCredentials(); + if (!helperPreflight.ok) { + const diagnostic = helperPreflight.diagnostics[0]!; + throw new AuthoredFlowExecutionError(diagnostic.kind as AuthoredFlowExecutionErrorCode, diagnostic.message); + } if (headerFields.length > 0) { throw new AuthoredFlowExecutionError( 'unsupported_header', @@ -205,7 +208,7 @@ export async function executeAuthoredFlow( id, `slack.${call.verb}`, () => assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion), async () => { - const receipt = await runSlackEffect(journal, definition.name, id, snapshot, + const receipt = await runHelperEffect(journal, definition.name, id, snapshot, options.dataDir ?? dirname(journal.socketPath), journalSteps); return (call.verb === 'react' ? undefined : receipt) as T; }, @@ -214,6 +217,17 @@ export async function executeAuthoredFlow( } const context: Ctx = { + ...createHelpers((call: HelperCall): Step => { + const verb = `${call.provider}.${call.verb}`; + assertOperationAllowed(verb, definition.name, requestedCompletion); + const snapshot = snapshotJsonValue(call, `f.${verb} call`) as unknown as HelperCall; + const id = `${call.provider}-${slackRun}-${nextStep++}`; + return trackStep(authoredSteps, new AuthoredFlowOperation(id, verb, + () => assertOperationAllowed(verb, definition.name, requestedCompletion), + async () => await runHelperEffect(journal, definition.name, id, snapshot, + options.dataDir ?? dirname(journal.socketPath), journalSteps) as T, + lifecycle)); + }), slack: { post: (channel, text, opts) => slackOperation({ type: 'effect', provider: 'slack', verb: 'post', params: { channel, text, ...(opts === undefined ? {} : { opts }) } }), dm: (user, text) => slackOperation({ type: 'effect', provider: 'slack', verb: 'dm', params: { user, text } }), diff --git a/packages/sdk/src/authored-helper-effect.ts b/packages/sdk/src/authored-helper-effect.ts new file mode 100644 index 000000000..22f84d9db --- /dev/null +++ b/packages/sdk/src/authored-helper-effect.ts @@ -0,0 +1,143 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { randomUUID } from 'node:crypto'; +import { JournalClient } from './journal-client.js'; +import { AuthoredFlowExecutionError, type AuthoredFlowExecutionErrorCode } from './authored-flow-error.js'; +import { readCompletedStepOutput } from './authored-step-output.js'; +import type { AuthoredFlowJournalStep } from './authored-flow-executor.js'; +import { compileSpec, toKernelSpec } from './compile.js'; +import { SPEC_SCHEMA_VERSION, type KernelAgentStep } from './spec.js'; +import type { StepDispatchEvent } from './protocol.js'; +import { withWorkerLease } from './worker-lease.js'; +import { helperProviders, type HelperCall } from '@relayflows/surface/runtime'; +import { helperWriteback, HelperDeliveryError } from './helper-writeback.js'; +import { checkSlackHelpers } from './slack-preflight.js'; +import { atomicJson, readSlackReceipt, receiptPath, slackWriteback, type SlackCall } from './slack-writeback.js'; + +type ProviderCall = HelperCall | SlackCall; + +export function assertHelperCredentials(provider: string): void { + const namespace = helperProviders.find(p => p.provider === provider)?.namespace; + if (!namespace) throw new AuthoredFlowExecutionError('helper_provider.unsupported', `Unknown helper ${provider}`); + const report = checkSlackHelpers({ header: { tools: { [namespace]: true } }, body() {} }); + if (!report.ok) { + const diagnostic = report.diagnostics[0]!; + throw new AuthoredFlowExecutionError( + diagnostic.kind as AuthoredFlowExecutionErrorCode, diagnostic.message); + } +} + +/** Each helper uses the existing agent lease + effect protocol; no new kernel verb. */ +export async function runHelperEffect( + journal: JournalClient, name: string, stepId: string, call: ProviderCall, + dataDir: string, journalSteps: AuthoredFlowJournalStep[], +): Promise { + assertHelperCredentials(call.provider); + if (call.provider === 'notion' && call.verb === 'appendBlock' && process.env.RELAYFLOWS_NOTION_MOCK !== '1') { + throw new AuthoredFlowExecutionError('helper_provider.unsupported', 'Notion appendBlock has no upstream mount writeback route'); + } + const stream = `${call.provider}-helper-${randomUUID()}`; + const spec = toKernelSpec(compileSpec({ + version: SPEC_SCHEMA_VERSION, name: `${name}/${stepId}`, + steps: [{ id: stepId, type: 'agent', instruction: JSON.stringify(call), + maxIterations: 3, recoveryMode: 'reset', + surfaces: { streams: [{ stream }], external: [`/${call.provider}`] } }], + })); + const outcome = await journal.runStart(spec); + await atomicJson(join(dataDir, 'helper-runs', `${outcome.run_id}.json`), { provider: call.provider }); + await driveHelperEffect(journal, outcome.run_id, spec.steps[0] as KernelAgentStep, call, dataDir); + const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps); + return (output as { receipt: unknown }).receipt; +} + +/** Recognize only the journaled helper envelope, so ordinary agent runs keep their worker path. */ +export async function resumeHelperEffect(journal: JournalClient, runId: string, dataDir: string): Promise { + if (!/^[A-Za-z0-9_-]+$/.test(runId) || !existsSync(join(dataDir, 'helper-runs', `${runId}.json`))) return false; + const entries = (await journal.journalRead(runId, 1)).entries as Array<{ + entry_type: string; payload: { spec?: { steps?: KernelAgentStep[] } }; + }>; + const steps = entries.find(entry => entry.entry_type === 'run.spawned')?.payload.spec?.steps; + if (steps?.length !== 1) return false; + const step = steps[0]!; + if (step.type !== 'agent' || !step.surfaces?.streams?.[0]?.stream.includes('-helper-')) return false; + const call = JSON.parse(step.instruction) as ProviderCall; + if (call.type !== 'effect' || !helperProviders.some(p => p.provider === call.provider && p.supported)) return false; + const snapshot = await journal.runGet(runId); + if (snapshot.status === 'completed' || snapshot.status === 'failed') return true; + assertHelperCredentials(call.provider); + await driveHelperEffect(journal, runId, step, call, dataDir); + return true; +} + +async function driveHelperEffect( + journal: JournalClient, runId: string, step: KernelAgentStep, call: ProviderCall, dataDir: string, +): Promise { + const client = new JournalClient(journal.socketPath); + const stream = step.surfaces!.streams![0]!.stream; + const pins = { workspace: [], streams: [{ stream, read_offset: 0 }] }; + let resolve!: () => void; + let reject!: (error: unknown) => void; + const completed = new Promise((yes, no) => { resolve = yes; reject = no; }); + // Observe early dispatch failures even while attach/run.resume is still pending. + void completed.catch(() => undefined); + let executing = false; + client.on('step.dispatch', (dispatch: StepDispatchEvent) => { + if (executing || dispatch.run_id !== runId || dispatch.step_id !== step.id) return; + executing = true; + void completeHelperDispatch(client, dispatch, call, dataDir).then(resolve, reject); + }); + client.on('error', reject); + try { + await client.connect(); + await client.hello('flows-provider-helper'); + await client.workerAttach(`${call.provider}-${randomUUID()}`, ['agent'], pins, 1); + const outcome = await journal.runResume(runId); + if (outcome.status === 'completed') return; + if (outcome.status === 'failed') { + throw new AuthoredFlowExecutionError('step_failed', `Helper run ${runId} ${outcome.status}`, undefined, runId); + } + await completed; + } catch (error) { + if (error instanceof AuthoredFlowExecutionError) throw error; + throw new AuthoredFlowExecutionError('step_failed', error instanceof Error ? error.message : 'Helper effect failed', 'worker_error', runId); + } finally { client.close(); } +} + +async function completeHelperDispatch(client: JournalClient, dispatch: StepDispatchEvent, call: ProviderCall, dataDir: string): Promise { + let output: unknown; + try { + output = await withWorkerLease(client, dispatch, async signal => { + const file = receiptPath(dataDir, dispatch.run_id, dispatch.step_id); + let receipt: unknown; + await client.performEffect({ + runId: dispatch.run_id, stepId: dispatch.step_id, attempt: dispatch.attempt, + idempotencyKey: dispatch.idempotency_key, surfacePath: `/${call.provider}`, + revisionBefore: 'pending', revisionAfter: `${dispatch.run_id}:${dispatch.step_id}`, + }, async () => { + try { receipt = await readSlackReceipt(file); } + catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + receipt = await (call.provider === 'slack' && 'params' in call + ? slackWriteback(call, dataDir, dispatch.run_id, dispatch.step_id, signal) + : helperWriteback(call as HelperCall, dataDir, dispatch.run_id, dispatch.step_id, signal)); + await atomicJson(file, receipt); + } + signal.throwIfAborted(); + }); + // Also required after a confirmed election followed by a crash before step.complete. + if (receipt === undefined) receipt = await readSlackReceipt(file); + return { ...call, idempotencyKey: `${dispatch.run_id}:${dispatch.step_id}`, receipt }; + }); + } catch (error) { + if (!(error instanceof HelperDeliveryError)) throw error; + await client.stepComplete(dispatch.run_id, dispatch.step_id, dispatch.attempt, + dispatch.idempotency_key, 'worker_error', { + output: { ...call, diagnostic: error.message }, + started_pins: dispatch.pins, end_pins: dispatch.pins, effects: [], + }); + return; + } + await client.stepComplete(dispatch.run_id, dispatch.step_id, dispatch.attempt, + dispatch.idempotency_key, 'success', { output, started_pins: dispatch.pins, end_pins: dispatch.pins, + effects: [{ surface_path: `/${call.provider}`, idempotency_key: dispatch.idempotency_key }] }); +} diff --git a/packages/sdk/src/authored-slack-effect.ts b/packages/sdk/src/authored-slack-effect.ts deleted file mode 100644 index 0c4fd0e76..000000000 --- a/packages/sdk/src/authored-slack-effect.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { existsSync } from 'node:fs'; -import { join } from 'node:path'; -import { randomUUID } from 'node:crypto'; -import { JournalClient } from './journal-client.js'; -import { AuthoredFlowExecutionError } from './authored-flow-error.js'; -import { readCompletedStepOutput } from './authored-step-output.js'; -import type { AuthoredFlowJournalStep } from './authored-flow-executor.js'; -import { compileSpec, toKernelSpec } from './compile.js'; -import { SPEC_SCHEMA_VERSION, type KernelAgentStep } from './spec.js'; -import type { StepDispatchEvent } from './protocol.js'; -import { completeHelperDispatch } from './yaml-helper-effect.js'; -import { checkSlackHelpers } from './slack-preflight.js'; -import { atomicJson, type SlackCall } from './slack-writeback.js'; - -export function assertSlackCredentials(): void { - const report = checkSlackHelpers({ header: { tools: { slack: true } }, body() {} }); - if (!report.ok) { - const diagnostic = report.diagnostics[0]!; - throw new AuthoredFlowExecutionError( - diagnostic.kind === 'helper_slack.mount_required' ? diagnostic.kind : 'helper_slack.credential_missing', diagnostic.message); - } -} - -/** Each helper uses the existing agent lease + effect protocol; no new kernel verb. */ -export async function runSlackEffect( - journal: JournalClient, name: string, stepId: string, call: SlackCall, - dataDir: string, journalSteps: AuthoredFlowJournalStep[], -): Promise { - assertSlackCredentials(); - const stream = `slack-helper-${randomUUID()}`; - const spec = toKernelSpec(compileSpec({ - version: SPEC_SCHEMA_VERSION, name: `${name}/${stepId}`, - steps: [{ id: stepId, type: 'agent', instruction: JSON.stringify(call), - maxIterations: 3, recoveryMode: 'reset', - surfaces: { streams: [{ stream }], external: ['/slack'] } }], - })); - const outcome = await journal.runStart(spec); - await atomicJson(join(dataDir, 'helper-runs', `${outcome.run_id}.json`), { provider: 'slack' }); - await driveSlackEffect(journal, outcome.run_id, spec.steps[0] as KernelAgentStep, call, dataDir); - const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps); - return (output as { receipt: unknown }).receipt; -} - -/** Recognize only the journaled helper envelope, so ordinary agent runs keep their worker path. */ -export async function resumeSlackEffect(journal: JournalClient, runId: string, dataDir: string): Promise { - if (!/^[A-Za-z0-9_-]+$/.test(runId) || !existsSync(join(dataDir, 'helper-runs', `${runId}.json`))) return false; - const entries = (await journal.journalRead(runId, 1)).entries as Array<{ - entry_type: string; payload: { spec?: { steps?: KernelAgentStep[] } }; - }>; - const steps = entries.find(entry => entry.entry_type === 'run.spawned')?.payload.spec?.steps; - if (steps?.length !== 1) return false; - const step = steps[0]!; - if (step.type !== 'agent' || !step.surfaces?.streams?.[0]?.stream.startsWith('slack-helper-')) return false; - const call = JSON.parse(step.instruction) as SlackCall; - if (call.type !== 'effect' || call.provider !== 'slack' || !['post', 'dm', 'reply', 'react'].includes(call.verb)) return false; - const snapshot = await journal.runGet(runId); - if (snapshot.status === 'completed' || snapshot.status === 'failed') return true; - assertSlackCredentials(); - await driveSlackEffect(journal, runId, step, call, dataDir); - return true; -} - -async function driveSlackEffect( - journal: JournalClient, runId: string, step: KernelAgentStep, call: SlackCall, dataDir: string, -): Promise { - const client = new JournalClient(journal.socketPath); - const stream = step.surfaces!.streams![0]!.stream; - const pins = { workspace: [], streams: [{ stream, read_offset: 0 }] }; - let resolve!: () => void; - let reject!: (error: unknown) => void; - const completed = new Promise((yes, no) => { resolve = yes; reject = no; }); - // Observe early dispatch failures even while attach/run.resume is still pending. - void completed.catch(() => undefined); - let executing = false; - client.on('step.dispatch', (dispatch: StepDispatchEvent) => { - if (executing || dispatch.run_id !== runId || dispatch.step_id !== step.id) return; - executing = true; - void completeHelperDispatch(client, dispatch, call, dataDir).then(resolve, reject); - }); - client.on('error', reject); - try { - await client.connect(); - await client.hello('flows-slack-helper'); - await client.workerAttach(`slack-${randomUUID()}`, ['agent'], pins, 1); - const outcome = await journal.runResume(runId); - if (outcome.status === 'completed') return; - if (outcome.status === 'failed') { - throw new AuthoredFlowExecutionError('step_failed', `Slack helper run ${runId} ${outcome.status}`, undefined, runId); - } - await completed; - } catch (error) { - if (error instanceof AuthoredFlowExecutionError) throw error; - throw new AuthoredFlowExecutionError('step_failed', error instanceof Error ? error.message : 'Slack effect failed', 'worker_error', runId); - } finally { client.close(); } -} diff --git a/packages/sdk/src/cli/run.ts b/packages/sdk/src/cli/run.ts index c68bbd227..b842da90b 100644 --- a/packages/sdk/src/cli/run.ts +++ b/packages/sdk/src/cli/run.ts @@ -1,7 +1,7 @@ import { parseDigestReference } from '../bundle-transport.js'; import { prepareDigestRun } from './run-digest.js'; import { reuseSummary } from './reuse.js'; -import { resumeSlackEffect } from '../authored-slack-effect.js'; +import { resumeHelperEffect } from '../authored-helper-effect.js'; import { AuthoredFlowExecutionError } from '../authored-flow-error.js'; import { join, resolve } from 'node:path'; import type { ProgressEvent } from '../progress.js'; @@ -151,6 +151,7 @@ export async function resumeFlow( if (connected !== undefined) return connected; try { + await resumeHelperEffect(client, runId, dataDir); let outcome = await client.runResume(runId, options.allowHumanInfluenced); if (await resumeSlackEffect(client, runId, dataDir)) { outcome = await client.runResume(runId, options.allowHumanInfluenced); @@ -162,7 +163,8 @@ export async function resumeFlow( diagnostics: [{ severity: 'refusal', kind: 'human_influenced_run', message: error.message.replace(/^human_influenced_run: /, '') }] } }; } if (error instanceof AuthoredFlowExecutionError - && (error.code === 'helper_slack.credential_missing' || error.code === 'helper_slack.mount_required')) { + && (error.code === 'helper_slack.credential_missing' || error.code === 'helper_slack.mount_required' + || error.code === 'helper_provider.mount_required' || error.code === 'helper_provider.unsupported')) { return { exitCode: 2, report: { ...base, runId, socketPath, diagnostics: [{ severity: 'refusal', kind: error.code, message: error.message }] } }; } diff --git a/packages/sdk/src/failure-kinds.ts b/packages/sdk/src/failure-kinds.ts index 7b2917d9c..fd46d5f1c 100644 --- a/packages/sdk/src/failure-kinds.ts +++ b/packages/sdk/src/failure-kinds.ts @@ -6,6 +6,8 @@ const SHARED_SPEC_FAILURE_KINDS = ['invalid_spec'] as const; const PREFLIGHT_ENVIRONMENT_FAILURE_KINDS = [ ...NAMED_GATE_FAILURE_KINDS, ...PLUGIN_FAILURE_KINDS, + 'helper_provider.mount_required', + 'helper_provider.unsupported', 'helper_slack.credential_missing', 'helper_slack.mount_required', 'helper_mount_required', diff --git a/packages/sdk/src/helper-preflight.ts b/packages/sdk/src/helper-preflight.ts new file mode 100644 index 000000000..3f22d264a --- /dev/null +++ b/packages/sdk/src/helper-preflight.ts @@ -0,0 +1,34 @@ +import { helperProviders } from '@relayflows/surface/runtime'; +import type { PreflightResult, PreflightDiagnostic } from './preflight.js'; + +/** Static discovery never executes the body; dynamic aliases are checked at call time. */ +export function preflightHelpers( + definition: { header?: { tools?: Readonly> }; body?: Function }, + facts: { slackToken?: string; slackMount?: boolean; slackMock?: boolean; + providers?: Readonly> }, +): PreflightResult { + const body = typeof definition.body === 'function' ? Function.prototype.toString.call(definition.body) : ''; + const parameter = body.match(/^(?:async\s+)?(?:function(?:\s+[\w$]+)?\s*)?(?:\(\s*([\w$]+)|([\w$]+)\s*=>)/); + const root = (parameter?.[1] ?? parameter?.[2])?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const diagnostics: PreflightDiagnostic[] = []; + for (const { provider, namespace, supported } of helperProviders) { + const used = definition.header?.tools?.[namespace] === true + || (root !== undefined && new RegExp(`(?:^|[^\\w$.])${root}\\s*(?:\\.\\s*${namespace}\\b|\\[\\s*['"]${namespace}['"]\\s*\\])`).test(body)); + if (!used) continue; + const fact = facts.providers?.[provider] ?? (provider === 'slack' + ? { mount: facts.slackMount, mock: facts.slackMock, token: facts.slackToken } + : { mount: false, mock: false, token: undefined }); + if (!supported) { + diagnostics.push({ severity: 'refusal', kind: 'helper_provider.unsupported', + message: `f.${namespace} has no upstream relayfile writeback client.` }); + } else if (!fact.mock && !fact.mount) { + diagnostics.push({ severity: 'refusal', + kind: provider === 'slack' ? (fact.token?.trim() ? 'helper_slack.mount_required' : 'helper_slack.credential_missing') : 'helper_provider.mount_required', + message: `f.${namespace} requires a relayfile ${provider} mount; direct-token transport is not implemented.` }); + } else if (provider === 'notion' && !fact.mock && /\.\s*appendBlock\b/.test(body)) { + diagnostics.push({ severity: 'refusal', kind: 'helper_provider.unsupported', + message: 'f.notion.appendBlock is mock-only: the Notion adapter has no append-block writeback route.' }); + } + } + return { ok: diagnostics.length === 0, gates: [], resolutions: [], diagnostics }; +} diff --git a/packages/sdk/src/helper-storage.ts b/packages/sdk/src/helper-storage.ts new file mode 100644 index 000000000..99c394c2d --- /dev/null +++ b/packages/sdk/src/helper-storage.ts @@ -0,0 +1,23 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { mkdir, open, readFile, rename } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; + +/** Durable receipt precedes effect.confirm, so a confirmed replay can recover it. */ +export async function atomicJson(path: string, value: unknown): Promise { + await mkdir(dirname(path), { recursive: true }); + const temporary = `${path}.${randomUUID()}.tmp`; + const file = await open(temporary, 'wx', 0o600); + try { await file.writeFile(JSON.stringify(value)); await file.sync(); } + finally { await file.close(); } + await rename(temporary, path); + const directory = await open(dirname(path), 'r'); + try { await directory.sync(); } finally { await directory.close(); } +} + +export function receiptPath(dataDir: string, runId: string, stepId: string): string { + return join(dataDir, 'helper-receipts', createHash('sha256').update(`${runId}:${stepId}`).digest('hex') + '.json'); +} + +export async function readHelperReceipt(path: string): Promise { + return JSON.parse(await readFile(path, 'utf8')) as unknown; +} diff --git a/packages/sdk/src/helper-writeback.ts b/packages/sdk/src/helper-writeback.ts new file mode 100644 index 000000000..d5fe108f6 --- /dev/null +++ b/packages/sdk/src/helper-writeback.ts @@ -0,0 +1,83 @@ +import { createHash } from 'node:crypto'; +import { join } from 'node:path'; +import { writeJsonFile, readJsonFile, listJsonFiles, type WritebackResult } from '@relayfile/adapter-core/vfs-client'; +import type { RelayTransport, RelayTransportRequest } from '@relayfile/relay-helpers/transport'; +import { helperClients, helperProviders, invokeHelper, type HelperCall } from '@relayflows/surface/runtime'; +import { providerMount } from './slack-preflight.js'; +import { atomicJson } from './helper-storage.js'; +import type { SlackCall } from './slack-writeback.js'; + +export class HelperDeliveryError extends Error {} + +export async function helperWriteback(call: HelperCall, dataDir: string, runId: string, stepId: string, signal: AbortSignal): Promise { + const factory = helperClients[call.provider]; + if (!factory) throw new Error(`No writeback client for ${call.provider}`); + const { transport } = helperTransport(call, dataDir, runId, stepId, signal); + try { return await invokeHelper(factory, call, transport); } + catch (cause) { throw new HelperDeliveryError(cause instanceof Error ? cause.message : String(cause), { cause }); } +} + +/** One transport for every provider, including Slack. Only confirmed writes succeed. */ +export function helperTransport(call: HelperCall | SlackCall, dataDir: string, runId: string, stepId: string, signal: AbortSignal) { + const provider = helperProviders.find(p => p.provider === call.provider); + if (!provider) throw new Error(`Unknown helper provider ${call.provider}`); + const mock = process.env[provider.mockEnv] === '1'; + const mount = providerMount(provider.provider); + const options = () => { + signal.throwIfAborted(); + if (!mount) throw new Error(`${provider.provider} requires a relayfile mount`); + return { relayfileMountRoot: mount }; + }; + const idempotencyKey = `${runId}:${stepId}`; + let deliveredRef = ''; + const transport: RelayTransport = { + async read(request: RelayTransportRequest): Promise { + signal.throwIfAborted(); + if (mock) return {} as T; + return readJsonFile(options(), call.provider, `read.${request.resource}`, request.path); + }, + async list(request: RelayTransportRequest): Promise { + signal.throwIfAborted(); + if (mock) return []; + return (await listJsonFiles(options(), call.provider, `list.${request.resource}`, request.path)).map(file => file.value); + }, + async write(request) { + signal.throwIfAborted(); + if (request.provider !== call.provider) throw new Error('Helper transport provider mismatch'); + if (!mock && call.provider === 'notion' && call.verb === 'appendBlock') { + throw new Error('Notion appendBlock has no upstream mount writeback route'); + } + if (typeof request.body !== 'object' || request.body === null || Array.isArray(request.body)) { + throw new Error('Helper writeback requires a JSON object'); + } + const body: Record = { ...request.body as Record, idempotencyKey }; + // Item paths must remain canonical; only collections receive a draft filename. + const draft = /\.[a-z]+$/i.test(request.path) ? request.path + : `${request.path}/draft-${createHash('sha256').update(idempotencyKey).digest('hex')}.json`; + let result: WritebackResult; + if (mock) { + deliveredRef = `mock-ref-${stepId}`; + result = { path: deliveredRef, absolutePath: draft, deliveryStatus: 'confirmed', receipt: { externalId: `mock-${stepId}` } }; + await atomicJson(join(dataDir, 'mock-writeback', call.provider, `${stepId}.json`), { + ...call, ...body, + ...(call.provider === 'slack' ? { channel: request.parameters.channelId, + ...(body.parentRef === undefined ? {} : { replyTo: body.parentRef }) } : {}), + runId, stepId, request: { ...request, body }, receipt: result.receipt, + }); + } else { + try { result = await writeJsonFile(options(), call.provider, `write.${request.resource}`, draft, body); } + catch (cause) { + // Upstream created() converts pending/terminal adapter errors into values. + // A journal effect must fail instead of confirming an undelivered write. + throw new Error(`${call.provider} writeback failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause }); + } + if (result.deliveryStatus !== 'confirmed' || !result.receipt) throw new Error(`${call.provider} writeback is pending; no delivery receipt`); + if (call.provider === 'slack' && call.verb !== 'react' && !result.receipt.externalId && !result.receipt.ts) throw new Error('Slack writeback has no delivered timestamp'); + deliveredRef = result.path; + } + signal.throwIfAborted(); + return result; + }, + }; + return { transport, deliveredRef: () => deliveredRef }; +} diff --git a/packages/sdk/src/preflight.ts b/packages/sdk/src/preflight.ts index 069bd2135..9561e5a37 100644 --- a/packages/sdk/src/preflight.ts +++ b/packages/sdk/src/preflight.ts @@ -607,24 +607,7 @@ function probeNamedGate(step: StepSpec, probes: PreflightProbes, diagnostics: Pr } } -/** Helper preflight never evaluates the authored body. Dynamic uses are checked at call time. */ -export function preflightHelpers( - definition: { header?: { tools?: { slack?: boolean } }; body?: Function }, - facts: { slackToken?: string; slackMount: boolean; slackMock: boolean }, -): PreflightResult { - const usesSlack = definition.header?.tools?.slack === true - || (typeof definition.body === 'function' - && /(?:\.\s*slack\b|\[\s*['"]slack['"]\s*\])/.test(Function.prototype.toString.call(definition.body))); - const diagnostics: PreflightDiagnostic[] = usesSlack && !facts.slackMock - && !facts.slackToken?.trim() && !facts.slackMount - ? [{ severity: 'refusal', kind: 'helper_slack.credential_missing', - message: 'f.slack requires SLACK_BOT_TOKEN or a relayfile Slack mount.' }] - : usesSlack && !facts.slackMock && !facts.slackMount - ? [{ severity: 'refusal', kind: 'helper_slack.mount_required', - message: 'f.slack direct bot-token transport is not implemented; configure a relayfile Slack mount.' }] - : []; - return { ok: diagnostics.length === 0, gates: [], resolutions: [], diagnostics }; -} +export { preflightHelpers } from './helper-preflight.js'; /** Authored memory probes run before invoking the body or contacting the journal. */ export async function preflightMemory( diff --git a/packages/sdk/src/slack-preflight.ts b/packages/sdk/src/slack-preflight.ts index 18a7e0cd7..051d2195d 100644 --- a/packages/sdk/src/slack-preflight.ts +++ b/packages/sdk/src/slack-preflight.ts @@ -1,19 +1,25 @@ import { statSync } from 'node:fs'; import { join } from 'node:path'; +import { helperProviders } from '@relayflows/surface/runtime'; import { preflightHelpers } from './preflight.js'; -export function slackMount(env: NodeJS.ProcessEnv = process.env): string | undefined { +export function providerMount(provider: string, env: NodeJS.ProcessEnv = process.env): string | undefined { const root = [env.RELAYFILE_MOUNT_PATH, env.WORKSPACE_ROOT, env.WORKFORCE_SANDBOX_ROOT, env.RELAYFILE_MOUNT_ROOT, env.RELAYFILE_ROOT].find(value => value?.trim()); if (!root?.trim()) return undefined; - try { return statSync(join(root, 'slack')).isDirectory() ? root : undefined; } + try { return statSync(join(root, provider)).isDirectory() ? root : undefined; } catch { return undefined; } } -export function checkSlackHelpers(definition: Parameters[0]) { +export const slackMount = (env: NodeJS.ProcessEnv = process.env) => providerMount('slack', env); + +export function checkProviderHelpers(definition: Parameters[0]) { return preflightHelpers(definition, { - slackToken: process.env.SLACK_BOT_TOKEN, - slackMount: slackMount() !== undefined, - slackMock: process.env.RELAYFLOWS_SLACK_MOCK === '1', + providers: Object.fromEntries(helperProviders.map(p => [p.provider, { + mount: providerMount(p.provider) !== undefined, + mock: process.env[p.mockEnv] === '1', + token: p.provider === 'slack' ? process.env.SLACK_BOT_TOKEN : undefined, + }])), }); } +export const checkSlackHelpers = checkProviderHelpers; diff --git a/packages/sdk/src/slack-writeback.ts b/packages/sdk/src/slack-writeback.ts index daa2d2162..58237f14f 100644 --- a/packages/sdk/src/slack-writeback.ts +++ b/packages/sdk/src/slack-writeback.ts @@ -1,12 +1,7 @@ -import { createHash, randomUUID } from 'node:crypto'; -import { mkdir, open, readFile, rename } from 'node:fs/promises'; -import { dirname, join } from 'node:path'; import { slackClient } from '@relayfile/relay-helpers'; -import { flowRunWritebackIdempotency, type SlackHelper } from '@relayflows/surface'; -import { slackPostBody } from '@relayflows/surface/runtime'; -import type { RelayTransport } from '@relayfile/relay-helpers/transport'; -import { writeJsonFile, type WritebackResult } from '@relayfile/adapter-core/vfs-client'; -import { slackMount } from './slack-preflight.js'; +import type { SlackHelper } from '@relayflows/surface'; +import { helperTransport } from './helper-writeback.js'; +export { atomicJson, receiptPath, readHelperReceipt as readSlackReceipt } from './helper-storage.js'; export type SlackCall = | { type: 'effect'; provider: 'slack'; verb: 'post'; params: { channel: string; text: Parameters[1]; opts?: Parameters[2] } } @@ -14,72 +9,16 @@ export type SlackCall = | { type: 'effect'; provider: 'slack'; verb: 'reply'; params: { channel: string; threadTs: string; text: string } } | { type: 'effect'; provider: 'slack'; verb: 'react'; params: { channel: string; messageTs: string; emoji: string } }; -/** Durable receipt precedes effect.confirm, so a confirmed replay can recover it. */ -export async function atomicJson(path: string, value: unknown): Promise { - await mkdir(dirname(path), { recursive: true }); - const temporary = `${path}.${randomUUID()}.tmp`; - const file = await open(temporary, 'wx', 0o600); - try { await file.writeFile(JSON.stringify(value)); await file.sync(); } - finally { await file.close(); } - await rename(temporary, path); - const directory = await open(dirname(path), 'r'); - try { await directory.sync(); } finally { await directory.close(); } -} - -export function receiptPath(dataDir: string, runId: string, stepId: string): string { - return join(dataDir, 'helper-receipts', createHash('sha256').update(`${runId}:${stepId}`).digest('hex') + '.json'); -} - export async function slackWriteback( call: SlackCall, dataDir: string, runId: string, stepId: string, signal: AbortSignal, ): Promise { - const idempotencyKey = flowRunWritebackIdempotency(runId, stepId); - let deliveredRef = ''; - const transport: RelayTransport = { - async read() { throw new Error('Slack effect transport is write-only'); }, - async list() { throw new Error('Slack effect transport is write-only'); }, - async write(request) { - signal.throwIfAborted(); - // The pinned adapter's ergonomic post accepts text only. Preserve structured - // content at its transport boundary, retaining its paths and receipt handling. - const content = call.verb === 'post' - ? slackPostBody(call.params.text, call.params.opts) - : request.body as Record; - const body: Record = { ...content, idempotencyKey }; - const stamped = { ...request, body }; - const draft = `${request.path}/draft-${createHash('sha256').update(idempotencyKey).digest('hex')}.json`; - if (process.env.RELAYFLOWS_SLACK_MOCK === '1') { - const ts = `mock-${stepId}`; - deliveredRef = `mock-ref-${stepId}`; - const result: WritebackResult = { path: deliveredRef, absolutePath: draft, deliveryStatus: 'confirmed', receipt: { externalId: ts } }; - await atomicJson(join(dataDir, 'mock-writeback', 'slack', `${stepId}.json`), { - ...call, ...body, channel: request.parameters.channelId, - ...(body.parentRef === undefined ? {} : { replyTo: body.parentRef }), - runId, stepId, request: stamped, receipt: result.receipt, - }); - return result; - } - const mount = slackMount(); - if (mount !== undefined) { - const result = await writeJsonFile({ relayfileMountRoot: mount }, 'slack', `write.${request.resource}`, draft, body); - if (result.deliveryStatus !== 'confirmed' || !result.receipt) throw new Error('Slack writeback is pending; no delivery receipt'); - if (call.verb !== 'react' && !result.receipt.externalId && !result.receipt.ts) throw new Error('Slack writeback has no delivered timestamp'); - deliveredRef = result.path; - return result; - } - throw new Error('Slack effect requires a relayfile mount; direct bot-token transport is not implemented'); - }, - }; + const { transport, deliveredRef } = helperTransport(call, dataDir, runId, stepId, signal); const client = slackClient({ transport }); switch (call.verb) { case 'post': return client.post(call.params.channel, typeof call.params.text === 'string' ? call.params.text : call.params.text.text ?? '', call.params.opts); case 'dm': return client.dm(call.params.user, call.params.text); - case 'reply': return { ...await client.reply(call.params.channel, call.params.threadTs, call.params.text), ref: deliveredRef }; + case 'reply': return { ...await client.reply(call.params.channel, call.params.threadTs, call.params.text), ref: deliveredRef() }; case 'react': await client.react(call.params.channel, call.params.messageTs, call.params.emoji); return null; } } - -export async function readSlackReceipt(path: string): Promise { - return JSON.parse(await readFile(path, 'utf8')) as unknown; -} diff --git a/packages/sdk/tests/authored-helpers.test.ts b/packages/sdk/tests/authored-helpers.test.ts new file mode 100644 index 000000000..7d23e2b28 --- /dev/null +++ b/packages/sdk/tests/authored-helpers.test.ts @@ -0,0 +1,190 @@ +import { spawn, spawnSync, type ChildProcess } from 'node:child_process'; +import { once } from 'node:events'; +import { existsSync, mkdirSync, symlinkSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; +import { homedir, tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { flow } from '@relayflows/surface'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { executeAuthoredFlow } from '../src/authored-flow-executor.js'; +import { JournalClient } from '../src/journal-client.js'; +import { socketPathFor } from '../src/daemon-connection.js'; +import { resumeFlow } from '../src/cli/run.js'; +import { runCli } from '../src/cli.js'; +import { helperProviders, type HelperCall } from '@relayflows/surface/runtime'; +import { WRITEBACK_PATH_CATALOG } from '@relayfile/adapter-core/writeback-paths'; +import { runHelperEffect, resumeHelperEffect } from '../src/authored-helper-effect.js'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); +// Match ops/cargo.sh's per-worktree build output for a plain `npm test` too. +const worktreeKey = spawnSync('cksum', { input: realpathSync(root), encoding: 'utf8' }).stdout.trim().split(' ')[0]!; +const target = process.env.CARGO_TARGET_DIR ?? join(process.env.RELAYFLOWS_TOOLCHAIN_HOME ?? join(homedir(), '.relayflows-toolchain'), 'target', worktreeKey); +const binary = process.env.RELAYFLOWD_BIN ?? join(target, 'debug/relayflowd'); +const directories: string[] = []; +const children: ChildProcess[] = []; +const clients: JournalClient[] = []; +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +afterEach(async () => { + for (const client of clients.splice(0)) client.close(); + for (const child of children.splice(0)) await kill(child); + for (const dir of directories.splice(0)) rmSync(dir, { recursive: true, force: true }); + vi.unstubAllEnvs(); + vi.restoreAllMocks(); +}); + +function temporary(): string { + const dir = mkdtempSync(join(tmpdir(), 'f-helpers-')); + directories.push(dir); + return dir; +} + +async function kill(child: ChildProcess): Promise { + if (child.exitCode !== null || child.signalCode !== null) return; + const exited = once(child, 'exit'); + child.kill('SIGKILL'); + await exited; +} + +async function start(dataDir: string): Promise<{ daemon: ChildProcess; client: JournalClient }> { + expect(existsSync(binary), `Build relayflowd first: ${binary}`).toBe(true); + const daemon = spawn(binary, ['--data-dir', dataDir, 'serve'], { stdio: ['ignore', 'pipe', 'pipe'] }); + children.push(daemon); + let stderr = ''; + daemon.stderr!.on('data', chunk => { stderr += String(chunk); }); + for (let attempt = 0; attempt < 100; attempt++) { + const client = new JournalClient(socketPathFor(dataDir), { connectTimeoutMs: 100 }); + try { + await client.connect(); await client.hello('slack-test'); clients.push(client); + return { daemon, client }; + } catch { client.close(); } + if (daemon.exitCode !== null) throw new Error(stderr); + await delay(20); + } + throw new Error(`Daemon did not start: ${stderr}`); +} + +it('lowers the named acceptance helpers and Slack to confirmed journal effects', async () => { + for (const p of helperProviders) vi.stubEnv(p.mockEnv, '1'); + const dataDir = temporary(); + const { client } = await start(dataDir); + const result = await executeAuthoredFlow(flow('helper-smoke', async f => { + const github = await f.github.createIssue({ repo: 'owner/repo', title: 'Smoke', body: 'body' }); + expect(github).toMatchObject({ status: 'confirmed' }); + await f.linear.createIssue({ teamId: 'team', title: 'Smoke' }); + await f.notion.appendBlock({ pageId: 'page', block: { type: 'paragraph' } }); + await f.stripe.createInvoice({ customer: 'cus_fixture' }); + await f.slack.post('#test', 'hello'); + f.done('success'); + }), client, undefined, { dataDir }); + expect(result.completionReason).toBe('success'); + expect(result.journalSteps).toHaveLength(6); + for (const step of result.journalSteps.slice(0, -1)) { + const entries = (await client.journalRead(step.runId, 1)).entries as any[]; + expect(entries.filter(e => e.entry_type === 'effect.confirmed')).toHaveLength(1); + expect(entries.find(e => e.entry_type === 'step.completed').payload).toMatchObject({ completionReason: 'success', output: { type: 'effect' } }); + } +}, 30_000); + +it('runs every available provider through the real kernel and resumes completed effects without a second write', async () => { + const dataDir = temporary(); + const { client } = await start(dataDir); + const catalog = WRITEBACK_PATH_CATALOG as Record>; + for (const p of helperProviders.filter(p => p.supported && p.provider !== 'slack')) { + vi.stubEnv(p.mockEnv, '1'); + const [resource, variants] = Object.entries(catalog[p.provider] ?? {})[0] ?? []; + const params = Object.fromEntries((variants?.[0]?.params ?? []).map(param => [param, 'fixture'])); + const call: HelperCall = p.provider === 'stripe' + ? { type: 'effect', provider: p.provider, verb: 'createInvoice', args: [{ customer: 'cus_fixture' }] } + : { type: 'effect', provider: p.provider, verb: `${resource}.write`, args: [params, { text: 'smoke' }] }; + const steps: import('../src/authored-flow-executor.js').AuthoredFlowJournalStep[] = []; + await runHelperEffect(client, 'fanout', p.provider, call, dataDir, steps); + expect(steps).toHaveLength(1); + const runId = steps[0]!.runId; + expect(await resumeHelperEffect(client, runId, dataDir)).toBe(true); + const entries = (await client.journalRead(runId, 1)).entries as any[]; + expect(entries.filter(e => e.entry_type === 'effect.confirmed'), p.provider).toHaveLength(1); + expect(entries.filter(e => e.entry_type === 'step.completed'), p.provider).toHaveLength(1); + expect(readdirSync(join(dataDir, 'mock-writeback', p.provider))).toEqual([`${p.provider}.json`]); + } +}, 60_000); + +it('rejects malformed arguments before a journal write, without evaluating getters', async () => { + vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', '1'); + let evaluated = false; + const args = { repo: 'a/b', get title() { evaluated = true; return 'bad'; }, body: '' }; + const client = new JournalClient('/must-not-connect'); + await expect(executeAuthoredFlow(flow('bad-args', async f => { + await f.github.createIssue(args); f.done('success'); + }), client)).rejects.toThrow('accessors'); + expect(evaluated).toBe(false); +}); + +function mockFiles(dataDir: string): string[] { + return readdirSync(join(dataDir, "mock-writeback/github")).filter(file => file.endsWith(".json")); +} + + it.each(['confirm', 'complete'] as const)('replays after SIGKILL before %s with the same token and one successful completion', async boundary => { + vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', '1'); + const dataDir = temporary(); + const first = await start(dataDir); + const script = join(dataDir, 'crash.mjs'); + writeFileSync(script, ` + import { flow } from ${JSON.stringify(join(root, 'packages/sdk/node_modules/@relayflows/surface/dist/index.js'))}; + import { JournalClient } from ${JSON.stringify(join(root, 'packages/sdk/dist/journal-client.js'))}; + import { executeAuthoredFlow } from ${JSON.stringify(join(root, 'packages/sdk/dist/authored-flow-executor.js'))}; + JournalClient.prototype.${boundary === 'confirm' ? 'effectConfirm' : 'stepComplete'} = async function() { process.stdout.write('CRASH_BOUNDARY\\n'); await new Promise(() => {}); }; + const client = new JournalClient(${JSON.stringify(socketPathFor(dataDir))}); + await client.connect(); await client.hello('crash-child'); + await executeAuthoredFlow(flow('crash-github', async f => { await f.github.createIssue({ repo: 'owner/repo', title: 'hi', body: '' }); f.done('success'); }), client, undefined, { dataDir: ${JSON.stringify(dataDir)} }); + `); + const child = spawn(process.execPath, [script], { stdio: ['ignore', 'pipe', 'pipe'] }); + children.push(child); + await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('Did not reach crash window')), 10_000); + child.stdout!.on('data', chunk => { if (String(chunk).includes('CRASH_BOUNDARY')) { clearTimeout(timer); resolve(); } }); + child.stderr!.on('data', chunk => { clearTimeout(timer); reject(new Error(String(chunk))); }); + }); + const file = join(dataDir, 'mock-writeback/github', mockFiles(dataDir)[0]!); + const before = JSON.parse(readFileSync(file, 'utf8')); + await kill(child); first.client.close(); await kill(first.daemon); + const second = await start(dataDir); + // Unconfirmed: lose the cache too to prove the provider token is stable. + // Confirmed: the persisted receipt must suffice without another provider write. + if (boundary === 'confirm') { + for (const cache of readdirSync(join(dataDir, 'helper-receipts'))) rmSync(join(dataDir, 'helper-receipts', cache)); + } + for (const key of ['GITHUB_BOT_TOKEN', 'RELAYFLOWS_GITHUB_MOCK', 'RELAYFILE_MOUNT_PATH', 'WORKSPACE_ROOT', 'WORKFORCE_SANDBOX_ROOT', 'RELAYFILE_MOUNT_ROOT', 'RELAYFILE_ROOT']) vi.stubEnv(key, ''); + const refused = await resumeFlow(before.runId, dataDir, { daemon: { spawn: false } }); + expect(refused.exitCode).toBe(2); + expect(refused.report.diagnostics).toContainEqual(expect.objectContaining({ kind: 'helper_provider.mount_required' })); + vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', '1'); + const resumed = await resumeFlow(before.runId, dataDir, { daemon: { spawn: false } }); + expect(resumed.exitCode, JSON.stringify(resumed.report)).toBe(0); + const after = JSON.parse(readFileSync(file, 'utf8')); + expect(after.idempotencyKey).toBe(before.idempotencyKey); + expect(after.receipt).toEqual(before.receipt); + const entries = (await second.client.journalRead(before.runId, 1)).entries as any[]; + // The kernel records abandoned attempts as step.completed(crashed); one successful + // completion is the exactly-once result, while retaining honest crash history. + expect(entries.filter(entry => entry.entry_type === 'step.completed' && entry.payload.completionReason === 'success')).toHaveLength(1); + expect(entries.filter(entry => entry.entry_type === 'step.completed' && entry.payload.completionReason === 'crashed')).toHaveLength(1); + expect(entries.filter(entry => entry.entry_type === 'effect.recorded').length).toBeGreaterThan(1); + const again = await resumeFlow(before.runId, dataDir, { daemon: { spawn: false } }); + expect(again.exitCode).toBe(0); + expect(mockFiles(dataDir)).toHaveLength(1); + }, 30_000); + + +it('journals provider failure with worker_error and never confirms the effect', async () => { + vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', '1'); + const dataDir = temporary(); const { client } = await start(dataDir); + const steps: import('../src/authored-flow-executor.js').AuthoredFlowJournalStep[] = []; + await expect(runHelperEffect(client, 'bad-helper', 'invalid', { + type: 'effect', provider: 'github', verb: 'createIssue', args: [{ repo: 'missing-owner', title: 'hi', body: '' }], + }, dataDir, steps)).rejects.toMatchObject({ code: 'step_failed', completionReason: 'worker_error' }); + expect(steps).toHaveLength(1); + const entries = (await client.journalRead(steps[0]!.runId, 1)).entries as any[]; + expect(entries.filter(e => e.entry_type === 'effect.confirmed')).toHaveLength(0); + expect(entries.find(e => e.entry_type === 'step.completed').payload.completionReason).toBe('worker_error'); +}); diff --git a/packages/sdk/tests/helpers-fanout.test.ts b/packages/sdk/tests/helpers-fanout.test.ts new file mode 100644 index 000000000..013787f6b --- /dev/null +++ b/packages/sdk/tests/helpers-fanout.test.ts @@ -0,0 +1,112 @@ +import { mkdtempSync, mkdirSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, expect, it, vi } from 'vitest'; +import { flow } from '@relayflows/surface'; +import { createHelpers, helperProviders, type HelperCall } from '@relayflows/surface/runtime'; +import { WRITEBACK_PATH_CATALOG } from '@relayfile/adapter-core/writeback-paths'; +import * as vfs from '@relayfile/adapter-core/vfs-client'; +import { helperWriteback } from '../src/helper-writeback.js'; +import { checkProviderHelpers, providerMount } from '../src/slack-preflight.js'; +import { executeAuthoredFlow } from '../src/authored-flow-executor.js'; +import { JournalClient } from '../src/journal-client.js'; + +vi.mock('@relayfile/adapter-core/vfs-client', async () => { + const actual = await vi.importActual('@relayfile/adapter-core/vfs-client'); + return { ...actual, writeJsonFile: vi.fn() }; +}); + +const dirs: string[] = []; +function temporary() { const dir = mkdtempSync(join(tmpdir(), 'helper-fanout-')); dirs.push(dir); return dir; } +afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs(); for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); }); +const signal = () => new AbortController().signal; +const github: HelperCall = { type: 'effect', provider: 'github', verb: 'createIssue', args: [{ repo: 'owner/repo', title: 'hi', body: 'body' }] }; + +for (const provider of helperProviders) { + it(`${provider.provider}: checks mount and its uniform mock variable`, () => { + const dir = temporary(); + for (const key of ['RELAYFILE_MOUNT_PATH', 'WORKSPACE_ROOT', 'WORKFORCE_SANDBOX_ROOT', 'RELAYFILE_MOUNT_ROOT', 'RELAYFILE_ROOT']) vi.stubEnv(key, ''); + vi.stubEnv(provider.mockEnv, ''); + const definition = { header: { tools: { [provider.namespace]: true } } }; + expect(checkProviderHelpers(definition).ok).toBe(false); + vi.stubEnv(provider.mockEnv, '1'); + expect(checkProviderHelpers(definition).ok).toBe(provider.supported); + vi.stubEnv(provider.mockEnv, ''); + vi.stubEnv('WORKSPACE_ROOT', dir); + mkdirSync(join(dir, provider.provider)); + expect(providerMount(provider.provider)).toBe(dir); + expect(checkProviderHelpers(definition).ok).toBe(provider.supported); + }); + if (!provider.supported || provider.provider === 'slack') continue; + it(`${provider.provider}: consumes its upstream client through the mock transport`, async () => { + vi.stubEnv(provider.mockEnv, '1'); + const dir = temporary(); + const catalog = WRITEBACK_PATH_CATALOG as Record>; + const [resource, variants] = Object.entries(catalog[provider.provider] ?? {})[0] ?? []; + const params = Object.fromEntries((variants?.[0]?.params ?? []).map(param => [param, 'fixture'])); + const call: HelperCall = provider.provider === 'stripe' + ? { type: 'effect', provider: 'stripe', verb: 'createInvoice', args: [{ customer: 'cus_fixture' }] } + : { type: 'effect', provider: provider.provider, verb: `${resource}.write`, args: [params, { text: 'smoke' }] }; + await helperWriteback(call, dir, 'run', 'step', signal()); + const record = JSON.parse(readFileSync(join(dir, 'mock-writeback', provider.provider, 'step.json'), 'utf8')); + expect(record.request.provider).toBe(provider.provider); + expect(record.request.body.idempotencyKey).toBe('run:step'); + expect(record.receipt.externalId).toBe('mock-step'); + }); +} + +it('binds named verbs lazily with intact argument snapshots and synchronous paths', () => { + const calls: HelperCall[] = []; + const helpers = createHelpers(call => { calls.push(call); return {} as never; }); + const args = { repo: 'owner/repo', title: 'title', body: 'body' }; + helpers.github.createIssue(args); + expect(calls).toEqual([{ type: 'effect', provider: 'github', verb: 'createIssue', args: [args] }]); + expect(helpers.github.issues.path({ owner: 'owner', repo: 'repo' })).toBe('/github/repos/owner/repo/issues'); +}); + +it('refuses body and bracket uses before any body or journal work', async () => { + vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', ''); + for (const key of ['RELAYFILE_MOUNT_PATH', 'WORKSPACE_ROOT', 'WORKFORCE_SANDBOX_ROOT', 'RELAYFILE_MOUNT_ROOT', 'RELAYFILE_ROOT']) vi.stubEnv(key, ''); + let entered = false; + const handle = flow('missing-github', async f => { entered = true; await f['github'].createIssue({ repo: 'a/b', title: 'x', body: '' }); f.done('success'); }); + await expect(executeAuthoredFlow(handle, new JournalClient('/must-not-connect'))).rejects.toMatchObject({ code: 'helper_provider.mount_required' }); + expect(entered).toBe(false); +}); + +it('preserves collection drafts, item paths, and confirmed receipts in mount mode', async () => { + const dir = temporary(); mkdirSync(join(dir, 'github')); vi.stubEnv('RELAYFILE_MOUNT_PATH', dir); vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', ''); + const write = vi.mocked(vfs.writeJsonFile).mockResolvedValue({ path: 'delivered', absolutePath: 'delivered', deliveryStatus: 'confirmed', receipt: { externalId: '123' } }); + await expect(helperWriteback(github, dir, 'run', 'issue', signal())).resolves.toMatchObject({ status: 'confirmed', id: '123' }); + expect(write.mock.calls[0]?.[3]).toMatch(/^\/github\/repos\/owner\/repo\/issues\/draft-[a-f0-9]+\.json$/); + expect(write.mock.calls[0]?.[4]).toEqual({ title: 'hi', body: 'body', idempotencyKey: 'run:issue' }); + await helperWriteback({ ...github, verb: 'updateRef', args: [{ owner: 'owner', repo: 'repo', ref: 'branch', sha: '123' }] }, dir, 'run', 'ref', signal()); + expect(write.mock.calls[1]?.[3]).toMatch(/refs\/refs%2Fheads%2Fbranch\.json$/); +}); + +it('never confirms pending delivery or permits upstream created() to swallow a pending error', async () => { + const dir = temporary(); mkdirSync(join(dir, 'github')); vi.stubEnv('RELAYFILE_MOUNT_PATH', dir); vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', ''); + const write = vi.mocked(vfs.writeJsonFile).mockResolvedValue({ path: 'pending', absolutePath: 'pending', deliveryStatus: 'pending' }); + await expect(helperWriteback(github, dir, 'run', 'step', signal())).rejects.toThrow('pending'); + write.mockRejectedValue(new vfs.RelayfileWritebackPendingError({ provider: 'github', operation: 'write.issues', path: 'pending', opId: 'op', status: 'pending', timeoutMs: 1 })); + await expect(helperWriteback(github, dir, 'run', 'step', signal())).rejects.toThrow(); +}); + +it('blocks Notion append in mount mode and tests its lowering in mock mode', async () => { + const dir = temporary(); mkdirSync(join(dir, 'notion')); vi.stubEnv('RELAYFILE_MOUNT_PATH', dir); vi.stubEnv('RELAYFLOWS_NOTION_MOCK', ''); + const call: HelperCall = { type: 'effect', provider: 'notion', verb: 'appendBlock', args: [{ pageId: 'page', block: { type: 'paragraph' } }] }; + await expect(helperWriteback(call, dir, 'run', 'step', signal())).rejects.toThrow('no upstream'); + vi.stubEnv('RELAYFLOWS_NOTION_MOCK', '1'); + await expect(helperWriteback(call, dir, 'run', 'step', signal())).resolves.toMatchObject({ status: 'confirmed' }); +}); + +it('rejects cancellation and prototype method invocation', async () => { + const dir = temporary(); vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', '1'); + const controller = new AbortController(); controller.abort(new Error('cancelled')); + await expect(helperWriteback(github, dir, 'run', 'step', controller.signal)).rejects.toThrow('cancelled'); + await expect(helperWriteback({ ...github, verb: 'constructor' }, dir, 'run', 'step', signal())).rejects.toThrow('Unknown helper verb'); +}); + +it('does not mistake memory.recall or unrelated object properties for helper namespaces', () => { + const body = async (f: import('@relayflows/surface').Ctx) => { await f.memory.recall('query'); const item = { x: 1 }; return item.x; }; + expect(checkProviderHelpers({ body }).ok).toBe(true); +}); diff --git a/packages/sdk/tests/preflight.test.ts b/packages/sdk/tests/preflight.test.ts index 501a0d9c3..a263b1487 100644 --- a/packages/sdk/tests/preflight.test.ts +++ b/packages/sdk/tests/preflight.test.ts @@ -367,6 +367,8 @@ describe('preflight: CLI resolution and refusal predicates', () => { // not read as coming from this test alone. it('reaches every declared refusal kind, with the converse held by the type', async () => { const scenarios = [ + preflightHelpers({ header: { tools: { github: true } } }, {}), + preflightHelpers({ header: { tools: { airtable: true } } }, {}), preflightHelpers({ header: { tools: { slack: true } }, body() {} }, { slackMount: false, slackMock: false }), preflightHelpers({ header: { tools: { slack: true } }, body() {} }, { slackToken: 'present', slackMount: false, slackMock: false }), await preflight(flow({ id: 'a', type: 'deterministic', command: 'x' }), { diff --git a/packages/sdk/tsconfig.tests.json b/packages/sdk/tsconfig.tests.json index b20bf50f0..5ef64519e 100644 --- a/packages/sdk/tsconfig.tests.json +++ b/packages/sdk/tsconfig.tests.json @@ -15,6 +15,8 @@ "tests/deploy.test.ts", "tests/run-from-digest.test.ts", "tests/run-digest-live.test.ts", + "tests/helpers-fanout.test.ts", + "tests/authored-helpers.test.ts", "tests/fixtures/build.flow.ts", "tests/typed-output.test.ts", "tests/mcp.test.ts", diff --git a/packages/surface/package-lock.json b/packages/surface/package-lock.json index 448dab2b8..e1867bee8 100644 --- a/packages/surface/package-lock.json +++ b/packages/surface/package-lock.json @@ -18,6 +18,9 @@ }, "engines": { "node": ">=20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@relayfile/relay-helpers": "0.4.11" } }, "node_modules/@esbuild/aix-ppc64": { @@ -423,6 +426,19 @@ "hono": "^4" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", @@ -490,6 +506,159 @@ "node": "^22.20 || ^24.12 || >=25" } }, + "node_modules/@relayfile/adapter-core": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@relayfile/adapter-core/-/adapter-core-0.5.24.tgz", + "integrity": "sha512-bOQRuBoAw2RlYs30RtKsOvXlXzcRx4owhHdj384hPrznBIY3U4ZYcb4pVfzreW9TStHEiX7EguUyqhD9g8DJmA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@scalar/postman-to-openapi": "^0.6.0", + "cheerio": "^1.2.0", + "minimatch": "^10.0.3", + "yaml": "^2.8.1" + }, + "bin": { + "adapter-core": "dist/src/cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@relayfile/sdk": ">=0.6.0 <1" + } + }, + "node_modules/@relayfile/adapter-linear": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/@relayfile/adapter-linear/-/adapter-linear-0.4.12.tgz", + "integrity": "sha512-obICrTmIkVKXX0vAjGiezOK/cu3iUH6i67fuKLe7Fqcp/Upwrsr70Ap68YxyMvpNOvE4TdbbM1vU+2uGXwM5sg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@relayfile/adapter-core": "^0.5.18" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@relayfile/sdk": ">=0.6.0 <1" + } + }, + "node_modules/@relayfile/adapter-reddit": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@relayfile/adapter-reddit/-/adapter-reddit-0.2.9.tgz", + "integrity": "sha512-/ZWkr4SguRRCk4fY6DEtP5NVUHSMWP66GtMou4NJOcf5PaxYSa0ceSVxAN/IcTBG6JqdtbqB4IoWn8V9d/JC3g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@relayfile/adapter-core": "^0.5.15" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@relayfile/sdk": ">=0.6.0 <1" + } + }, + "node_modules/@relayfile/core": { + "version": "0.10.57", + "resolved": "https://registry.npmjs.org/@relayfile/core/-/core-0.10.57.tgz", + "integrity": "sha512-oKwNJKGovzdl/6IF3sSVZFsiG2ITH7jXnmFgxbCIYPZ+J7QsQ2zvBhT35sPGiNcD4FfiMpyX9OB8LJXOefNdcQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@relayfile/mount-darwin-arm64": { + "version": "0.10.57", + "resolved": "https://registry.npmjs.org/@relayfile/mount-darwin-arm64/-/mount-darwin-arm64-0.10.57.tgz", + "integrity": "sha512-yCcjQ7HoCJECnjtZeG7eZwLXa4RAT3RAVAAymdIqKZbMe3wbkF1kg3YNQHuRqYsDEIQzGkA4k5AsITmG1j2PfQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@relayfile/mount-darwin-x64": { + "version": "0.10.57", + "resolved": "https://registry.npmjs.org/@relayfile/mount-darwin-x64/-/mount-darwin-x64-0.10.57.tgz", + "integrity": "sha512-5Ze1a16Mxc2PTj2Q+AdtRZIMKQ4sxJIIXwPjdWD+9rTMEQeqs7xzSZ1WRIjI89FzyNHBjx/+gzD9FK8Kjuc7Yg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@relayfile/mount-linux-arm64": { + "version": "0.10.57", + "resolved": "https://registry.npmjs.org/@relayfile/mount-linux-arm64/-/mount-linux-arm64-0.10.57.tgz", + "integrity": "sha512-+2MhcDwzb7KSC3qjKWOW+9EnZ8mFKJiVns5fiJwO1oThiAd3CnnCfJzYkbEWRKIL0dH5wg4npdayF23DiBtB5w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@relayfile/mount-linux-x64": { + "version": "0.10.57", + "resolved": "https://registry.npmjs.org/@relayfile/mount-linux-x64/-/mount-linux-x64-0.10.57.tgz", + "integrity": "sha512-Y0OGuGEpglQsUrrR0uzjBzqFyrM8wU5jG1tELJ8LBNhedNUEUupGKD7ixs+i4k9kR8Q68voVsQTfzdgqzW3M+Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@relayfile/relay-helpers": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/@relayfile/relay-helpers/-/relay-helpers-0.4.11.tgz", + "integrity": "sha512-J9S2L+dVRQcxv32BxMq+xEqpBJoG1mNzo1E/CCXY4LYUSl0euZD0ZayaFMcpBhGqx+2IDNiSlGjFun/YQk9OqA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@relayfile/adapter-core": "^0.5.15", + "@relayfile/adapter-linear": "^0.4.11", + "@relayfile/adapter-reddit": "^0.2.9" + } + }, + "node_modules/@relayfile/sdk": { + "version": "0.10.57", + "resolved": "https://registry.npmjs.org/@relayfile/sdk/-/sdk-0.10.57.tgz", + "integrity": "sha512-9AJIEyyruqVogbdqVfY7q0pjuXJy8IhzxR/1pvwWJiybU1BZO8LFNurlFePTAo/uhLG9cshr0YJRBxv2WbG0Sg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@relayfile/core": "0.10.57", + "ignore": "^7.0.5", + "tar": "^7.5.10" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@relayfile/mount-darwin-arm64": "0.10.57", + "@relayfile/mount-darwin-x64": "0.10.57", + "@relayfile/mount-linux-arm64": "0.10.57", + "@relayfile/mount-linux-x64": "0.10.57" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.63.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", @@ -879,6 +1048,40 @@ "win32" ] }, + "node_modules/@scalar/helpers": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.5.1.tgz", + "integrity": "sha512-9VvPfv8b+YZVIFwR3SWeq4Y8ij/kU3/kf2M6NKcbf2iVyh63d8s0ssap5m/nOhiz/Puidv/29MAJlJCA0LRssA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-types": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.7.0.tgz", + "integrity": "sha512-kN0PwlJW0de4bwQ4ib+mBHzKJUvBCyR/gwU4zLEq6SCbj+GfgYUh+2a0/yl1WYVUiSkkwFsHjfmQ8KjhR3HK0Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/postman-to-openapi": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@scalar/postman-to-openapi/-/postman-to-openapi-0.6.3.tgz", + "integrity": "sha512-Y/tMuRZG34wEfpTxDfXFp5o2X3ibb5ojGWupGJ9ZxkThCx7rOGydnszJPzEbgDK3eF6nJ6UuE7bCTpIEutYnPw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@scalar/helpers": "0.5.1", + "@scalar/openapi-types": "0.7.0" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1082,6 +1285,16 @@ "node": ">=12" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -1119,6 +1332,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC", + "peer": true + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1194,6 +1427,60 @@ "node": ">= 16" } }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "peer": true, + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -1265,6 +1552,36 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1301,6 +1618,65 @@ "node": ">= 0.8" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1330,6 +1706,46 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1691,6 +2107,39 @@ "node": ">=16.9.0" } }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -1727,6 +2176,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ignore": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1860,6 +2319,45 @@ "url": "https://opencollective.com/express" } }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "peer": true, + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1914,6 +2412,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1956,6 +2467,59 @@ "wrappy": "1" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "peer": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "peer": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2370,6 +2934,23 @@ "dev": true, "license": "MIT" }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -2468,6 +3049,23 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -2486,13 +3084,6 @@ "node": ">= 0.8" } }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -2642,6 +3233,43 @@ } } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "peer": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2680,6 +3308,32 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/zod": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.2.tgz", diff --git a/packages/surface/src/effect-transport.ts b/packages/surface/src/effect-transport.ts new file mode 100644 index 000000000..3caaeb463 --- /dev/null +++ b/packages/surface/src/effect-transport.ts @@ -0,0 +1,59 @@ +import type { RelayClientOptions, RelayTransport } from '@relayfile/relay-helpers/transport'; +import type { Step } from './step.js'; + +/** Promise-returning client verbs become lazy, journal-owned steps. */ +export type JournalHelper = { + [K in keyof T]: T[K] extends (...args: infer A) => Promise + ? (...args: A) => Step + : T[K] extends (...args: infer A) => infer R ? (...args: A) => R + : T[K] extends object ? JournalHelper : T[K]; +}; +export interface HelperCall { + type: 'effect'; + provider: string; + verb: string; + args: unknown[]; +} +export type EffectDispatcher = (call: HelperCall) => Step; +export interface UnavailableHelper { readonly available: false } +export type HelperFactory = (options: RelayClientOptions) => object; + +const unavailableTransport: RelayTransport = { + async read() { throw new Error('Helper I/O requires a journaled step'); }, + async list() { throw new Error('Helper I/O requires a journaled step'); }, + async write() { throw new Error('Helper I/O requires a journaled step'); }, +}; + +export function bindHelper(provider: string, factory: (options: RelayClientOptions) => T, dispatch: EffectDispatcher): JournalHelper; +export function bindHelper(provider: string, factory: undefined, dispatch: EffectDispatcher): UnavailableHelper; +export function bindHelper(provider: string, factory: HelperFactory | undefined, dispatch: EffectDispatcher): object { + if (!factory) return Object.freeze({ available: false }); + function wrap(client: object, prefix = ''): object { + return Object.fromEntries(Object.entries(client).map(([key, value]) => { + const verb = `${prefix}${key}`; + if (typeof value === 'function') return [key, key === 'path' + ? value.bind(client) + : (...args: unknown[]) => { + while (args.length && args.at(-1) === undefined) args.pop(); + return dispatch({ type: 'effect', provider, verb, args }); + }]; + return [key, value && typeof value === 'object' ? wrap(value, `${verb}.`) : value]; + })); + } + return wrap(factory({ transport: unavailableTransport })); +} + +/** Resolve only own, generated client methods; never arbitrary prototype members. */ +export async function invokeHelper(factory: HelperFactory, call: HelperCall, transport: RelayTransport): Promise { + let target: unknown = factory({ transport }); + const parts = call.verb.split('.'); + for (const part of parts.slice(0, -1)) { + if (!target || typeof target !== 'object' || !Object.hasOwn(target, part)) throw new Error('Unknown helper resource'); + target = (target as Record)[part]; + } + const verb = parts.at(-1)!; + if (!target || typeof target !== 'object' || !Object.hasOwn(target, verb) || verb === 'path') throw new Error('Unknown helper verb'); + const method = (target as Record)[verb]; + if (typeof method !== 'function') throw new Error('Unknown helper verb'); + return await method.apply(target, call.args) ?? null; +} diff --git a/packages/surface/src/flow.ts b/packages/surface/src/flow.ts index 4eec2c712..10193c7d7 100644 --- a/packages/surface/src/flow.ts +++ b/packages/surface/src/flow.ts @@ -1,3 +1,6 @@ +import { helperProviders } from "./helpers/providers.js"; +import type { Helpers } from "./helpers/index.js"; +type HelperTools = Partial>; import type { Ctx } from "./context.js"; import { webhook, type TriggerSource } from "./triggers.js"; @@ -8,7 +11,7 @@ export interface FlowHeader { identity?: string; memory?: { script?: boolean; agent?: boolean }; budget?: string | { tokens?: number; dollars?: number; wallclock?: string }; - tools?: { slack?: boolean; relayfile?: string[]; mcp?: string[] }; + tools?: HelperTools & { relayfile?: string[]; mcp?: string[] }; workspace?: string; } @@ -19,7 +22,7 @@ export interface ReadonlyFlowHeader { readonly identity?: string; readonly memory?: Readonly<{ script?: boolean; agent?: boolean }>; readonly budget?: string | Readonly<{ tokens?: number; dollars?: number; wallclock?: string }>; - readonly tools?: Readonly<{ + readonly tools?: Readonly header.tools?.[p.namespace] !== undefined) + .map(p => [p.namespace, header.tools![p.namespace]])), ...(header.tools.relayfile === undefined ? {} : { relayfile: Object.freeze([...header.tools.relayfile]) }), @@ -224,10 +228,10 @@ function assertFlowHeader(value: unknown, flowName: string): asserts value is Fl assertHeaderObject(value.tools, `${at}.tools`); assertKnownKeys( value.tools, - ["relayfile", "mcp", "slack"], + ["relayfile", "mcp", ...helperProviders.map(p => p.namespace)], `${at}.tools`, ); - assertOptionalBoolean(value.tools, "slack", `${at}.tools`); + for (const { namespace } of helperProviders) assertOptionalBoolean(value.tools, namespace, `${at}.tools`); assertOptionalStringArray(value.tools, "relayfile", `${at}.tools`); assertOptionalStringArray(value.tools, "mcp", `${at}.tools`); } diff --git a/packages/surface/src/helper-clients.ts b/packages/surface/src/helper-clients.ts new file mode 100644 index 000000000..03a54fdf2 --- /dev/null +++ b/packages/surface/src/helper-clients.ts @@ -0,0 +1,30 @@ +import { githubClient as upstreamGithub, notionClient as upstreamNotion, created } from '@relayfile/relay-helpers'; +import type { RelayClientOptions } from '@relayfile/relay-helpers/transport'; + +/** Accept the conventional owner/repo shorthand as well as upstream arguments. */ +export function githubClient(options: RelayClientOptions) { + const client = upstreamGithub(options); + return { ...client, createIssue(args: { owner?: string; repo: string; title: string; body: string; labels?: string[] }) { + const [owner, repo, extra] = args.repo.split('/'); + if (!args.owner && (!owner || !repo || extra)) throw new Error('github.createIssue requires repo: "owner/repo" or an explicit owner'); + return client.createIssue({ ...args, owner: args.owner ?? owner!, repo: args.owner ? args.repo : repo! }); + } }; +} + +export function stripeClient(options: RelayClientOptions) { + return { createInvoice(args: { customer: string; auto_advance?: boolean; collection_method?: 'charge_automatically' | 'send_invoice'; days_until_due?: number; description?: string; metadata?: Record }) { + if (!options.transport) throw new Error('Stripe requires a journal transport'); + return created(options.transport.write({ provider: 'stripe', resource: 'invoices', + parameters: {}, path: '/stripe/invoices', body: args })); + } }; +} + +export function notionClient(options: RelayClientOptions) { + return { ...upstreamNotion(options), async appendBlock(args: { pageId: string; block: Record }) { + if (!options.transport) throw new Error('Notion requires a journal transport'); + // The adapter does not yet support this route. The runtime refuses it in + // mount mode, while mock mode can exercise authoring and effect lowering. + return created(options.transport.write({ provider: 'notion', resource: 'blocks', + parameters: { pageId: args.pageId }, path: `/notion/pages/${encodeURIComponent(args.pageId)}/blocks`, body: { children: [args.block] } })); + } }; +} diff --git a/packages/surface/src/helpers/airtable.ts b/packages/surface/src/helpers/airtable.ts new file mode 100644 index 000000000..e62767d26 --- /dev/null +++ b/packages/surface/src/helpers/airtable.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type AirtableHelper = UnavailableHelper; + +export const createAirtableHelper = (dispatch: EffectDispatcher): AirtableHelper => + bindHelper("airtable", undefined, dispatch); diff --git a/packages/surface/src/helpers/asana.ts b/packages/surface/src/helpers/asana.ts new file mode 100644 index 000000000..35e6d2860 --- /dev/null +++ b/packages/surface/src/helpers/asana.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { asanaClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type AsanaHelper = JournalHelper>; + +export const createAsanaHelper = (dispatch: EffectDispatcher): AsanaHelper => + bindHelper("asana", asanaClient, dispatch); diff --git a/packages/surface/src/helpers/azure-blob.ts b/packages/surface/src/helpers/azure-blob.ts new file mode 100644 index 000000000..508d36d04 --- /dev/null +++ b/packages/surface/src/helpers/azure-blob.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { azureBlobClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type AzureBlobHelper = JournalHelper>; + +export const createAzureBlobHelper = (dispatch: EffectDispatcher): AzureBlobHelper => + bindHelper("azure-blob", azureBlobClient, dispatch); diff --git a/packages/surface/src/helpers/box.ts b/packages/surface/src/helpers/box.ts new file mode 100644 index 000000000..8ed424fac --- /dev/null +++ b/packages/surface/src/helpers/box.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { boxClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type BoxHelper = JournalHelper>; + +export const createBoxHelper = (dispatch: EffectDispatcher): BoxHelper => + bindHelper("box", boxClient, dispatch); diff --git a/packages/surface/src/helpers/calendly.ts b/packages/surface/src/helpers/calendly.ts new file mode 100644 index 000000000..8287923d9 --- /dev/null +++ b/packages/surface/src/helpers/calendly.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { calendlyClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type CalendlyHelper = JournalHelper>; + +export const createCalendlyHelper = (dispatch: EffectDispatcher): CalendlyHelper => + bindHelper("calendly", calendlyClient, dispatch); diff --git a/packages/surface/src/helpers/clickup.ts b/packages/surface/src/helpers/clickup.ts new file mode 100644 index 000000000..707f0c8c9 --- /dev/null +++ b/packages/surface/src/helpers/clickup.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { clickupClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type ClickupHelper = JournalHelper>; + +export const createClickupHelper = (dispatch: EffectDispatcher): ClickupHelper => + bindHelper("clickup", clickupClient, dispatch); diff --git a/packages/surface/src/helpers/clients.ts b/packages/surface/src/helpers/clients.ts new file mode 100644 index 000000000..f74167f7a --- /dev/null +++ b/packages/surface/src/helpers/clients.ts @@ -0,0 +1,48 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import * as upstream from "@relayfile/relay-helpers"; +import * as custom from "../helper-clients.js"; +import type { HelperFactory } from "../effect-transport.js"; + +export const helperClients: Readonly> = { + "asana": upstream.asanaClient, + "azure-blob": upstream.azureBlobClient, + "box": upstream.boxClient, + "calendly": upstream.calendlyClient, + "clickup": upstream.clickupClient, + "cloudflare": upstream.cloudflareClient, + "confluence": upstream.confluenceClient, + "daytona": upstream.daytonaClient, + "dropbox": upstream.dropboxClient, + "gcs": upstream.gcsClient, + "github": custom.githubClient, + "gitlab": upstream.gitlabClient, + "gmail": upstream.gmailClient, + "google-calendar": upstream.googleCalendarClient, + "google-drive": upstream.googleDriveClient, + "granola": upstream.granolaClient, + "hubspot": upstream.hubspotClient, + "intercom": upstream.intercomClient, + "jira": upstream.jiraClient, + "linear": upstream.linearClient, + "mailgun": upstream.mailgunClient, + "mixpanel": upstream.mixpanelClient, + "notion": custom.notionClient, + "onedrive": upstream.onedriveClient, + "pipedrive": upstream.pipedriveClient, + "postgres": upstream.postgresClient, + "ramp": (options) => upstream.providerClient("ramp", options), + "recall": upstream.recallClient, + "reddit": upstream.redditClient, + "redis": upstream.redisClient, + "s3": upstream.s3Client, + "salesforce": upstream.salesforceClient, + "sendgrid": upstream.sendgridClient, + "sharepoint": upstream.sharepointClient, + "shortcut": upstream.shortcutClient, + "stripe": custom.stripeClient, + "teams": upstream.teamsClient, + "telegram": upstream.telegramClient, + "zendesk": upstream.zendeskClient, +}; diff --git a/packages/surface/src/helpers/cloudflare.ts b/packages/surface/src/helpers/cloudflare.ts new file mode 100644 index 000000000..bafbec1a6 --- /dev/null +++ b/packages/surface/src/helpers/cloudflare.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { cloudflareClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type CloudflareHelper = JournalHelper>; + +export const createCloudflareHelper = (dispatch: EffectDispatcher): CloudflareHelper => + bindHelper("cloudflare", cloudflareClient, dispatch); diff --git a/packages/surface/src/helpers/confluence.ts b/packages/surface/src/helpers/confluence.ts new file mode 100644 index 000000000..4afbe977d --- /dev/null +++ b/packages/surface/src/helpers/confluence.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { confluenceClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type ConfluenceHelper = JournalHelper>; + +export const createConfluenceHelper = (dispatch: EffectDispatcher): ConfluenceHelper => + bindHelper("confluence", confluenceClient, dispatch); diff --git a/packages/surface/src/helpers/daytona.ts b/packages/surface/src/helpers/daytona.ts new file mode 100644 index 000000000..af220cbb6 --- /dev/null +++ b/packages/surface/src/helpers/daytona.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { daytonaClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type DaytonaHelper = JournalHelper>; + +export const createDaytonaHelper = (dispatch: EffectDispatcher): DaytonaHelper => + bindHelper("daytona", daytonaClient, dispatch); diff --git a/packages/surface/src/helpers/docker-hub.ts b/packages/surface/src/helpers/docker-hub.ts new file mode 100644 index 000000000..82fe1b7d5 --- /dev/null +++ b/packages/surface/src/helpers/docker-hub.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type DockerHubHelper = UnavailableHelper; + +export const createDockerHubHelper = (dispatch: EffectDispatcher): DockerHubHelper => + bindHelper("docker-hub", undefined, dispatch); diff --git a/packages/surface/src/helpers/dropbox.ts b/packages/surface/src/helpers/dropbox.ts new file mode 100644 index 000000000..2c6b9f50e --- /dev/null +++ b/packages/surface/src/helpers/dropbox.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { dropboxClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type DropboxHelper = JournalHelper>; + +export const createDropboxHelper = (dispatch: EffectDispatcher): DropboxHelper => + bindHelper("dropbox", dropboxClient, dispatch); diff --git a/packages/surface/src/helpers/fathom.ts b/packages/surface/src/helpers/fathom.ts new file mode 100644 index 000000000..59ae6742f --- /dev/null +++ b/packages/surface/src/helpers/fathom.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type FathomHelper = UnavailableHelper; + +export const createFathomHelper = (dispatch: EffectDispatcher): FathomHelper => + bindHelper("fathom", undefined, dispatch); diff --git a/packages/surface/src/helpers/gcp.ts b/packages/surface/src/helpers/gcp.ts new file mode 100644 index 000000000..2d8970d22 --- /dev/null +++ b/packages/surface/src/helpers/gcp.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type GcpHelper = UnavailableHelper; + +export const createGcpHelper = (dispatch: EffectDispatcher): GcpHelper => + bindHelper("gcp", undefined, dispatch); diff --git a/packages/surface/src/helpers/gcs.ts b/packages/surface/src/helpers/gcs.ts new file mode 100644 index 000000000..dc0609a9e --- /dev/null +++ b/packages/surface/src/helpers/gcs.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { gcsClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type GcsHelper = JournalHelper>; + +export const createGcsHelper = (dispatch: EffectDispatcher): GcsHelper => + bindHelper("gcs", gcsClient, dispatch); diff --git a/packages/surface/src/helpers/github.ts b/packages/surface/src/helpers/github.ts new file mode 100644 index 000000000..2814756ca --- /dev/null +++ b/packages/surface/src/helpers/github.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { githubClient } from "../helper-clients.js"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type GithubHelper = JournalHelper>; + +export const createGithubHelper = (dispatch: EffectDispatcher): GithubHelper => + bindHelper("github", githubClient, dispatch); diff --git a/packages/surface/src/helpers/gitlab.ts b/packages/surface/src/helpers/gitlab.ts new file mode 100644 index 000000000..3d338f0c8 --- /dev/null +++ b/packages/surface/src/helpers/gitlab.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { gitlabClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type GitlabHelper = JournalHelper>; + +export const createGitlabHelper = (dispatch: EffectDispatcher): GitlabHelper => + bindHelper("gitlab", gitlabClient, dispatch); diff --git a/packages/surface/src/helpers/gmail.ts b/packages/surface/src/helpers/gmail.ts new file mode 100644 index 000000000..87bebc8c5 --- /dev/null +++ b/packages/surface/src/helpers/gmail.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { gmailClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type GmailHelper = JournalHelper>; + +export const createGmailHelper = (dispatch: EffectDispatcher): GmailHelper => + bindHelper("gmail", gmailClient, dispatch); diff --git a/packages/surface/src/helpers/google-calendar.ts b/packages/surface/src/helpers/google-calendar.ts new file mode 100644 index 000000000..750a77853 --- /dev/null +++ b/packages/surface/src/helpers/google-calendar.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { googleCalendarClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type GoogleCalendarHelper = JournalHelper>; + +export const createGoogleCalendarHelper = (dispatch: EffectDispatcher): GoogleCalendarHelper => + bindHelper("google-calendar", googleCalendarClient, dispatch); diff --git a/packages/surface/src/helpers/google-drive.ts b/packages/surface/src/helpers/google-drive.ts new file mode 100644 index 000000000..8ca401245 --- /dev/null +++ b/packages/surface/src/helpers/google-drive.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { googleDriveClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type GoogleDriveHelper = JournalHelper>; + +export const createGoogleDriveHelper = (dispatch: EffectDispatcher): GoogleDriveHelper => + bindHelper("google-drive", googleDriveClient, dispatch); diff --git a/packages/surface/src/helpers/granola.ts b/packages/surface/src/helpers/granola.ts new file mode 100644 index 000000000..317069328 --- /dev/null +++ b/packages/surface/src/helpers/granola.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { granolaClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type GranolaHelper = JournalHelper>; + +export const createGranolaHelper = (dispatch: EffectDispatcher): GranolaHelper => + bindHelper("granola", granolaClient, dispatch); diff --git a/packages/surface/src/helpers/hubspot.ts b/packages/surface/src/helpers/hubspot.ts new file mode 100644 index 000000000..95b8f44de --- /dev/null +++ b/packages/surface/src/helpers/hubspot.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { hubspotClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type HubspotHelper = JournalHelper>; + +export const createHubspotHelper = (dispatch: EffectDispatcher): HubspotHelper => + bindHelper("hubspot", hubspotClient, dispatch); diff --git a/packages/surface/src/helpers/index.ts b/packages/surface/src/helpers/index.ts index c62a5cba8..0dcbe20c9 100644 --- a/packages/surface/src/helpers/index.ts +++ b/packages/surface/src/helpers/index.ts @@ -2,10 +2,211 @@ // Run `npm run gen --prefix packages/surface` from the repository root. import type { SlackHelper } from "./slack.js"; +import { createAirtableHelper, type AirtableHelper } from "./airtable.js"; +import { createAsanaHelper, type AsanaHelper } from "./asana.js"; +import { createAzureBlobHelper, type AzureBlobHelper } from "./azure-blob.js"; +import { createBoxHelper, type BoxHelper } from "./box.js"; +import { createCalendlyHelper, type CalendlyHelper } from "./calendly.js"; +import { createClickupHelper, type ClickupHelper } from "./clickup.js"; +import { createCloudflareHelper, type CloudflareHelper } from "./cloudflare.js"; +import { createConfluenceHelper, type ConfluenceHelper } from "./confluence.js"; +import { createDaytonaHelper, type DaytonaHelper } from "./daytona.js"; +import { createDockerHubHelper, type DockerHubHelper } from "./docker-hub.js"; +import { createDropboxHelper, type DropboxHelper } from "./dropbox.js"; +import { createFathomHelper, type FathomHelper } from "./fathom.js"; +import { createGcpHelper, type GcpHelper } from "./gcp.js"; +import { createGcsHelper, type GcsHelper } from "./gcs.js"; +import { createGithubHelper, type GithubHelper } from "./github.js"; +import { createGitlabHelper, type GitlabHelper } from "./gitlab.js"; +import { createGmailHelper, type GmailHelper } from "./gmail.js"; +import { createGoogleCalendarHelper, type GoogleCalendarHelper } from "./google-calendar.js"; +import { createGoogleDriveHelper, type GoogleDriveHelper } from "./google-drive.js"; +import { createGranolaHelper, type GranolaHelper } from "./granola.js"; +import { createHubspotHelper, type HubspotHelper } from "./hubspot.js"; +import { createIntercomHelper, type IntercomHelper } from "./intercom.js"; +import { createJiraHelper, type JiraHelper } from "./jira.js"; +import { createLinearHelper, type LinearHelper } from "./linear.js"; +import { createMailgunHelper, type MailgunHelper } from "./mailgun.js"; +import { createMixpanelHelper, type MixpanelHelper } from "./mixpanel.js"; +import { createNeonHelper, type NeonHelper } from "./neon.js"; +import { createNotionHelper, type NotionHelper } from "./notion.js"; +import { createOnedriveHelper, type OnedriveHelper } from "./onedrive.js"; +import { createPipedriveHelper, type PipedriveHelper } from "./pipedrive.js"; +import { createPostgresHelper, type PostgresHelper } from "./postgres.js"; +import { createPosthogHelper, type PosthogHelper } from "./posthog.js"; +import { createRampHelper, type RampHelper } from "./ramp.js"; +import { createRecallHelper, type RecallHelper } from "./recall.js"; +import { createRedditHelper, type RedditHelper } from "./reddit.js"; +import { createRedisHelper, type RedisHelper } from "./redis.js"; +import { createS3Helper, type S3Helper } from "./s3.js"; +import { createSalesforceHelper, type SalesforceHelper } from "./salesforce.js"; +import { createSegmentHelper, type SegmentHelper } from "./segment.js"; +import { createSendgridHelper, type SendgridHelper } from "./sendgrid.js"; +import { createSharepointHelper, type SharepointHelper } from "./sharepoint.js"; +import { createShopifyHelper, type ShopifyHelper } from "./shopify.js"; +import { createShortcutHelper, type ShortcutHelper } from "./shortcut.js"; +import { createStripeHelper, type StripeHelper } from "./stripe.js"; +import { createTeamsHelper, type TeamsHelper } from "./teams.js"; +import { createTelegramHelper, type TelegramHelper } from "./telegram.js"; +import { createWebhookServerHelper, type WebhookServerHelper } from "./webhook-server.js"; +import { createXHelper, type XHelper } from "./x.js"; +import { createZendeskHelper, type ZendeskHelper } from "./zendesk.js"; +import type { EffectDispatcher } from "../effect-transport.js"; +export type { AirtableHelper } from "./airtable.js"; +export type { AsanaHelper } from "./asana.js"; +export type { AzureBlobHelper } from "./azure-blob.js"; +export type { BoxHelper } from "./box.js"; +export type { CalendlyHelper } from "./calendly.js"; +export type { ClickupHelper } from "./clickup.js"; +export type { CloudflareHelper } from "./cloudflare.js"; +export type { ConfluenceHelper } from "./confluence.js"; +export type { DaytonaHelper } from "./daytona.js"; +export type { DockerHubHelper } from "./docker-hub.js"; +export type { DropboxHelper } from "./dropbox.js"; +export type { FathomHelper } from "./fathom.js"; +export type { GcpHelper } from "./gcp.js"; +export type { GcsHelper } from "./gcs.js"; +export type { GithubHelper } from "./github.js"; +export type { GitlabHelper } from "./gitlab.js"; +export type { GmailHelper } from "./gmail.js"; +export type { GoogleCalendarHelper } from "./google-calendar.js"; +export type { GoogleDriveHelper } from "./google-drive.js"; +export type { GranolaHelper } from "./granola.js"; +export type { HubspotHelper } from "./hubspot.js"; +export type { IntercomHelper } from "./intercom.js"; +export type { JiraHelper } from "./jira.js"; +export type { LinearHelper } from "./linear.js"; +export type { MailgunHelper } from "./mailgun.js"; +export type { MixpanelHelper } from "./mixpanel.js"; +export type { NeonHelper } from "./neon.js"; +export type { NotionHelper } from "./notion.js"; +export type { OnedriveHelper } from "./onedrive.js"; +export type { PipedriveHelper } from "./pipedrive.js"; +export type { PostgresHelper } from "./postgres.js"; +export type { PosthogHelper } from "./posthog.js"; +export type { RampHelper } from "./ramp.js"; +export type { RecallHelper } from "./recall.js"; +export type { RedditHelper } from "./reddit.js"; +export type { RedisHelper } from "./redis.js"; +export type { S3Helper } from "./s3.js"; +export type { SalesforceHelper } from "./salesforce.js"; +export type { SegmentHelper } from "./segment.js"; +export type { SendgridHelper } from "./sendgrid.js"; +export type { SharepointHelper } from "./sharepoint.js"; +export type { ShopifyHelper } from "./shopify.js"; +export type { ShortcutHelper } from "./shortcut.js"; export type { SlackHelper } from "./slack.js"; +export type { StripeHelper } from "./stripe.js"; +export type { TeamsHelper } from "./teams.js"; +export type { TelegramHelper } from "./telegram.js"; +export type { WebhookServerHelper } from "./webhook-server.js"; +export type { XHelper } from "./x.js"; +export type { ZendeskHelper } from "./zendesk.js"; -/** Supported helper namespaces composed into Ctx. */ export interface Helpers { + airtable: AirtableHelper; + asana: AsanaHelper; + azureBlob: AzureBlobHelper; + box: BoxHelper; + calendly: CalendlyHelper; + clickup: ClickupHelper; + cloudflare: CloudflareHelper; + confluence: ConfluenceHelper; + daytona: DaytonaHelper; + dockerHub: DockerHubHelper; + dropbox: DropboxHelper; + fathom: FathomHelper; + gcp: GcpHelper; + gcs: GcsHelper; + github: GithubHelper; + gitlab: GitlabHelper; + gmail: GmailHelper; + googleCalendar: GoogleCalendarHelper; + googleDrive: GoogleDriveHelper; + granola: GranolaHelper; + hubspot: HubspotHelper; + intercom: IntercomHelper; + jira: JiraHelper; + linear: LinearHelper; + mailgun: MailgunHelper; + mixpanel: MixpanelHelper; + neon: NeonHelper; + notion: NotionHelper; + onedrive: OnedriveHelper; + pipedrive: PipedriveHelper; + postgres: PostgresHelper; + posthog: PosthogHelper; + ramp: RampHelper; + recall: RecallHelper; + reddit: RedditHelper; + redis: RedisHelper; + s3: S3Helper; + salesforce: SalesforceHelper; + segment: SegmentHelper; + sendgrid: SendgridHelper; + sharepoint: SharepointHelper; + shopify: ShopifyHelper; + shortcut: ShortcutHelper; slack: SlackHelper; + stripe: StripeHelper; + teams: TeamsHelper; + telegram: TelegramHelper; + webhookServer: WebhookServerHelper; + x: XHelper; + zendesk: ZendeskHelper; +} + +export function createHelpers(dispatch: EffectDispatcher): Omit { + return { + airtable: createAirtableHelper(dispatch), + asana: createAsanaHelper(dispatch), + azureBlob: createAzureBlobHelper(dispatch), + box: createBoxHelper(dispatch), + calendly: createCalendlyHelper(dispatch), + clickup: createClickupHelper(dispatch), + cloudflare: createCloudflareHelper(dispatch), + confluence: createConfluenceHelper(dispatch), + daytona: createDaytonaHelper(dispatch), + dockerHub: createDockerHubHelper(dispatch), + dropbox: createDropboxHelper(dispatch), + fathom: createFathomHelper(dispatch), + gcp: createGcpHelper(dispatch), + gcs: createGcsHelper(dispatch), + github: createGithubHelper(dispatch), + gitlab: createGitlabHelper(dispatch), + gmail: createGmailHelper(dispatch), + googleCalendar: createGoogleCalendarHelper(dispatch), + googleDrive: createGoogleDriveHelper(dispatch), + granola: createGranolaHelper(dispatch), + hubspot: createHubspotHelper(dispatch), + intercom: createIntercomHelper(dispatch), + jira: createJiraHelper(dispatch), + linear: createLinearHelper(dispatch), + mailgun: createMailgunHelper(dispatch), + mixpanel: createMixpanelHelper(dispatch), + neon: createNeonHelper(dispatch), + notion: createNotionHelper(dispatch), + onedrive: createOnedriveHelper(dispatch), + pipedrive: createPipedriveHelper(dispatch), + postgres: createPostgresHelper(dispatch), + posthog: createPosthogHelper(dispatch), + ramp: createRampHelper(dispatch), + recall: createRecallHelper(dispatch), + reddit: createRedditHelper(dispatch), + redis: createRedisHelper(dispatch), + s3: createS3Helper(dispatch), + salesforce: createSalesforceHelper(dispatch), + segment: createSegmentHelper(dispatch), + sendgrid: createSendgridHelper(dispatch), + sharepoint: createSharepointHelper(dispatch), + shopify: createShopifyHelper(dispatch), + shortcut: createShortcutHelper(dispatch), + stripe: createStripeHelper(dispatch), + teams: createTeamsHelper(dispatch), + telegram: createTelegramHelper(dispatch), + webhookServer: createWebhookServerHelper(dispatch), + x: createXHelper(dispatch), + zendesk: createZendeskHelper(dispatch), + }; } diff --git a/packages/surface/src/helpers/intercom.ts b/packages/surface/src/helpers/intercom.ts new file mode 100644 index 000000000..d718193a1 --- /dev/null +++ b/packages/surface/src/helpers/intercom.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { intercomClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type IntercomHelper = JournalHelper>; + +export const createIntercomHelper = (dispatch: EffectDispatcher): IntercomHelper => + bindHelper("intercom", intercomClient, dispatch); diff --git a/packages/surface/src/helpers/jira.ts b/packages/surface/src/helpers/jira.ts new file mode 100644 index 000000000..571316453 --- /dev/null +++ b/packages/surface/src/helpers/jira.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { jiraClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type JiraHelper = JournalHelper>; + +export const createJiraHelper = (dispatch: EffectDispatcher): JiraHelper => + bindHelper("jira", jiraClient, dispatch); diff --git a/packages/surface/src/helpers/linear.ts b/packages/surface/src/helpers/linear.ts new file mode 100644 index 000000000..9896b3d95 --- /dev/null +++ b/packages/surface/src/helpers/linear.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { linearClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type LinearHelper = JournalHelper>; + +export const createLinearHelper = (dispatch: EffectDispatcher): LinearHelper => + bindHelper("linear", linearClient, dispatch); diff --git a/packages/surface/src/helpers/mailgun.ts b/packages/surface/src/helpers/mailgun.ts new file mode 100644 index 000000000..e9dfad15f --- /dev/null +++ b/packages/surface/src/helpers/mailgun.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { mailgunClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type MailgunHelper = JournalHelper>; + +export const createMailgunHelper = (dispatch: EffectDispatcher): MailgunHelper => + bindHelper("mailgun", mailgunClient, dispatch); diff --git a/packages/surface/src/helpers/mixpanel.ts b/packages/surface/src/helpers/mixpanel.ts new file mode 100644 index 000000000..6fe290233 --- /dev/null +++ b/packages/surface/src/helpers/mixpanel.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { mixpanelClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type MixpanelHelper = JournalHelper>; + +export const createMixpanelHelper = (dispatch: EffectDispatcher): MixpanelHelper => + bindHelper("mixpanel", mixpanelClient, dispatch); diff --git a/packages/surface/src/helpers/neon.ts b/packages/surface/src/helpers/neon.ts new file mode 100644 index 000000000..34d2529c7 --- /dev/null +++ b/packages/surface/src/helpers/neon.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type NeonHelper = UnavailableHelper; + +export const createNeonHelper = (dispatch: EffectDispatcher): NeonHelper => + bindHelper("neon", undefined, dispatch); diff --git a/packages/surface/src/helpers/notion.ts b/packages/surface/src/helpers/notion.ts new file mode 100644 index 000000000..09eb698a0 --- /dev/null +++ b/packages/surface/src/helpers/notion.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { notionClient } from "../helper-clients.js"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type NotionHelper = JournalHelper>; + +export const createNotionHelper = (dispatch: EffectDispatcher): NotionHelper => + bindHelper("notion", notionClient, dispatch); diff --git a/packages/surface/src/helpers/onedrive.ts b/packages/surface/src/helpers/onedrive.ts new file mode 100644 index 000000000..9513a5a68 --- /dev/null +++ b/packages/surface/src/helpers/onedrive.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { onedriveClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type OnedriveHelper = JournalHelper>; + +export const createOnedriveHelper = (dispatch: EffectDispatcher): OnedriveHelper => + bindHelper("onedrive", onedriveClient, dispatch); diff --git a/packages/surface/src/helpers/pipedrive.ts b/packages/surface/src/helpers/pipedrive.ts new file mode 100644 index 000000000..5c4e78a85 --- /dev/null +++ b/packages/surface/src/helpers/pipedrive.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { pipedriveClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type PipedriveHelper = JournalHelper>; + +export const createPipedriveHelper = (dispatch: EffectDispatcher): PipedriveHelper => + bindHelper("pipedrive", pipedriveClient, dispatch); diff --git a/packages/surface/src/helpers/postgres.ts b/packages/surface/src/helpers/postgres.ts new file mode 100644 index 000000000..b5f7a86b3 --- /dev/null +++ b/packages/surface/src/helpers/postgres.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { postgresClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type PostgresHelper = JournalHelper>; + +export const createPostgresHelper = (dispatch: EffectDispatcher): PostgresHelper => + bindHelper("postgres", postgresClient, dispatch); diff --git a/packages/surface/src/helpers/posthog.ts b/packages/surface/src/helpers/posthog.ts new file mode 100644 index 000000000..b0b6fb453 --- /dev/null +++ b/packages/surface/src/helpers/posthog.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type PosthogHelper = UnavailableHelper; + +export const createPosthogHelper = (dispatch: EffectDispatcher): PosthogHelper => + bindHelper("posthog", undefined, dispatch); diff --git a/packages/surface/src/helpers/providers.ts b/packages/surface/src/helpers/providers.ts new file mode 100644 index 000000000..9d29e4d52 --- /dev/null +++ b/packages/surface/src/helpers/providers.ts @@ -0,0 +1,305 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +export const helperProviders = [ + { + "provider": "airtable", + "namespace": "airtable", + "mockEnv": "RELAYFLOWS_AIRTABLE_MOCK", + "supported": false + }, + { + "provider": "asana", + "namespace": "asana", + "mockEnv": "RELAYFLOWS_ASANA_MOCK", + "supported": true + }, + { + "provider": "azure-blob", + "namespace": "azureBlob", + "mockEnv": "RELAYFLOWS_AZURE_BLOB_MOCK", + "supported": true + }, + { + "provider": "box", + "namespace": "box", + "mockEnv": "RELAYFLOWS_BOX_MOCK", + "supported": true + }, + { + "provider": "calendly", + "namespace": "calendly", + "mockEnv": "RELAYFLOWS_CALENDLY_MOCK", + "supported": true + }, + { + "provider": "clickup", + "namespace": "clickup", + "mockEnv": "RELAYFLOWS_CLICKUP_MOCK", + "supported": true + }, + { + "provider": "cloudflare", + "namespace": "cloudflare", + "mockEnv": "RELAYFLOWS_CLOUDFLARE_MOCK", + "supported": true + }, + { + "provider": "confluence", + "namespace": "confluence", + "mockEnv": "RELAYFLOWS_CONFLUENCE_MOCK", + "supported": true + }, + { + "provider": "daytona", + "namespace": "daytona", + "mockEnv": "RELAYFLOWS_DAYTONA_MOCK", + "supported": true + }, + { + "provider": "docker-hub", + "namespace": "dockerHub", + "mockEnv": "RELAYFLOWS_DOCKER_HUB_MOCK", + "supported": false + }, + { + "provider": "dropbox", + "namespace": "dropbox", + "mockEnv": "RELAYFLOWS_DROPBOX_MOCK", + "supported": true + }, + { + "provider": "fathom", + "namespace": "fathom", + "mockEnv": "RELAYFLOWS_FATHOM_MOCK", + "supported": false + }, + { + "provider": "gcp", + "namespace": "gcp", + "mockEnv": "RELAYFLOWS_GCP_MOCK", + "supported": false + }, + { + "provider": "gcs", + "namespace": "gcs", + "mockEnv": "RELAYFLOWS_GCS_MOCK", + "supported": true + }, + { + "provider": "github", + "namespace": "github", + "mockEnv": "RELAYFLOWS_GITHUB_MOCK", + "supported": true + }, + { + "provider": "gitlab", + "namespace": "gitlab", + "mockEnv": "RELAYFLOWS_GITLAB_MOCK", + "supported": true + }, + { + "provider": "gmail", + "namespace": "gmail", + "mockEnv": "RELAYFLOWS_GMAIL_MOCK", + "supported": true + }, + { + "provider": "google-calendar", + "namespace": "googleCalendar", + "mockEnv": "RELAYFLOWS_GOOGLE_CALENDAR_MOCK", + "supported": true + }, + { + "provider": "google-drive", + "namespace": "googleDrive", + "mockEnv": "RELAYFLOWS_GOOGLE_DRIVE_MOCK", + "supported": true + }, + { + "provider": "granola", + "namespace": "granola", + "mockEnv": "RELAYFLOWS_GRANOLA_MOCK", + "supported": true + }, + { + "provider": "hubspot", + "namespace": "hubspot", + "mockEnv": "RELAYFLOWS_HUBSPOT_MOCK", + "supported": true + }, + { + "provider": "intercom", + "namespace": "intercom", + "mockEnv": "RELAYFLOWS_INTERCOM_MOCK", + "supported": true + }, + { + "provider": "jira", + "namespace": "jira", + "mockEnv": "RELAYFLOWS_JIRA_MOCK", + "supported": true + }, + { + "provider": "linear", + "namespace": "linear", + "mockEnv": "RELAYFLOWS_LINEAR_MOCK", + "supported": true + }, + { + "provider": "mailgun", + "namespace": "mailgun", + "mockEnv": "RELAYFLOWS_MAILGUN_MOCK", + "supported": true + }, + { + "provider": "mixpanel", + "namespace": "mixpanel", + "mockEnv": "RELAYFLOWS_MIXPANEL_MOCK", + "supported": true + }, + { + "provider": "neon", + "namespace": "neon", + "mockEnv": "RELAYFLOWS_NEON_MOCK", + "supported": false + }, + { + "provider": "notion", + "namespace": "notion", + "mockEnv": "RELAYFLOWS_NOTION_MOCK", + "supported": true + }, + { + "provider": "onedrive", + "namespace": "onedrive", + "mockEnv": "RELAYFLOWS_ONEDRIVE_MOCK", + "supported": true + }, + { + "provider": "pipedrive", + "namespace": "pipedrive", + "mockEnv": "RELAYFLOWS_PIPEDRIVE_MOCK", + "supported": true + }, + { + "provider": "postgres", + "namespace": "postgres", + "mockEnv": "RELAYFLOWS_POSTGRES_MOCK", + "supported": true + }, + { + "provider": "posthog", + "namespace": "posthog", + "mockEnv": "RELAYFLOWS_POSTHOG_MOCK", + "supported": false + }, + { + "provider": "ramp", + "namespace": "ramp", + "mockEnv": "RELAYFLOWS_RAMP_MOCK", + "supported": true + }, + { + "provider": "recall", + "namespace": "recall", + "mockEnv": "RELAYFLOWS_RECALL_MOCK", + "supported": true + }, + { + "provider": "reddit", + "namespace": "reddit", + "mockEnv": "RELAYFLOWS_REDDIT_MOCK", + "supported": true + }, + { + "provider": "redis", + "namespace": "redis", + "mockEnv": "RELAYFLOWS_REDIS_MOCK", + "supported": true + }, + { + "provider": "s3", + "namespace": "s3", + "mockEnv": "RELAYFLOWS_S3_MOCK", + "supported": true + }, + { + "provider": "salesforce", + "namespace": "salesforce", + "mockEnv": "RELAYFLOWS_SALESFORCE_MOCK", + "supported": true + }, + { + "provider": "segment", + "namespace": "segment", + "mockEnv": "RELAYFLOWS_SEGMENT_MOCK", + "supported": false + }, + { + "provider": "sendgrid", + "namespace": "sendgrid", + "mockEnv": "RELAYFLOWS_SENDGRID_MOCK", + "supported": true + }, + { + "provider": "sharepoint", + "namespace": "sharepoint", + "mockEnv": "RELAYFLOWS_SHAREPOINT_MOCK", + "supported": true + }, + { + "provider": "shopify", + "namespace": "shopify", + "mockEnv": "RELAYFLOWS_SHOPIFY_MOCK", + "supported": false + }, + { + "provider": "shortcut", + "namespace": "shortcut", + "mockEnv": "RELAYFLOWS_SHORTCUT_MOCK", + "supported": true + }, + { + "provider": "slack", + "namespace": "slack", + "mockEnv": "RELAYFLOWS_SLACK_MOCK", + "supported": true + }, + { + "provider": "stripe", + "namespace": "stripe", + "mockEnv": "RELAYFLOWS_STRIPE_MOCK", + "supported": true + }, + { + "provider": "teams", + "namespace": "teams", + "mockEnv": "RELAYFLOWS_TEAMS_MOCK", + "supported": true + }, + { + "provider": "telegram", + "namespace": "telegram", + "mockEnv": "RELAYFLOWS_TELEGRAM_MOCK", + "supported": true + }, + { + "provider": "webhook-server", + "namespace": "webhookServer", + "mockEnv": "RELAYFLOWS_WEBHOOK_SERVER_MOCK", + "supported": false + }, + { + "provider": "x", + "namespace": "x", + "mockEnv": "RELAYFLOWS_X_MOCK", + "supported": false + }, + { + "provider": "zendesk", + "namespace": "zendesk", + "mockEnv": "RELAYFLOWS_ZENDESK_MOCK", + "supported": true + } +] as const; diff --git a/packages/surface/src/helpers/ramp.ts b/packages/surface/src/helpers/ramp.ts new file mode 100644 index 000000000..9c42a021e --- /dev/null +++ b/packages/surface/src/helpers/ramp.ts @@ -0,0 +1,12 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { providerClient } from "@relayfile/relay-helpers"; +import type { RelayClientOptions } from "@relayfile/relay-helpers/transport"; +const rampClient = (options: RelayClientOptions) => providerClient("ramp", options); +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type RampHelper = JournalHelper>; + +export const createRampHelper = (dispatch: EffectDispatcher): RampHelper => + bindHelper("ramp", rampClient, dispatch); diff --git a/packages/surface/src/helpers/recall.ts b/packages/surface/src/helpers/recall.ts new file mode 100644 index 000000000..a6ea32ad2 --- /dev/null +++ b/packages/surface/src/helpers/recall.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { recallClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type RecallHelper = JournalHelper>; + +export const createRecallHelper = (dispatch: EffectDispatcher): RecallHelper => + bindHelper("recall", recallClient, dispatch); diff --git a/packages/surface/src/helpers/reddit.ts b/packages/surface/src/helpers/reddit.ts new file mode 100644 index 000000000..7876dea78 --- /dev/null +++ b/packages/surface/src/helpers/reddit.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { redditClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type RedditHelper = JournalHelper>; + +export const createRedditHelper = (dispatch: EffectDispatcher): RedditHelper => + bindHelper("reddit", redditClient, dispatch); diff --git a/packages/surface/src/helpers/redis.ts b/packages/surface/src/helpers/redis.ts new file mode 100644 index 000000000..75486fe1c --- /dev/null +++ b/packages/surface/src/helpers/redis.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { redisClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type RedisHelper = JournalHelper>; + +export const createRedisHelper = (dispatch: EffectDispatcher): RedisHelper => + bindHelper("redis", redisClient, dispatch); diff --git a/packages/surface/src/helpers/s3.ts b/packages/surface/src/helpers/s3.ts new file mode 100644 index 000000000..85b14d741 --- /dev/null +++ b/packages/surface/src/helpers/s3.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { s3Client } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type S3Helper = JournalHelper>; + +export const createS3Helper = (dispatch: EffectDispatcher): S3Helper => + bindHelper("s3", s3Client, dispatch); diff --git a/packages/surface/src/helpers/salesforce.ts b/packages/surface/src/helpers/salesforce.ts new file mode 100644 index 000000000..c47536fe1 --- /dev/null +++ b/packages/surface/src/helpers/salesforce.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { salesforceClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type SalesforceHelper = JournalHelper>; + +export const createSalesforceHelper = (dispatch: EffectDispatcher): SalesforceHelper => + bindHelper("salesforce", salesforceClient, dispatch); diff --git a/packages/surface/src/helpers/segment.ts b/packages/surface/src/helpers/segment.ts new file mode 100644 index 000000000..05d000d05 --- /dev/null +++ b/packages/surface/src/helpers/segment.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type SegmentHelper = UnavailableHelper; + +export const createSegmentHelper = (dispatch: EffectDispatcher): SegmentHelper => + bindHelper("segment", undefined, dispatch); diff --git a/packages/surface/src/helpers/sendgrid.ts b/packages/surface/src/helpers/sendgrid.ts new file mode 100644 index 000000000..7a26aa033 --- /dev/null +++ b/packages/surface/src/helpers/sendgrid.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { sendgridClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type SendgridHelper = JournalHelper>; + +export const createSendgridHelper = (dispatch: EffectDispatcher): SendgridHelper => + bindHelper("sendgrid", sendgridClient, dispatch); diff --git a/packages/surface/src/helpers/sharepoint.ts b/packages/surface/src/helpers/sharepoint.ts new file mode 100644 index 000000000..47e746bdb --- /dev/null +++ b/packages/surface/src/helpers/sharepoint.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { sharepointClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type SharepointHelper = JournalHelper>; + +export const createSharepointHelper = (dispatch: EffectDispatcher): SharepointHelper => + bindHelper("sharepoint", sharepointClient, dispatch); diff --git a/packages/surface/src/helpers/shopify.ts b/packages/surface/src/helpers/shopify.ts new file mode 100644 index 000000000..2f5bf559f --- /dev/null +++ b/packages/surface/src/helpers/shopify.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type ShopifyHelper = UnavailableHelper; + +export const createShopifyHelper = (dispatch: EffectDispatcher): ShopifyHelper => + bindHelper("shopify", undefined, dispatch); diff --git a/packages/surface/src/helpers/shortcut.ts b/packages/surface/src/helpers/shortcut.ts new file mode 100644 index 000000000..987e89907 --- /dev/null +++ b/packages/surface/src/helpers/shortcut.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { shortcutClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type ShortcutHelper = JournalHelper>; + +export const createShortcutHelper = (dispatch: EffectDispatcher): ShortcutHelper => + bindHelper("shortcut", shortcutClient, dispatch); diff --git a/packages/surface/src/helpers/stripe.ts b/packages/surface/src/helpers/stripe.ts new file mode 100644 index 000000000..9d58f6d2e --- /dev/null +++ b/packages/surface/src/helpers/stripe.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { stripeClient } from "../helper-clients.js"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type StripeHelper = JournalHelper>; + +export const createStripeHelper = (dispatch: EffectDispatcher): StripeHelper => + bindHelper("stripe", stripeClient, dispatch); diff --git a/packages/surface/src/helpers/teams.ts b/packages/surface/src/helpers/teams.ts new file mode 100644 index 000000000..4b3ae3d6c --- /dev/null +++ b/packages/surface/src/helpers/teams.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { teamsClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type TeamsHelper = JournalHelper>; + +export const createTeamsHelper = (dispatch: EffectDispatcher): TeamsHelper => + bindHelper("teams", teamsClient, dispatch); diff --git a/packages/surface/src/helpers/telegram.ts b/packages/surface/src/helpers/telegram.ts new file mode 100644 index 000000000..2389f6f75 --- /dev/null +++ b/packages/surface/src/helpers/telegram.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { telegramClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type TelegramHelper = JournalHelper>; + +export const createTelegramHelper = (dispatch: EffectDispatcher): TelegramHelper => + bindHelper("telegram", telegramClient, dispatch); diff --git a/packages/surface/src/helpers/webhook-server.ts b/packages/surface/src/helpers/webhook-server.ts new file mode 100644 index 000000000..e2f695dc7 --- /dev/null +++ b/packages/surface/src/helpers/webhook-server.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type WebhookServerHelper = UnavailableHelper; + +export const createWebhookServerHelper = (dispatch: EffectDispatcher): WebhookServerHelper => + bindHelper("webhook-server", undefined, dispatch); diff --git a/packages/surface/src/helpers/x.ts b/packages/surface/src/helpers/x.ts new file mode 100644 index 000000000..d3721bbc8 --- /dev/null +++ b/packages/surface/src/helpers/x.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type XHelper = UnavailableHelper; + +export const createXHelper = (dispatch: EffectDispatcher): XHelper => + bindHelper("x", undefined, dispatch); diff --git a/packages/surface/src/helpers/zendesk.ts b/packages/surface/src/helpers/zendesk.ts new file mode 100644 index 000000000..00e102d80 --- /dev/null +++ b/packages/surface/src/helpers/zendesk.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { zendeskClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type ZendeskHelper = JournalHelper>; + +export const createZendeskHelper = (dispatch: EffectDispatcher): ZendeskHelper => + bindHelper("zendesk", zendeskClient, dispatch); diff --git a/packages/surface/src/runtime.ts b/packages/surface/src/runtime.ts index dec331e46..85813bfd9 100644 --- a/packages/surface/src/runtime.ts +++ b/packages/surface/src/runtime.ts @@ -8,3 +8,8 @@ export { type ReadonlyFlowHeader, } from "./flow.js"; export { slackPostBody } from "./slack.js"; + +export { createHelpers } from "./helpers/index.js"; +export { helperProviders } from "./helpers/providers.js"; +export { helperClients } from "./helpers/clients.js"; +export { invokeHelper, type HelperCall } from "./effect-transport.js"; diff --git a/packages/surface/tests/fixtures/helpers.flow.ts b/packages/surface/tests/fixtures/helpers.flow.ts new file mode 100644 index 000000000..2fb0d956d --- /dev/null +++ b/packages/surface/tests/fixtures/helpers.flow.ts @@ -0,0 +1,26 @@ +import { flow, type Step } from '../../src/index.js'; + +/** Smoke fixture: these four operations must compile to journal-backed steps. */ +export default flow('helpers-smoke', async f => { + const issue = f.github.createIssue({ repo: 'owner/repo', title: 'Smoke', body: 'hello' }); + const check: Step<{ status: string }> = issue; + await check; + await f.linear.createIssue({ teamId: 'team', title: 'Smoke' }); + await f.notion.appendBlock({ pageId: 'page', block: { type: 'paragraph', paragraph: { rich_text: [] } } }); + await f.stripe.createInvoice({ customer: 'cus_123' }); + await f.slack.post('#test', 'hello'); + await f.googleDrive.files.write({}, { name: 'Smoke' }); + f.done('success'); +}); + +export const invalid = flow('invalid-helper-args', async f => { + // @ts-expect-error upstream requires teamId + await f.linear.createIssue({ title: 'Missing team' }); + // @ts-expect-error repo must be a string + await f.github.createIssue({ repo: 42, title: 'Bad repo', body: '' }); + // @ts-expect-error invoice requires a customer + await f.stripe.createInvoice({}); + // @ts-expect-error nonexistent provider verb + await f.asana.noSuchVerb({}); + f.done('success'); +}); diff --git a/packages/surface/tests/helpers.snapshot.test.ts b/packages/surface/tests/helpers.snapshot.test.ts index 5e871fd4e..3f19e44c4 100644 --- a/packages/surface/tests/helpers.snapshot.test.ts +++ b/packages/surface/tests/helpers.snapshot.test.ts @@ -5,5 +5,5 @@ import { expect, it } from 'vitest'; it('regenerates helpers byte-identically from the pinned adapter', () => { const guard = fileURLToPath(new URL('../scripts/check-generated-helpers.mjs', import.meta.url)); expect(execFileSync(process.execPath, [guard], { encoding: 'utf8' })) - .toContain('HELPERS_GENERATED_OK index.ts, slack.ts'); + .toContain('HELPERS_GENERATED_OK airtable.ts, asana.ts, azure-blob.ts'); }); diff --git a/scripts/generate-helpers.mjs b/scripts/generate-helpers.mjs index 0b5173114..c33a41a33 100644 --- a/scripts/generate-helpers.mjs +++ b/scripts/generate-helpers.mjs @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -8,33 +8,41 @@ import { parseArgs } from 'node:util'; const surface = fileURLToPath(new URL('../packages/surface/', import.meta.url)); const require = createRequire(join(surface, 'package.json')); const ts = require('typescript'); +const upstream = require('@relayfile/relay-helpers'); +const { WRITEBACK_PATH_CATALOG: catalog, ADAPTERS_WITHOUT_WRITEBACK_PATHS: missing } = require('@relayfile/adapter-core/writeback-paths'); const { values } = parseArgs({ options: { 'out-dir': { type: 'string', default: join(surface, 'src/helpers') }, 'adapters-dir': { type: 'string' }, } }); -// The pinned published declaration makes regeneration reproducible in CI. -// An explicit checkout lets adapter authors check source changes before release. -const input = values['adapters-dir'] - ? join(resolve(values['adapters-dir']), 'packages/relay-helpers/src/slack.ts') - : join(dirname(require.resolve('@relayfile/relay-helpers/package.json')), 'dist/slack.d.ts'); -const source = ts.createSourceFile(input, readFileSync(input, 'utf8'), ts.ScriptTarget.Latest, true); -assert.equal(source.parseDiagnostics.length, 0, 'Invalid Slack adapter source'); -const client = source.statements.find(node => ts.isInterfaceDeclaration(node) && node.name.text === 'SlackClient'); -assert(client, 'Missing upstream SlackClient interface'); -const verbs = ['post', 'dm', 'reply', 'react']; -assert.deepEqual(client.members.map(member => member.name?.getText(source)), verbs, - 'Slack adapter verbs changed; review dispatcher support before regenerating'); +const names = new Set([...Object.keys(catalog), ...missing.map(item => item.provider), 'webhook-server']); +if (values['adapters-dir']) { + const packages = join(resolve(values['adapters-dir']), 'packages'); + for (const entry of readdirSync(packages, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const pkg = JSON.parse(readFileSync(join(packages, entry.name, 'package.json'), 'utf8')); + if (pkg.name.startsWith('@relayfile/adapter-') && entry.name !== 'core') names.add(entry.name); + } +} +const camel = name => name.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); +const title = name => camel(name).replace(/^./, c => c.toUpperCase()); +const header = '// GENERATED by scripts/generate-helpers.mjs — do not edit.\n' + + '// Run `npm run gen --prefix packages/surface` from the repository root.\n'; +const destination = resolve(values['out-dir']); +mkdirSync(destination, { recursive: true }); +const files = {}; +// Keep Slack's established receipt and argument contract, copied from its AST. +const slackSource = join(dirname(require.resolve('@relayfile/relay-helpers/package.json')), 'dist/slack.d.ts'); +const source = ts.createSourceFile(slackSource, readFileSync(slackSource, 'utf8'), ts.ScriptTarget.Latest, true); +const slack = source.statements.find(node => ts.isInterfaceDeclaration(node) && node.name.text === 'SlackClient'); +assert(slack, 'Missing upstream SlackClient interface'); +assert.deepEqual(slack.members.map(member => member.name?.getText(source)), ['post', 'dm', 'reply', 'react']); const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, removeComments: true }); -const methods = client.members.map(member => { - assert(ts.isMethodSignature(member) && !member.typeParameters?.length, - 'Expected a non-generic Slack method'); - // Copy the parameter AST unchanged: no hand-maintained approximation of args. - const parameters = member.parameters.map(parameter => - printer.printNode(ts.EmitHint.Unspecified, parameter, source)).join(', '); +const methods = slack.members.map(member => { const name = member.name.getText(source); // The surface adds structured posts while the pinned adapter accepts text only. if (name === 'post') return ' post(...args: Parameters): ReturnType;'; - return ` ${name}(${parameters}): ReturnType;`.replaceAll('\n', '\n '); + const params = member.parameters.map(p => printer.printNode(ts.EmitHint.Unspecified, p, source)).join(', '); + return ` ${name}(${params}): ReturnType;`.replaceAll('\n', '\n '); }); // Pinned OpenAPI fragments fill the gap until relay-helpers exports message types. // Slack's schema deliberately leaves block contents and attachment fields open. @@ -49,20 +57,37 @@ function schemaType(schema) { } const messageTypes = Object.entries(messageSchema.schemas).map(([name, { schema }]) => `export type ${name} = ${schemaType(schema)};`).join('\n'); -const header = '// GENERATED by scripts/generate-helpers.mjs — do not edit.\n' - + '// Run `npm run gen --prefix packages/surface` from the repository root.\n'; -const files = { - 'slack.ts': `${header}\nimport type { SlackHelper as RuntimeSlackHelper } from "../slack.js";\n\n` - + '/** Generated from the pinned Slack OpenAPI fragments in scripts/slack-message-schema.json. */\n' - + `${messageTypes}\n\n` - + '/** Adapter argument shapes over the journal-backed Slack dispatcher. */\n' - + `export interface SlackHelper {\n${methods.join('\n')}\n}\n`, - 'index.ts': `${header}\nimport type { SlackHelper } from "./slack.js";\n\n` - + 'export type { SlackHelper } from "./slack.js";\n\n' - + '/** Supported helper namespaces composed into Ctx. */\n' - + 'export interface Helpers {\n slack: SlackHelper;\n}\n', -}; -const destination = resolve(values['out-dir']); -mkdirSync(destination, { recursive: true }); +files['slack.ts'] = `${header}\nimport type { SlackHelper as RuntimeSlackHelper } from "../slack.js";\n\n` + + '/** Generated from the pinned Slack OpenAPI fragments in scripts/slack-message-schema.json. */\n' + + `${messageTypes}\n\n` + + '/** Adapter argument shapes over the journal-backed Slack dispatcher. */\n' + + `export interface SlackHelper {\n${methods.join('\n')}\n}\n`; +const providers = [...names].sort(); +for (const provider of providers.filter(p => p !== 'slack')) { + const factory = `${camel(provider)}Client`; + const supported = catalog[provider] !== undefined; + const special = ['github', 'notion', 'stripe'].includes(provider); + files[`${provider}.ts`] = header + '\n' + + (special ? `import { ${factory} } from "../helper-clients.js";\n` + : typeof upstream[factory] === 'function' ? `import { ${factory} } from "@relayfile/relay-helpers";\n` + : supported ? `import { providerClient } from "@relayfile/relay-helpers";\nimport type { RelayClientOptions } from "@relayfile/relay-helpers/transport";\nconst ${factory} = (options: RelayClientOptions) => providerClient("${provider}", options);\n` : '') + + `import { bindHelper, type EffectDispatcher, type JournalHelper${!supported && !special ? ', type UnavailableHelper' : ''} } from "../effect-transport.js";\n\n` + + `export type ${title(provider)}Helper = ${supported || special ? `JournalHelper>` : 'UnavailableHelper'};\n\n` + + `export const create${title(provider)}Helper = (dispatch: EffectDispatcher): ${title(provider)}Helper =>\n` + + ` bindHelper(${JSON.stringify(provider)}, ${supported || special ? factory : 'undefined'}, dispatch);\n`; +} +files['index.ts'] = header + '\nimport type { SlackHelper } from "./slack.js";\n' + + providers.filter(p => p !== 'slack').map(p => `import { create${title(p)}Helper, type ${title(p)}Helper } from "./${p}.js";`).join('\n') + + '\nimport type { EffectDispatcher } from "../effect-transport.js";\n\n' + + providers.map(p => `export type { ${title(p)}Helper } from "./${p}.js";`).join('\n') + + '\n\nexport interface Helpers {\n' + providers.map(p => ` ${camel(p)}: ${title(p)}Helper;`).join('\n') + '\n}\n\n' + + 'export function createHelpers(dispatch: EffectDispatcher): Omit {\n return {\n' + + providers.filter(p => p !== 'slack').map(p => ` ${camel(p)}: create${title(p)}Helper(dispatch),`).join('\n') + '\n };\n}\n'; +files['clients.ts'] = header + '\nimport * as upstream from "@relayfile/relay-helpers";\nimport * as custom from "../helper-clients.js";\nimport type { HelperFactory } from "../effect-transport.js";\n\nexport const helperClients: Readonly> = {\n' + + providers.filter(p => p !== 'slack' && (p === 'stripe' || catalog[p])).map(p => ` "${p}": ${['github', 'notion', 'stripe'].includes(p) ? `custom.${camel(p)}Client` : typeof upstream[`${camel(p)}Client`] === 'function' ? `upstream.${camel(p)}Client` : `(options) => upstream.providerClient("${p}", options)`},`).join('\n') + '\n};\n'; +files['providers.ts'] = header + '\nexport const helperProviders = ' + JSON.stringify(providers.map(p => ({ + provider: p, namespace: camel(p), mockEnv: `RELAYFLOWS_${p.replaceAll('-', '_').toUpperCase()}_MOCK`, + supported: p === 'stripe' || catalog[p] !== undefined, +})), null, 2) + ' as const;\n'; for (const [name, content] of Object.entries(files)) writeFileSync(join(destination, name), content); -console.log(`Generated ${Object.keys(files).join(', ')}`); +console.log(`Generated ${providers.length} provider helpers (${providers.filter(p => !catalog[p] && p !== 'stripe').length} without upstream writeback clients)`); From 9b8cf5ab3f757243a4827d3c0308f370c425668e Mon Sep 17 00:00:00 2001 From: kjgbot Date: Sat, 12 Sep 2026 01:19:53 +0200 Subject: [PATCH 2/3] fix(cli): drop stale resumeSlackEffect ref; resumeHelperEffect subsumes it Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82 Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82 Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82 Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82 --- packages/sdk/src/cli/run.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/cli/run.ts b/packages/sdk/src/cli/run.ts index b842da90b..baabbf83e 100644 --- a/packages/sdk/src/cli/run.ts +++ b/packages/sdk/src/cli/run.ts @@ -151,9 +151,12 @@ export async function resumeFlow( if (connected !== undefined) return connected; try { - await resumeHelperEffect(client, runId, dataDir); + // resumeHelperEffect subsumes the old resumeSlackEffect: it handles the + // slack effect resume plus every other provider from N's codegen. The + // second call the earlier rebase left is a stale reference from before + // the helper fanout renamed the API. let outcome = await client.runResume(runId, options.allowHumanInfluenced); - if (await resumeSlackEffect(client, runId, dataDir)) { + if (await resumeHelperEffect(client, runId, dataDir)) { outcome = await client.runResume(runId, options.allowHumanInfluenced); } return await classifyOutcome(client, 'resume', outcome, base, socketPath, options); From 400785fcc0b886860193c013f9a1fedf3cea9ec3 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Sat, 12 Sep 2026 02:21:56 +0200 Subject: [PATCH 3/3] fix(sdk): forward Block Kit blocks+attachments through helper writeback Slice Y widened SlackPostMessage to carry blocks/attachments (via {text, blocks, attachments} or opts). Slice S's helper transport went through slackClient which only receives flattened text, so those fields were lost. Wrap the transport for slack.post to project the original call params onto the writeback body. Co-Authored-By: Claude Opus 4.7 (1M context) Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82 Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82 Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82 --- packages/sdk/src/slack-writeback.ts | 41 ++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/slack-writeback.ts b/packages/sdk/src/slack-writeback.ts index 58237f14f..308133a35 100644 --- a/packages/sdk/src/slack-writeback.ts +++ b/packages/sdk/src/slack-writeback.ts @@ -1,4 +1,5 @@ import { slackClient } from '@relayfile/relay-helpers'; +import type { RelayTransport } from '@relayfile/relay-helpers/transport'; import type { SlackHelper } from '@relayflows/surface'; import { helperTransport } from './helper-writeback.js'; export { atomicJson, receiptPath, readHelperReceipt as readSlackReceipt } from './helper-storage.js'; @@ -9,11 +10,49 @@ export type SlackCall = | { type: 'effect'; provider: 'slack'; verb: 'reply'; params: { channel: string; threadTs: string; text: string } } | { type: 'effect'; provider: 'slack'; verb: 'react'; params: { channel: string; messageTs: string; emoji: string } }; +interface SlackPostBodyExtras { text?: string; dropText: boolean; blocks?: unknown; attachments?: unknown } + +// Slice Y widened SlackPostMessage to carry blocks/attachments; the transport +// body is otherwise built by slackClient, which only sees the flattened text. +// Reconstruct the intended body by projecting from the original call.params. +function slackPostBodyExtras(params: Extract['params']): SlackPostBodyExtras { + const extras: SlackPostBodyExtras = { dropText: false }; + const t = params.text; + const opts = params.opts as { blocks?: unknown; attachments?: unknown } | undefined; + if (typeof t === 'object' && t !== null) { + if ('text' in t && typeof t.text === 'string') extras.text = t.text; + else extras.dropText = true; + if ('blocks' in t && t.blocks !== undefined) extras.blocks = t.blocks; + if ('attachments' in t && t.attachments !== undefined) extras.attachments = t.attachments; + } + if (opts?.blocks !== undefined) extras.blocks = opts.blocks; + if (opts?.attachments !== undefined) extras.attachments = opts.attachments; + return extras; +} + +function wrapSlackPostTransport(transport: RelayTransport, extras: SlackPostBodyExtras): RelayTransport { + return { + read: transport.read.bind(transport), + list: transport.list.bind(transport), + async write(request) { + const body = { ...request.body as Record }; + if (extras.dropText) delete body.text; + else if (extras.text !== undefined) body.text = extras.text; + if (extras.blocks !== undefined) body.blocks = extras.blocks; + if (extras.attachments !== undefined) body.attachments = extras.attachments; + return transport.write({ ...request, body }); + }, + }; +} + export async function slackWriteback( call: SlackCall, dataDir: string, runId: string, stepId: string, signal: AbortSignal, ): Promise { const { transport, deliveredRef } = helperTransport(call, dataDir, runId, stepId, signal); - const client = slackClient({ transport }); + const effectiveTransport = call.verb === 'post' + ? wrapSlackPostTransport(transport, slackPostBodyExtras(call.params)) + : transport; + const client = slackClient({ transport: effectiveTransport }); switch (call.verb) { case 'post': return client.post(call.params.channel, typeof call.params.text === 'string' ? call.params.text : call.params.text.text ?? '', call.params.opts);