diff --git a/docs/YAML-HELPERS.md b/docs/YAML-HELPERS.md new file mode 100644 index 000000000..7d1461008 --- /dev/null +++ b/docs/YAML-HELPERS.md @@ -0,0 +1,57 @@ +# YAML helper verbs + +Helpers compile into ordinary agent steps that execute journal-backed effects +through `@relayfile/relay-helpers`. Slack uses the same writeback path as +`f.slack`. No provider step type is added to the kernel. + +```yaml +version: 0.1.0 +steps: + - id: notify + slack: + post: + channel: "#test" + text: hi + - id: ticket + dependsOn: [notify] + linear: + createIssue: + teamId: engineering + title: Follow up +``` + +Each step has an `id` and exactly one provider containing exactly one verb. +Optional step fields are `dependsOn`, `maxIterations`, `verification`, and +`output`. Arguments are literal JSON-compatible data; templates and dynamic +argument bindings are not supported. Unknown fields, verbs, and malformed +arguments fail compilation. `YamlFlowSpec` and `YamlHelperStepSpec` describe +the authoring shapes; `compileSpec` returns the normalized `FlowSpec`. + +Supported verbs: + +| Provider | Verbs | +| --- | --- | +| Slack | `post`, `dm`, `reply`, `react` | +| GitHub | `comment`, `createIssue`, `createPullRequest`, `closePullRequest` | +| Linear | `comment`, `createIssue`, `updateIssue` | + +Single-object client arguments appear directly under the verb. Slack's +positional arguments become named fields (`channel`, `text`, `opts`, etc.). +GitHub `comment` takes `{target: {owner, repo, number}, body}`; Linear `comment` +takes `{issueId, body}`, and `updateIssue` takes `{issueId, args}`. + +Configure a relayfile mount containing the provider directory using the same +mount environment variables as TS Slack helpers (`RELAYFILE_MOUNT_PATH`, +`WORKSPACE_ROOT`, `WORKFORCE_SANDBOX_ROOT`, `RELAYFILE_MOUNT_ROOT`, or +`RELAYFILE_ROOT`). `flows check` refuses missing mounts. To run locally: + +```sh +flows run notify.yaml --local-agent +``` + +An attached SDK `AgentWorker` can also execute these steps; it must have a +`dataDir` for durable receipts. Completion output contains the effect call, +its stable idempotency key, and `receipt`. As with TS Slack, receipts are saved +before effect confirmation, allowing unfinished attempts to recover without +repeating a confirmed provider write. Keep the worker's data directory across +restarts. diff --git a/evidence/spec-V/sdk-tests.txt b/evidence/spec-V/sdk-tests.txt new file mode 100644 index 000000000..ded9a8c6a --- /dev/null +++ b/evidence/spec-V/sdk-tests.txt @@ -0,0 +1,839 @@ + +> @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] target(s) in 0.28s + +> @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-V/packages/sdk + +stdout | tests/live-kernel.test.ts +LIVE_KERNEL relayflowd=/tmp/relayflowd +LIVE_KERNEL flows=/Users/khaliqgant/fl-slice-V/packages/sdk/dist/cli.js + + ❯ tests/preflight.test.ts (27 tests | 1 failed) 36ms + × preflight: CLI resolution and refusal predicates > reaches every declared refusal kind, with the converse held by the type 16ms + → expected Set{ …(27) } to deeply equal Set{ 'invalid_spec', …(27) } + ❯ tests/observer-link.test.ts (39 tests | 7 failed) 62ms + × flows run: observer link integration > prints the Observer: line immediately after RUN when a workspace key is set 4ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-f02a364602e4.sock + × flows run: observer link integration > emits no observer line and no fetch when no workspace key is set 1ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-0fd217077a89.sock + × flows run: observer link integration > warns on stderr and proceeds normally when the mint API 500s 1ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-f5ca95d93192.sock + × flows run: observer link integration > suppresses the mint entirely when --no-observer-link is passed 1ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-35880eb0d8da.sock + × flows run: observer link integration > suppresses the mint entirely when FLOWS_NO_OBSERVER=1 is set 1ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-2b372dc5b4e3.sock + × flows run: observer link integration > emits RUN before the mint resolves and appends Observer on a later line 0ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-ddc7262a615d.sock + × flows run: observer link integration > folds observerUrl into the --json payload rather than printing a bare line 0ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-894520b4a7b6.sock + ✓ tests/daemon-lifecycle.test.ts (42 tests) 45ms + ❯ tests/authored-flow.test.ts (25 tests | 18 failed) 64ms + × authored flow journal executor > lowers an imported .flow.ts run and completion through the journal protocol 38ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > surfaces the closed journal reason when a lowered run fails 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > rechecks terminal state when a precreated lazy step first starts 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > refuses manually chained work even when it settles before the body returns 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > refuses forgotten work even when the body remains open long enough to settle it 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > retains root operation failures even when a derived rejection handler consumes them 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > captures a rejected derived callback instead of leaking unhandled success 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > passes direct input into the journal-backed authored body 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > evaluates journal output through JavaScript truthiness 1ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > evaluates journal output through JavaScript negation 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > evaluates journal output through JavaScript loose equality 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > evaluates journal output through JavaScript strict equality 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > evaluates journal output through JavaScript ternary 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > evaluates journal output through JavaScript logical and 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > evaluates journal output through JavaScript logical or 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > preserves separately awaited sibling ordering before the join 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > requires an explicit completion after journal-backed steps 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + × authored flow journal executor > refuses a body that completes nothing at all 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-b77eeeee.sock + ❯ tests/live-kernel.test.ts (31 tests | 31 failed) 3802ms + × built flows CLI against live relayflowd > twenty-six-step reuses 25 durable completions after editing the failed final step 28ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-a7922ae553a6.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-a7922ae553a6.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > runs rung (a), parks rung (b), and keeps JSON report-shaped 25ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-5f211e4b7884.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-5f211e4b7884.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > allows a deterministic run to exceed the bounded request timeout 26ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-4e40c9691c13.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-4e40c9691c13.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > follows a live worker dispatch through flows run 1099ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-e337d7475fe6.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-e337d7475fe6.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > cancels over the real socket and rejects the lease holder after closure 31ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-6c8324f6e561.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-6c8324f6e561.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > runs an agent CLI end to end through the SDK worker 28ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-5db529486a52.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-5db529486a52.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > f.agent lowers to a real agent step and dispatches through a live worker 25ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-a031accc6bf9.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-a031accc6bf9.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > f.agent's default flowPath anchors on cwd, not cwd's parent 25ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-ac6ed447a5ea.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-ac6ed447a5ea.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > can always get a parked run to a late-attaching worker 24ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-0334a5a33908.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-0334a5a33908.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > reports a real manual-recovery NeedsHuman state as parked 26ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-054cf39d74f4.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-054cf39d74f4.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > runs hn-monitor analyze-story end-to-end via a stub agent CLI (gate 2 clause 2 demo) 30ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-c84e2faf7b80.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-c84e2faf7b80.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > hn-monitor analyze-story FAILS verification when the CLI omits required schema fields 22ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-d70e13b85ebf.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-d70e13b85ebf.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > agent step preserves the CliResult wrapper as output when the CLI emits non-JSON text 21ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-1ba6ce36c405.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-1ba6ce36c405.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > AgentWorker exposes wake_context to the CLI via RELAYFLOW_WAKE_CONTEXT env var (real analyzer prerequisite) 46ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-fe2b8b48cba6.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-fe2b8b48cba6.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > AgentWorker leaves RELAYFLOW_WAKE_CONTEXT UNSET when the run has no wake_context (undefined-vs-null pin) 22ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-6db3e56d6799.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-6db3e56d6799.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > AgentWorker passes a declared model to an identified wrapper as RELAYFLOW_MODEL 23ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-b47864a7d830.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-b47864a7d830.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > AgentWorker refuses a nonconforming journal-submitted wrapper before exposing RELAYFLOW_MODEL 23ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-54a2023ab33d.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-54a2023ab33d.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > AgentWorker executes the raw claude adapter with its real model flag 28ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-148b0ea59c2d.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-148b0ea59c2d.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > AgentWorker executes the raw codex adapter with its real model flag 26ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-99c5020df27f.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-99c5020df27f.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > AgentWorker leaves RELAYFLOW_MODEL UNSET when the step declares no model 29ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-9aef8bc77f55.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-9aef8bc77f55.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI 1605ms + → LIVE_ANALYZER_UNAVAILABLE: "/Users/khaliqgant/fl-slice-V/testdata/preflight/analyze-story-claude-cli auth status" exited 1: analyze-story-claude-cli: "claude -p --model leaked-parent-model" exited 1: "leaked-parent-model" isn't described by this version's model catalog; update Claude Code, or map it with behavesAs on a modelPicker row (or modelOverrides, if it is a provider id of a model this version knows). Until then auto-compact keeps this session within 200k tokens (the context window it assumes); if the model accepts more, append [1m] to the model name for 1M, or set CLAUDE_CODE_MAX_CONTEXT_TOKENS to its real window; CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT=1 restores the previous wait-for-the-API behavior. +[claude-code:unrecognized_model] {"model":"leaked-parent-model","query_source":"sdk"} — failing because gate-2 acceptance requires the real analyzer to execute. Set RELAYFLOWS_ALLOW_ANALYZER_SKIP=1 only if this run is not gate evidence. + × built flows CLI against live relayflowd > preflights before journaling and names an unreachable socket 24ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-6f6922a0fc49.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-6f6922a0fc49.sock + +Caused by: + Operation not permitted (os error 1) + + × built flows CLI against live relayflowd > starts exactly one daemon when two runs race for one empty data dir 372ms + → 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 [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-live-race-dM7eSg/data/relayflowd.log": +Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-55bcf9621c4a.sock + +Caused by: + Operation not permitted (os error 1) +Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-55bcf9621c4a.sock + +Caused by: + Operation not permitted (os error 1) +[observer] token mint failed: network error: fetch failed; skipping observer link +: expected 2 to be +0 // Object.is equality + × JournalClient wire conformance against live relayflowd > exercises every protocol-v0 verb with the real server 22ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-894a8bc39eb2.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-894a8bc39eb2.sock + +Caused by: + Operation not permitted (os error 1) + + × surface resume after a real daemon kill > resumes a three-step run with each successful completion exactly once 25ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-12f64160eaf2.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-12f64160eaf2.sock + +Caused by: + Operation not permitted (os error 1) + + × a relayflow can be scheduled: tick source against live relayflowd > a tick spawns a real run whose step reports the SCHEDULED instant 23ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-3a06f2cbbe70.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-3a06f2cbbe70.sock + +Caused by: + Operation not permitted (os error 1) + + × a relayflow can be scheduled: tick source against live relayflowd > TWO ticks for ONE scheduled instant produce exactly ONE run 27ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-72c9ac807192.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-72c9ac807192.sock + +Caused by: + Operation not permitted (os error 1) + + × a relayflow can be scheduled: tick source against live relayflowd > a poller RESTART re-emitting a slot does not re-run it 23ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-c7c5bd7e994c.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-c7c5bd7e994c.sock + +Caused by: + Operation not permitted (os error 1) + + × a relayflow can be scheduled: tick source against live relayflowd > a MISSED interval is backfilled into its own run, not collapsed into the current one 23ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-7636e15bf2d9.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-7636e15bf2d9.sock + +Caused by: + Operation not permitted (os error 1) + + × a relayflow can be scheduled: tick source against live relayflowd > a tick for a DIFFERENT schedule id does not wake this flow 24ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-f764ed9419f1.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-f764ed9419f1.sock + +Caused by: + Operation not permitted (os error 1) + + × a relayflow can be scheduled: tick source against live relayflowd > journals the declared silence budget, so a dead schedule is not silently zero 25ms + → relayflowd exited before binding /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-aff50438179e.sock: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-aff50438179e.sock + +Caused by: + Operation not permitted (os error 1) + + ✓ tests/validate.test.ts (68 tests) 10ms + ❯ tests/cli.test.ts (64 tests | 11 failed) 4153ms + ✓ flows check CLI > binds a checked relative wrapper to the flow directory for worker execution 1430ms + ✓ flows check CLI > uses Codex login status and reports a rejected model as unavailable, not unauthenticated 383ms + ✓ flows check CLI > accepts an exact allowlisted named-agent model and probes that model 368ms + ✓ flows check CLI > passes all three canonical ladder flows and prints their resolved CLI 304ms + × flows run/resume CLI over the journal protocol > parses run options, submits the kernel dialect, and exits 0 on success 4ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-825497140ec4.sock + × flows run/resume CLI over the journal protocol > exits 1 and emits the declared completionReason for a failed run 1ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-9ce0c2ffc607.sock + × flows run/resume CLI over the journal protocol > exits 3 and names the parked llm step 0ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-957d899a3619.sock + × flows run/resume CLI over the journal protocol > reports a needs_human agent step as parked for human recovery 0ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-ca6a7f684971.sock + × flows run/resume CLI over the journal protocol > classifies a typed hello refusal as a protocol error, not an unreachable daemon 0ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-6e8ecab91d22.sock + × flows run/resume CLI over the journal protocol > follows a dispatched worker step instead of reporting a protocol error 0ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-408edd128286.sock + × flows run/resume CLI over the journal protocol > bounds a worker wait by its lease and reports what it is waiting for 0ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-c37fd1e3ca17.sock + × flows run/resume CLI over the journal protocol > allows a caller to cancel a worker-lease wait 0ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-494268b375ab.sock + × flows run/resume CLI over the journal protocol > resumes a parked run from snapshot step types without reading journal sequence one 0ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-3ad3744675c7.sock + × flows run/resume CLI over the journal protocol > refuses human-influenced resume with exit 2 and forwards the explicit override 0ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-a5c748711b57.sock + × flows run/resume CLI over the journal protocol > maps only run_not_found resumes to exit 2 0ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-5bc2424b667c.sock + ❯ tests/journal-client.test.ts (14 tests | 13 failed) 14ms + × JournalClient: protocol v0 over unix socket > handshakes with the matching protocol version 3ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-1d06f034.sock + × JournalClient: protocol v0 over unix socket > starts a run and receives the run_id (zero-agent spec is legal) 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-1d06f034.sock + × JournalClient: protocol v0 over unix socket > round-trips a spec straight from compileYaml through run.start in the kernel dialect 4ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-1d06f034.sock + × JournalClient: protocol v0 over unix socket > demultiplexes server-pushed events 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-1d06f034.sock + × JournalClient: protocol v0 over unix socket > reads the journal and streams 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-1d06f034.sock + × JournalClient: protocol v0 over unix socket > wires every out-of-band worker verb and receives dispatch events 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-1d06f034.sock + × JournalClient: protocol v0 over unix socket > wires run watch, resume/get, events, and replayable stream reads 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-1d06f034.sock + × JournalClient: protocol v0 over unix socket > cancels a run through the typed lifecycle surface 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-1d06f034.sock + × JournalClient: protocol v0 over unix socket > fails closed when the server returns an error 1ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-a071f0ca.sock + × JournalClient: protocol v0 over unix socket > does not apply the bounded request timeout to run lifecycle requests 1ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-30422c38.sock + × JournalClient: protocol v0 over unix socket > fails closed on connection drop (pending requests reject) 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-e47749e0.sock + × JournalClient: an effect election is atomic with its provider call > performs the effect exactly once when an attempt dies between electing and calling 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-0203db75.sock + × JournalClient: an effect election is atomic with its provider call > leaves an unconfirmed election reclaimable rather than suppressing the effect forever 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-60d8f692.sock + ✓ tests/tick-source.test.ts (33 tests) 11ms + ✓ tests/cloud-run.test.ts (47 tests) 92ms + ✓ tests/verb-field-lint.test.ts (78 tests) 187ms + ❯ tests/authored-flow-lifecycle-executor.test.ts (27 tests | 27 failed) 17ms + × authored flow lifecycle through the journal executor > refuses native resolver callbacks without starting terminal success 6ms + → expected Error: journal client: connect failed: co… to match object { code: 'unawaited_step' } + × authored flow lifecycle through the journal executor > refuses an ignored Promise.resolve operation without terminal success 1ms + → expected Error: journal client: connect failed: co… to match object { code: 'unawaited_step' } + × authored flow lifecycle through the journal executor > refuses an ignored Promise.all operation without terminal success 0ms + → expected Error: journal client: connect failed: co… to match object { code: 'unawaited_step' } + × authored flow lifecycle through the journal executor > retains a swallowed Promise.resolve callback failure before terminal success 0ms + → expected Error: journal client: connect failed: co… to match object { code: 'operation_callback_failed' } + × authored flow lifecycle through the journal executor > retains root failure through a swallowed Promise.resolve callback chain 1ms + → expected Error: journal client: connect failed: co… to match object { code: 'step_failed', …(1) } + × authored flow lifecycle through the journal executor > ignores forged Function.prototype.toString when refusing manual callbacks 0ms + → expected Error: journal client: connect failed: co… to match object { code: 'unawaited_step' } + × authored flow lifecycle through the journal executor > refuses a handled-and-forgotten derived failure deferred by ten microtask ticks 0ms + → expected Error: journal client: connect failed: co… to match object { code: 'unsettled_derived_work' } + × authored flow lifecycle through the journal executor > refuses a handled-and-forgotten derived failure deferred by setTimeout(0) 0ms + → expected Error: journal client: connect failed: co… to match object { code: 'unsettled_derived_work' } + × authored flow lifecycle through the journal executor > refuses terminal success while derived work is still in flight 0ms + → expected Error: journal client: connect failed: co… to match object { code: 'unsettled_derived_work' } + × authored flow lifecycle through the journal executor > preserves pre-constructed steps awaited in a loop 1ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-ac038052.sock + × authored flow lifecycle through the journal executor > preserves pre-constructed steps awaited out of order 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-ac038052.sock + × authored flow lifecycle through the journal executor > refuses a deferred derived failure behind an aggregate: allSettled resolved by an unrelated member 1ms + → expected Error: journal client: connect failed: co… to match object { code: StringMatching{…} } + × authored flow lifecycle through the journal executor > refuses a deferred derived failure behind an aggregate: allSettled with the step declared second 0ms + → expected Error: journal client: connect failed: co… to match object { code: StringMatching{…} } + × authored flow lifecycle through the journal executor > refuses a deferred derived failure behind an aggregate: all resolved by an unrelated member 0ms + → expected Error: journal client: connect failed: co… to match object { code: StringMatching{…} } + × authored flow lifecycle through the journal executor > refuses a deferred derived failure behind an aggregate: race resolved by an unrelated member 0ms + → expected Error: journal client: connect failed: co… to match object { code: StringMatching{…} } + × authored flow lifecycle through the journal executor > refuses a deferred derived failure behind an aggregate: any resolved by an unrelated member 0ms + → expected Error: journal client: connect failed: co… to match object { code: StringMatching{…} } + × authored flow lifecycle through the journal executor > refuses a deferred derived failure behind an aggregate: allSettled resolved by the step itself 0ms + → expected Error: journal client: connect failed: co… to match object { code: StringMatching{…} } + × authored flow lifecycle through the journal executor > refuses a deferred derived failure behind an aggregate: race resolved by the step itself 0ms + → expected Error: journal client: connect failed: co… to match object { code: StringMatching{…} } + × authored flow lifecycle through the journal executor > preserves authoring: await Promise.allSettled over two steps 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-ac038052.sock + × authored flow lifecycle through the journal executor > preserves authoring: await Promise.allSettled over five steps 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-ac038052.sock + × authored flow lifecycle through the journal executor > preserves authoring: await Promise.all over three steps 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-ac038052.sock + × authored flow lifecycle through the journal executor > preserves authoring: await Promise.race over two steps 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-ac038052.sock + × authored flow lifecycle through the journal executor > preserves authoring: await Promise.any over two steps 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-ac038052.sock + × authored flow lifecycle through the journal executor > preserves authoring: await an aggregate mixing a step and an unrelated promise 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-ac038052.sock + × authored flow lifecycle through the journal executor > refuses for-await over authored steps, and says so in the docs 0ms + → expected Error: journal client: connect failed: co… to match object { code: 'unawaited_step' } + × authored flow lifecycle through the journal executor > preserves ordinary awaited work after an authored step 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-ac038052.sock + × authored flow lifecycle through the journal executor > preserves direct await, Promise.resolve, and Promise.all authoring 0ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/rf-ac038052.sock + ✓ tests/cli-replay.test.ts (37 tests) 719ms + ✓ flows replay > --json is byte-identical across two CLI invocations (diff) 501ms + ✓ tests/tick-runner.test.ts (22 tests) 1308ms + ✓ tests/gate-contract.test.ts (20 tests) 72ms + ✓ tests/cli-hn-monitor.test.ts (16 tests) 95ms + ❯ tests/bundle.test.ts (21 tests | 1 failed) 3390ms + ✓ immutable bundles > builds and verifies the canonical YAML fixture through the compiled CLI 629ms + ✓ immutable bundles > emits the ephemeral warning on CLI stderr and uses the default output directory 425ms + ✓ immutable bundles > preserves quoted asset words and executable permissions 301ms + × immutable bundles > builds a standalone TS fixture twice with identical executable hashes 292ms + → expected 'REFUSED [bundle_invalid] package-lock…' to be '' // Object.is equality + ❯ tests/authored-flow-slack.test.ts (6 tests | 4 failed) 816ms + × authored Slack helper effects > journals exactly one effect with the authored params and typed receipt, without network 698ms + → Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-24df71c1bd86.sock + +Caused by: + Operation not permitted (os error 1) + + × authored Slack helper effects > replays after SIGKILL before confirm with the same token and one successful completion 26ms + → Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-0ecdd1ff74e4.sock + +Caused by: + Operation not permitted (os error 1) + + × authored Slack helper effects > replays after SIGKILL before complete with the same token and one successful completion 25ms + → Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-129d390d0db2.sock + +Caused by: + Operation not permitted (os error 1) + + × authored Slack helper effects > writes two files for two calls and supports dm, reply, and react 25ms + → Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-7f6561bbf623.sock + +Caused by: + Operation not permitted (os error 1) + + ✓ tests/stop-process-group.test.ts (6 tests) 6550ms + ✓ every stop reaches the process group, not just the direct child > exits the run after an execution-timeout stop 773ms + ✓ every stop reaches the process group, not just the direct child > exits the run after a protocol terminate stop 462ms + ✓ every stop reaches the process group, not just the direct child > kills a SIGTERM-deaf grandchild after a protocol terminate stop 1775ms + ✓ every stop reaches the process group, not just the direct child > kills a SIGTERM-deaf grandchild after an execution-timeout stop 2039ms + ✓ every stop reaches the process group, not just the direct child > holds the loop open long enough for the escalation to run 1169ms + ✓ every stop reaches the process group, not just the direct child > terminate() forces a group that outlives SIGTERM 332ms + ❯ tests/mcp.test.ts (30 tests | 7 skipped) 18614ms + ✓ MCP preflight and transports > flows check refuses an undeclared server with exit 2 and no daemon 307ms + ✓ MCP preflight and transports > flows check reports a refusing server and leaves no PID 466ms + ✓ MCP preflight and transports > kills a SIGTERM-resistant silent child after a parent-owned handshake deadline 1306ms + ✓ MCP preflight and transports > reaps a SIGTERM-resistant descendant with inherit stdio before cleanup finishes 1170ms + ✓ MCP preflight and transports > reaps a SIGTERM-resistant descendant with ignore stdio before cleanup finishes 2058ms + ✓ tests/backlog-picker.test.ts (14 tests) 63ms + ✓ tests/backlog-picker-flow.test.ts (6 tests) 575ms + ✓ tests/worker-cli.test.ts (13 tests) 20538ms + ✓ custom wrapper execution identity > refuses a wrapper symlink retarget before delivering private values 356ms + ✓ custom wrapper execution identity > bounds wrapper execution after acknowledgement 328ms + ✓ custom wrapper execution bounds are reader-owned > resolves when a conforming wrapper leaks a stdio pipe to a background helper 1753ms + ✓ custom wrapper execution bounds are reader-owned > resolves when the leaked helper inherits stderr only 1740ms + ✓ custom wrapper execution bounds are reader-owned > resolves when a wrapper leaks a stdio pipe and exits before identifying 3255ms + ✓ custom wrapper execution bounds are reader-owned > journals a completionReason at the default bound when a wrapper leaks a stdio pipe 11261ms + ✓ custom wrapper execution bounds are reader-owned > accepts the same over-8KiB payload whether or not it coalesces with the execute token 476ms + ✓ tests/authored-flow-operation.test.ts (23 tests) 263ms + ❯ tests/direct-input.test.ts (4 tests | 1 failed) 1366ms + × direct .flow.ts input through the built CLI and live runtime > executes inline and file JSON input through relayflowd 27ms + → relayflowd exited 1 + ✓ direct .flow.ts input through the built CLI and live runtime > refuses missing and malformed input before contacting relayflowd 877ms + ✓ tests/work-package-consumer.test.ts (13 tests) 181ms + ✓ tests/spec-parity.test.ts (31 tests) 183ms + ❯ tests/daemon-lifecycle-live.test.ts (9 tests | 7 failed) 15610ms + × 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 1217ms + → 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 [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-cold-start-4DDZXt/data/relayflowd.log": +stub relayflowd: bind failed: listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-146a37c5124f.sock +[observer] token mint failed: network error: fetch failed; skipping observer link +: expected 2 to be +0 // Object.is equality + × flows run against a data dir with no daemon (§6 test 7) > polls, bounded, for a daemon that holds the lock before it binds 1385ms + → 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 [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-slow-listen-yOETbs/data/relayflowd.log": +stub relayflowd: bind failed: listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-402d1e572957.sock +[observer] token mint failed: network error: fetch failed; skipping observer link +: expected 2 to be +0 // Object.is equality + × flows run against a data dir with no daemon (§6 test 7) > attaches to a serving daemon that has not published a connection file 550ms + → 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 [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-no-index-QmvYN4/data/relayflowd.log": +stub relayflowd: bind failed: listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-6b96fbbe50ae.sock +[observer] token mint failed: network error: fetch failed; skipping observer link +: expected 2 to be +0 // Object.is equality + × 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 490ms + → expected 2 to be +0 // Object.is equality + × 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 562ms + → expected 2 to be +0 // Object.is equality + × concurrent invocations against one empty data dir (§6 test 15) > ends with exactly one daemon owning the socket, and both runs succeed 10240ms + → 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 [daemon_start_timeout] relayflowd did not start serving "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-72795d072983.sock" within 10000ms. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-race-HTfKDp/data/relayflowd.log": +stub relayflowd: another relayflowd is already serving /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-race-HTfKDp/data +stub relayflowd: bind failed: listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-72795d072983.sock +[observer] token mint failed: network error: fetch failed; skipping observer link +: expected 2 to be +0 // Object.is equality + ✓ refusals from a spawn that cannot produce a daemon > names daemon_start_failed and quotes the daemon log when startup dies 494ms + × refusals from a spawn that cannot produce a daemon > refuses a daemon speaking another protocol version instead of binding over it 428ms + → expected 'WARNING [unprovable_effects] Step "gr…' to contain 'REFUSED [daemon_protocol_mismatch]' + ❯ tests/pty-sidechannel.test.ts (11 tests | 6 failed) 4255ms + × view attach preserves worker completion and marks only drive 635ms + → expected '' to contain 'visible' + × drive attach preserves worker completion and marks only drive 631ms + → expected undefined to be true // Object.is equality + × passthrough attach preserves worker completion and marks only drive 696ms + → expected '' to contain 'visible' + ✓ none attach preserves worker completion and marks only drive 614ms + × broken handshake and occupied socket do not affect the sidechannel owner 2ms + → expected undefined to be defined + × rejects drive after EOF without marking human intervention 520ms + → expected false to be true // Object.is equality + × delivers all drive bytes in order across child stdin backpressure 497ms + → expected '' to be '4194304:4f1f8b605b8fd4ba8b9a16234a54d…' // Object.is equality + ❯ tests/plugin-loader.test.ts (9 tests | 1 failed) 2227ms + × journals plugin effect input and receipt through the daemon 2185ms + → journal client: connect failed: connect ENOENT /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-f429c50f03fe.sock + ✓ tests/worker-lease.test.ts (7 tests) 5ms + ✓ tests/yaml-helpers.test.ts (33 tests) 34ms + ✓ tests/typed-output.test.ts (14 tests) 97ms + ✓ tests/json-schema-bound.test.ts (71 tests) 1544ms + ✓ JSON Schema termination bound > walks a deep schema with an explicit stack rather than recursion 1314ms + ❯ tests/effect-channel.test.ts (5 tests | 4 failed) 10244ms + × journals one post across interruption before none 2201ms + → daemon startup failed: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-8d2762eca5f7.sock + +Caused by: + Operation not permitted (os error 1) + + × journals one post across interruption before record 2174ms + → daemon startup failed: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-a038ba8179c7.sock + +Caused by: + Operation not permitted (os error 1) + + × journals one post across interruption before confirm 2142ms + → daemon startup failed: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-34b7c2d71b64.sock + +Caused by: + Operation not permitted (os error 1) + + × journals one post across interruption before complete 3725ms + → daemon startup failed: Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-22dda1a0e2e3.sock + +Caused by: + Operation not permitted (os error 1) + + ✓ tests/mcp-lifecycle.test.ts (4 tests) 10ms + ✓ tests/model-selection.test.ts (10 tests) 12ms + ✓ tests/f-memory.test.ts (7 tests) 1047ms + ✓ tests/relayflowd-path.test.ts (10 tests) 3ms + ❯ tests/local-agent-live.test.ts (5 tests | 5 failed) 2099ms + × built CLI local agent against a real daemon > dispatches through the wrapper and keeps --json stdout report-shaped 560ms + → [observer] token mint failed: network error: fetch failed; skipping observer link +REFUSED [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-local-agent-kxXZch/data/relayflowd.log": +Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-1de00edb785d.sock + +Caused by: + Operation not permitted (os error 1) +{"ok":false,"command":"run","resolutions":[],"diagnostics":[{"severity":"refusal","kind":"daemon_start_failed","message":"relayflowd exited 1 during startup. Last output in \"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-local-agent-kxXZch/data/relayflowd.log\":\nError: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-1de00edb785d.sock\n\nCaused by:\n Operation not permitted (os error 1)"}],"path":"hello.flow.ts","socketPath":"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-1de00edb785d.sock"} +: expected 2 to be +0 // Object.is equality + × built CLI local agent against a real daemon > runs beyond the initial 30-second lease without a second invocation 362ms + → [observer] token mint failed: network error: fetch failed; skipping observer link +REFUSED [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-local-agent-FCi81v/data/relayflowd.log": +Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-72823645522d.sock + +Caused by: + Operation not permitted (os error 1) +{"ok":false,"command":"run","resolutions":[],"diagnostics":[{"severity":"refusal","kind":"daemon_start_failed","message":"relayflowd exited 1 during startup. Last output in \"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-local-agent-FCi81v/data/relayflowd.log\":\nError: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-72823645522d.sock\n\nCaused by:\n Operation not permitted (os error 1)"}],"path":"hello.flow.ts","socketPath":"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-72823645522d.sock"} +: expected 2 to be +0 // Object.is equality + × built CLI local agent against a real daemon > renders actual agent completion in text output 416ms + → REFUSED [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-local-agent-t9Ov7h/data/relayflowd.log": +Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-54e9b36d19b7.sock + +Caused by: + Operation not permitted (os error 1) +[observer] token mint failed: network error: fetch failed; skipping observer link +: expected 2 to be +0 // Object.is equality + × built CLI local agent against a real daemon > returns a failed run when the agent process fails 366ms + → REFUSED [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-local-agent-qomOoc/data/relayflowd.log": +Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-de5f741d0aa7.sock + +Caused by: + Operation not permitted (os error 1) +[observer] token mint failed: network error: fetch failed; skipping observer link +: expected 2 to be 1 // Object.is equality + × built CLI local agent against a real daemon > refuses a workspace it cannot pin before invoking the agent 394ms + → expected 'REFUSED [daemon_start_failed] relayfl…' to contain 'stream-only' + ❯ tests/webhook.test.ts (6 tests | 3 failed) 71ms + × webhook ingress > accepts JSON through atomic files without creating a daemon 8ms + → listen EPERM: operation not permitted 127.0.0.1 + × webhook ingress > rejects malformed, oversized, traversal, and non-POST requests 2ms + → listen EPERM: operation not permitted 127.0.0.1 + × webhook ingress > fails closed on a symlink inbox target 5ms + → listen EPERM: operation not permitted 127.0.0.1 + ✓ tests/authored-plugin-effect.test.ts (6 tests) 47ms + ✓ tests/deploy.test.ts (9 tests) 3001ms + ✓ flows deploy file buckets > publishes the full signed layout byte-for-byte and redeploys as a noop 797ms + ✓ flows deploy file buckets > refuses a missing local bundle before creating the bucket 300ms + ✓ flows deploy file buckets > never labels a corrupt existing deployment as a noop 499ms + ❯ tests/yaml-local-agent-live.test.ts (7 tests | 7 failed) 5091ms + × YAML --local-agent through the built CLI and real daemon > runs with the checked step CLI and model and journals done 1393ms + → 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 [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-NHcQgf/data/relayflowd.log": +Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-f02e381247b8.sock + +Caused by: + Operation not permitted (os error 1) +{"ok":false,"command":"run","path":"flows/hello.flow.yaml","projectConfigPath":"/private/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-NHcQgf/flows.json","resolutions":[{"stepId":"greet","cli":"../agent.mjs","source":"step","model":"yaml-test-model"}],"diagnostics":[{"severity":"warning","kind":"editor_schema_missing","message":"For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json"},{"severity":"refusal","kind":"daemon_start_failed","message":"relayflowd exited 1 during startup. Last output in \"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-NHcQgf/data/relayflowd.log\":\nError: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-f02e381247b8.sock\n\nCaused by:\n Operation not permitted (os error 1)"}],"socketPath":"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-f02e381247b8.sock"} +: expected 2 to be +0 // Object.is equality + × YAML --local-agent through the built CLI and real daemon > runs with the checked named CLI and model and journals done 486ms + → 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 [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-NfFaig/data/relayflowd.log": +Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-5a2ef78a8832.sock + +Caused by: + Operation not permitted (os error 1) +{"ok":false,"command":"run","path":"flows/hello.flow.yaml","projectConfigPath":"/private/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-NfFaig/flows.json","resolutions":[{"stepId":"greet","cli":"../agent.mjs","source":"named","model":"yaml-test-model"}],"diagnostics":[{"severity":"warning","kind":"editor_schema_missing","message":"For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json"},{"severity":"refusal","kind":"daemon_start_failed","message":"relayflowd exited 1 during startup. Last output in \"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-NfFaig/data/relayflowd.log\":\nError: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-5a2ef78a8832.sock\n\nCaused by:\n Operation not permitted (os error 1)"}],"socketPath":"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-5a2ef78a8832.sock"} +: expected 2 to be +0 // Object.is equality + × YAML --local-agent through the built CLI and real daemon > runs with the checked flow CLI and model and journals done 600ms + → 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 [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-kbqdc5/data/relayflowd.log": +Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-e4c83769af74.sock + +Caused by: + Operation not permitted (os error 1) +{"ok":false,"command":"run","path":"flows/hello.flow.yaml","projectConfigPath":"/private/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-kbqdc5/flows.json","resolutions":[{"stepId":"greet","cli":"../agent.mjs","source":"flow","model":"yaml-test-model"}],"diagnostics":[{"severity":"warning","kind":"editor_schema_missing","message":"For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json"},{"severity":"refusal","kind":"daemon_start_failed","message":"relayflowd exited 1 during startup. Last output in \"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-kbqdc5/data/relayflowd.log\":\nError: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-e4c83769af74.sock\n\nCaused by:\n Operation not permitted (os error 1)"}],"socketPath":"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-e4c83769af74.sock"} +: expected 2 to be +0 // Object.is equality + × YAML --local-agent through the built CLI and real daemon > runs with the checked project CLI and model and journals done 572ms + → 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 [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-0mnfaE/data/relayflowd.log": +Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-ab5f63d92b00.sock + +Caused by: + Operation not permitted (os error 1) +{"ok":false,"command":"run","path":"flows/hello.flow.yaml","projectConfigPath":"/private/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-0mnfaE/flows.json","resolutions":[{"stepId":"greet","cli":"./agent.mjs","source":"project","model":"yaml-test-model"}],"diagnostics":[{"severity":"warning","kind":"editor_schema_missing","message":"For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json"},{"severity":"refusal","kind":"daemon_start_failed","message":"relayflowd exited 1 during startup. Last output in \"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-0mnfaE/data/relayflowd.log\":\nError: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-ab5f63d92b00.sock\n\nCaused by:\n Operation not permitted (os error 1)"}],"socketPath":"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-ab5f63d92b00.sock"} +: expected 2 to be +0 // Object.is equality + × YAML --local-agent through the built CLI and real daemon > still parks without --local-agent 756ms + → 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 [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-pqVgLK/data/relayflowd.log": +Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-6e27ecb877f7.sock + +Caused by: + Operation not permitted (os error 1) +{"ok":false,"command":"run","path":"flows/hello.flow.yaml","projectConfigPath":"/private/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-pqVgLK/flows.json","resolutions":[{"stepId":"greet","cli":"../agent.mjs","source":"step","model":"yaml-test-model"}],"diagnostics":[{"severity":"warning","kind":"editor_schema_missing","message":"For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json"},{"severity":"refusal","kind":"daemon_start_failed","message":"relayflowd exited 1 during startup. Last output in \"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-pqVgLK/data/relayflowd.log\":\nError: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-6e27ecb877f7.sock\n\nCaused by:\n Operation not permitted (os error 1)"}],"socketPath":"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-6e27ecb877f7.sock"} +: expected 2 to be 3 // Object.is equality + × YAML --local-agent through the built CLI and real daemon > reports the agent process failure 638ms + → 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 [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-Lm0tUQ/data/relayflowd.log": +Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-480ed1a76276.sock + +Caused by: + Operation not permitted (os error 1) +{"ok":false,"command":"run","path":"flows/hello.flow.yaml","projectConfigPath":"/private/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-Lm0tUQ/flows.json","resolutions":[{"stepId":"greet","cli":"../agent.mjs","source":"step","model":"yaml-test-model"}],"diagnostics":[{"severity":"warning","kind":"editor_schema_missing","message":"For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json"},{"severity":"refusal","kind":"daemon_start_failed","message":"relayflowd exited 1 during startup. Last output in \"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-Lm0tUQ/data/relayflowd.log\":\nError: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-480ed1a76276.sock\n\nCaused by:\n Operation not permitted (os error 1)"}],"socketPath":"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-480ed1a76276.sock"} +: expected 2 to be 1 // Object.is equality + × YAML --local-agent through the built CLI and real daemon > preserves declared workspace surfaces that the local worker cannot pin 645ms + → 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 [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-k9qCCp/data/relayflowd.log": +Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-8611097ec8af.sock + +Caused by: + Operation not permitted (os error 1) +{"ok":false,"command":"run","path":"flows/hello.flow.yaml","projectConfigPath":"/private/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-k9qCCp/flows.json","resolutions":[{"stepId":"greet","cli":"../agent.mjs","source":"step","model":"yaml-test-model"}],"diagnostics":[{"severity":"warning","kind":"editor_schema_missing","message":"For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json"},{"severity":"refusal","kind":"daemon_start_failed","message":"relayflowd exited 1 during startup. Last output in \"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-yaml-local-k9qCCp/data/relayflowd.log\":\nError: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-8611097ec8af.sock\n\nCaused by:\n Operation not permitted (os error 1)"}],"socketPath":"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-8611097ec8af.sock"} +: expected 2 to be 3 // Object.is equality + ✓ tests/local-dev-ux.test.ts (8 tests) 258ms + ❯ tests/webhook-live.test.ts (3 tests | 3 failed) 30259ms + × flows serve-webhook writes JSON before the daemon starts, then journals and archives exactly once 10026ms + → webhook integration timed out: FAILED [webhook_server] listen EPERM: operation not permitted 127.0.0.1 + + × replays a dropped file after SIGKILL before spawn 10193ms + → webhook integration timed out: FAILED [webhook_server] listen EPERM: operation not permitted 127.0.0.1 + + × resumes the same journal after SIGKILL after spawn and before acknowledgement 10039ms + → webhook integration timed out: FAILED [webhook_server] listen EPERM: operation not permitted 127.0.0.1 + + ✓ tests/dependency-validation.test.ts (6 tests) 823ms + ✓ dependency validation > accepts a valid 10,000-step reverse chain through every direct public boundary 352ms + ✓ dependency validation > bounds the author-facing path for a 10,000-step cycle 451ms + ✓ tests/input-binding.test.ts (12 tests) 295ms + ✓ tests/deterministic-llm.test.ts (5 tests) 105ms + ✓ tests/scope-compiler.test.ts (25 tests) 18ms + ✓ tests/bin.test.ts (7 tests) 8914ms + ✓ built flows binary > refuses through a symlink to the built artifact 5146ms + ✓ built flows binary > refuses through a symlinked directory component 654ms + ✓ built flows binary > classifies a signal-terminated auth probe as probe_failed 994ms + ✓ built flows binary > classifies an unavailable PATH resolver as probe_failed 698ms + ✓ built flows binary > does not describe a present non-executable CLI as missing 713ms + ✓ built flows binary > runs one auth probe for three steps sharing a flow CLI 706ms + ✓ tests/budget-attribution.test.ts (4 tests) 6ms +(node:24470) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1) +(Use `node --trace-warnings ...` to show where the warning was created) + ❯ tests/cli-watch.test.ts (10 tests | 9 failed) 49312ms + × flows check --watch > rechecks syntax errors, clears once, and returns the last refusal on Ctrl-C 5005ms + → 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 5006ms + → 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 5002ms + → 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 6492ms + → 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 5150ms + → 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 5015ms + → 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 5132ms + → 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 7407ms + → 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 > queues changes during a slow check without overlapping checks 5052ms + → 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". + ✓ tests/hn-poller.test.ts (6 tests) 19ms + ✓ tests/dir-watcher-poller.test.ts (6 tests) 3ms + ❯ tests/flow-executor-chain.test.ts (12 tests | 12 failed) 58944ms + × flow executor LLM and output-binding chain > runs f.llm -> f.agent -> f.run with schema-verified journal output and the exact allowed model 5014ms + → 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". + × flow executor LLM and output-binding chain > fails invalid LLM output before the next step: not JSON 5004ms + → 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". + × flow executor LLM and output-binding chain > fails invalid LLM output before the next step: {"message":7} 5003ms + → 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". + × flow executor LLM and output-binding chain > retains tagged-template text output 5003ms + → 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". + × flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: null 5004ms + → 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". + × flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: [1,2] 5107ms + → 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". + × flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: "hello" 5153ms + → 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". + × flow executor LLM and output-binding chain > refuses a model outside flows.json before a run or adapter execution 5013ms + → 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". + × flow executor LLM and output-binding chain > runs the authored LLM path through the built flows CLI 5126ms + → 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". + × flow executor LLM and output-binding chain > passes a declarative verified value through an agent into a deterministic artifact 7412ms + → 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". + × flow executor LLM and output-binding chain > journals a missing optional field as a failure before the consuming command executes 5030ms + → 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". + × flow executor LLM and output-binding chain > flows run consumes YAML bindings and resume reuses the original journal output 1072ms + → [observer] token mint failed: network error: fetch failed; skipping observer link +REFUSED [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-chain-40sloh/data/relayflowd.log": +Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-2db2a1febb70.sock + +Caused by: + Operation not permitted (os error 1) +: expected 2 to be +0 // Object.is equality + ✓ tests/hello-deterministic.test.ts (5 tests) 9ms + ✓ tests/yaml-helper-effect.test.ts (4 tests) 679ms + ❯ tests/run-from-digest.test.ts (6 tests | 1 failed) 11371ms + × flows run digest input > submits the sealed canonical spec through the normal journal path without checkout 3448ms + → listen EPERM: operation not permitted /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-969a1c94e98e.sock + ✓ flows run digest input > uses a verified cache hit even after the bucket is removed 589ms + ✓ flows run digest input > resolves deploy.bucket from flows.json and honors explicit override 2334ms + ✓ flows run digest input > refuses an unconfigured bucket 1285ms + ✓ flows run digest input > refuses tampered spec.canonical.json before creating run data 1465ms + ✓ flows run digest input > refuses tampered identity.json before creating run data 2246ms + ✓ tests/budget-preflight.test.ts (16 tests) 14ms + ✓ tests/work-package-validator.test.ts (7 tests) 7ms + ❯ tests/yaml-helper-live.test.ts (1 test | 1 failed) 2188ms + × runs compiled YAML helpers through the built CLI and kernel effect journal 2188ms + → {"ok":false,"command":"run","path":"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/yaml-helper-live-K7vnCQ/notify.yaml","resolutions":[],"diagnostics":[{"severity":"warning","kind":"unprovable_effects","stepId":"before","message":"Step \"before\" command \"true\" resolves, but its effects cannot be proven before execution."},{"severity":"warning","kind":"unprovable_effects","stepId":"after","message":"Step \"after\" command \"true\" resolves, but its effects cannot be proven before execution."},{"severity":"refusal","kind":"daemon_start_failed","message":"relayflowd exited 1 during startup. Last output in \"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/yaml-helper-live-K7vnCQ/data/relayflowd.log\":\nError: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-d641c6113fd4.sock\n\nCaused by:\n Operation not permitted (os error 1)"}],"socketPath":"/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-d641c6113fd4.sock"} +WARNING [unprovable_effects] Step "before" command "true" resolves, but its effects cannot be proven before execution. +WARNING [unprovable_effects] Step "after" command "true" resolves, but its effects cannot be proven before execution. +REFUSED [daemon_start_failed] relayflowd exited 1 during startup. Last output in "/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/yaml-helper-live-K7vnCQ/data/relayflowd.log": +Error: bind socket /var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/relayflowd-d641c6113fd4.sock + +Caused by: + Operation not permitted (os error 1) +: expected 2 to be +0 // Object.is equality + ✓ tests/plugin-add.test.ts (7 tests) 4511ms + ✓ installs a real offline npm fixture and includes declarations 2518ms + ✓ typechecks the augmented verb and rejects unknown namespaces 1751ms + ✓ tests/model-pricing.test.ts (8 tests) 8ms + ✓ tests/classify-outcome.test.ts (2 tests) 2309ms + ✓ classifyOutcome > gives up and reports when a running run never becomes classifiable 2143ms + ↓ tests/real-cli-adapters.test.ts (3 tests | 3 skipped) + ❯ tests/authored-use-loader.test.ts (0 test) + ❯ tests/memoization.test.ts (0 test) + ❯ tests/parse-json-output.test.ts (0 test) + ❯ tests/cli-adapter.test.ts (0 test) + ❯ tests/bundle-preflight.test.ts (4 tests | 3 failed) 2853ms + ✓ bundle execution preflight > ignores surrounding cache configuration on a verified cache hit 1841ms + × bundle execution preflight > uses the built alias for a nameless flow even in a digest-only cache directory 948ms + → expected 2 to be +0 // Object.is equality + × bundle execution preflight > still rejects a named spec requested under another alias 62ms + → ENOSPC: no space left on device, open '/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-deploy-BDvujg/dist/flows/.build-DWlxuE/metadata.json' + × bundle execution preflight > still checks command availability without project configuration 1ms + → ENOSPC: no space left on device, mkdtemp '/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/flows-deploy-O7Tll2' + ❯ tests/journal-client-completion.test.ts (0 test) + ❯ tests/worker-cli-abort.test.ts (0 test) + ❯ tests/direct-run-failure.test.ts (0 test) + ❯ tests/budget-authored-live.test.ts (0 test) + ❯ tests/slack-writeback.test.ts (0 test) + ❯ tests/bundle-transport.test.ts (0 test) + ❯ tests/cli-progress-wait.test.ts (0 test) + ❯ tests/run-digest-live.test.ts (0 test) + ❯ tests/placement.test.ts (0 test) + ❯ tests/run-digest.test.ts (0 test) + ❯ tests/memory.test.ts (0 test) + ❯ tests/worker-platform.test.ts (0 test) + +⎯⎯⎯⎯⎯⎯ Failed Suites 18 ⎯⎯⎯⎯⎯⎯ + + FAIL tests/authored-use-loader.test.ts [ tests/authored-use-loader.test.ts ] +Error: ENOSPC: no space left on device, open '/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/CEIJNJJF6kZvWoAmgwjUJ/ssr/30f50a4f0bd903ae5628051a07953a46ba487415' +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/194]⎯ + + FAIL tests/budget-authored-live.test.ts [ tests/budget-authored-live.test.ts ] +Error: ENOSPC: no space left on device, open '/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/CEIJNJJF6kZvWoAmgwjUJ/ssr/b9d631f1254be129f56bcdc0ec2ea9f8a8b2af2d' +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/194]⎯ + + FAIL tests/bundle-transport.test.ts [ tests/bundle-transport.test.ts ] +Error: ENOSPC: no space left on device, open '/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/CEIJNJJF6kZvWoAmgwjUJ/ssr/0277ae37872dd56683e816b137746512f0410f96' +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/194]⎯ + + FAIL tests/cli-adapter.test.ts [ tests/cli-adapter.test.ts ] +Error: ENOSPC: no space left on device, open '/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/CEIJNJJF6kZvWoAmgwjUJ/ssr/8365134012f2b02772f04d675c5b3b9fb8b83ea4' +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/194]⎯ + + FAIL tests/cli-progress-wait.test.ts [ tests/cli-progress-wait.test.ts ] +Error: ENOSPC: no space left on device, open '/var/folders/_z/f_fpl8j533g_r63706k2xvp00000gn/T/CEIJNJJF6kZvWoAmgwjUJ/ssr/05b6a45a28d11295d61113caf2326a580e7da34d' +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/194]⎯ + + +Node.js v25.8.1 diff --git a/evidence/spec-V/test-typecheck.txt b/evidence/spec-V/test-typecheck.txt new file mode 100644 index 000000000..e69de29bb diff --git a/evidence/spec-V/typecheck.txt b/evidence/spec-V/typecheck.txt new file mode 100644 index 000000000..326089087 --- /dev/null +++ b/evidence/spec-V/typecheck.txt @@ -0,0 +1,4 @@ + +> @relayflows/sdk@2.0.8 typecheck +> tsc --noEmit && tsc -p tsconfig.type-tests.json + diff --git a/packages/sdk/src/authored-slack-effect.ts b/packages/sdk/src/authored-slack-effect.ts index 992fbc6d6..0c4fd0e76 100644 --- a/packages/sdk/src/authored-slack-effect.ts +++ b/packages/sdk/src/authored-slack-effect.ts @@ -8,9 +8,9 @@ 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 { completeHelperDispatch } from './yaml-helper-effect.js'; import { checkSlackHelpers } from './slack-preflight.js'; -import { atomicJson, readSlackReceipt, receiptPath, slackWriteback, type SlackCall } from './slack-writeback.js'; +import { atomicJson, type SlackCall } from './slack-writeback.js'; export function assertSlackCredentials(): void { const report = checkSlackHelpers({ header: { tools: { slack: true } }, body() {} }); @@ -75,7 +75,7 @@ async function driveSlackEffect( client.on('step.dispatch', (dispatch: StepDispatchEvent) => { if (executing || dispatch.run_id !== runId || dispatch.step_id !== step.id) return; executing = true; - void completeSlackDispatch(client, dispatch, call, dataDir).then(resolve, reject); + void completeHelperDispatch(client, dispatch, call, dataDir).then(resolve, reject); }); client.on('error', reject); try { @@ -93,29 +93,3 @@ async function driveSlackEffect( throw new AuthoredFlowExecutionError('step_failed', error instanceof Error ? error.message : 'Slack effect failed', 'worker_error', runId); } finally { client.close(); } } - -async function completeSlackDispatch(client: JournalClient, dispatch: StepDispatchEvent, call: SlackCall, dataDir: string): Promise { - const 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: '/slack', - 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 slackWriteback(call, 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 }; - }); - 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: '/slack', idempotency_key: dispatch.idempotency_key }] }); -} diff --git a/packages/sdk/src/cli/check.ts b/packages/sdk/src/cli/check.ts index 9c1f8d9eb..0228710ee 100644 --- a/packages/sdk/src/cli/check.ts +++ b/packages/sdk/src/cli/check.ts @@ -3,6 +3,7 @@ import { dirname, isAbsolute, join, parse as parsePath, resolve } from 'node:pat import { spawnSync } from 'node:child_process'; import { parse as parseYaml } from 'yaml'; import { CompileError, compileSpec, kernelToAuthoring } from '../compile.js'; +import { helperReady } from '../yaml-helper-effect.js'; import { adapterIdentification, authenticationProbe, @@ -270,6 +271,7 @@ function findConfig(start: string): string | undefined { function systemProbes(flowDirectory: string, config: ProjectConfig): PreflightProbes { return { + helper: helperReady, cli: (cli, source, model) => probeCli(cli, source === 'project' ? config.directory : flowDirectory, model), executor: (trigger) => config.executors.includes(trigger.executor), command: (binary) => executableExists(binary, flowDirectory), diff --git a/packages/sdk/src/compile.ts b/packages/sdk/src/compile.ts index 8f0710169..cc1eb1739 100644 --- a/packages/sdk/src/compile.ts +++ b/packages/sdk/src/compile.ts @@ -40,6 +40,7 @@ import { canonicalize, specHash } from './canonical.js'; import { validateOutputDeclaration } from './output-schema.js'; import { validateSpec, type ValidationResult } from './validate.js'; import { snapshotJsonValue } from './json-value.js'; +import { expandYamlHelpers } from './yaml-helpers.js'; export class CompileError extends Error { readonly errors: string[]; @@ -122,6 +123,7 @@ export function compileSpec(spec: unknown): CompiledFlowSpec { let snapshot: unknown; try { snapshot = snapshotJsonValue(spec, 'spec'); + snapshot = expandYamlHelpers(snapshot); } catch (error) { throw new CompileError([ error instanceof Error ? error.message : 'spec: expected JSON-compatible data', diff --git a/packages/sdk/src/failure-kinds.ts b/packages/sdk/src/failure-kinds.ts index 92696ce84..a2be476a4 100644 --- a/packages/sdk/src/failure-kinds.ts +++ b/packages/sdk/src/failure-kinds.ts @@ -6,6 +6,7 @@ const PREFLIGHT_ENVIRONMENT_FAILURE_KINDS = [ ...PLUGIN_FAILURE_KINDS, 'helper_slack.credential_missing', 'helper_slack.mount_required', + 'helper_mount_required', 'mcp_undeclared_server', 'mcp_unreachable', 'budget_syntax_invalid', diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 80caf6a60..603844374 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -12,6 +12,9 @@ export type { DeterministicStepSpec, ExitCodeGate, FlowSpec, + YamlFlowSpec, + YamlHelperStepSpec, + YamlHelperParams, FlowsJson, McpServerConfig, JsonSchemaGate, diff --git a/packages/sdk/src/preflight.ts b/packages/sdk/src/preflight.ts index 7400e6b21..69c67bf64 100644 --- a/packages/sdk/src/preflight.ts +++ b/packages/sdk/src/preflight.ts @@ -7,6 +7,7 @@ import { budgetDiagnostics } from './budget-preflight.js'; import type { TriggerSource } from '@relayflows/surface'; import { acceptsAnyOutput, inspectStepGate, type StepGateInspection } from './gate-contract.js'; import { compileSpec, CompileError } from './compile.js'; +import { helperCall } from './yaml-helpers.js'; import type { PreflightFailureKind, PreflightWarningKind, @@ -54,6 +55,8 @@ type CliProbeOutcome = * emits `probe_failed` (or `command_unprovable` for a deterministic command). */ export interface PreflightProbes { + /** Whether the provider's relayfile mount is available to the helper worker. */ + helper?(provider: string): boolean; /** * Resolve relative paths against the file implied by `source`, then probe * `auth status`. When the step declared a `model`, the probe runs with that @@ -216,6 +219,7 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul // earlier command, provider/model, or trigger probe may run first. for (const step of compiled.steps) { if (step.type === 'deterministic') continue; + if (step.type === 'agent' && helperCall(step) !== undefined) continue; const resolution = resolveCli(step, compiled, options.projectCli); if (resolution === undefined) { diagnostics.push({ @@ -237,6 +241,22 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul warnOnVacuousGate(step, diagnostics); warnOnUnprovableEffects(step, options.probes, diagnostics); if (step.type === 'deterministic') continue; + const helper = step.type === 'agent' ? helperCall(step) : undefined; + if (helper !== undefined) { + try { + if (options.probes.helper === undefined) { + diagnostics.push({ severity: 'warning', kind: 'unprovable_effects', stepId: step.id, + message: `${helper.provider} helper requires a relayfile mount and an SDK agent worker.` }); + } else if (!options.probes.helper(helper.provider)) { + diagnostics.push({ severity: 'refusal', kind: 'helper_mount_required', stepId: step.id, + message: `${helper.provider} helper requires a relayfile mount.` }); + } + } catch { + diagnostics.push({ severity: 'refusal', kind: 'probe_failed', stepId: step.id, + message: `${helper.provider} helper mount could not be checked.` }); + } + continue; + } const resolution = resolutionByStep.get(step.id)!; probeResolvedCli(resolution, options.probes, cliProbeResults, diagnostics); } diff --git a/packages/sdk/src/spec.ts b/packages/sdk/src/spec.ts index e372126ca..42c072019 100644 --- a/packages/sdk/src/spec.ts +++ b/packages/sdk/src/spec.ts @@ -229,6 +229,33 @@ export interface AgentStepSpec extends BaseStepSpec { export type StepSpec = DeterministicStepSpec | LlmStepSpec | AgentStepSpec; +/** YAML argument maps use the same pinned client types as the TS helpers. */ +export interface YamlHelperParams { + slack: { [V in import('./slack-writeback.js').SlackCall['verb']]: + Extract['params'] }; + github: { + comment: { target: Parameters[0]; body: string }; + createIssue: Parameters[0]; + createPullRequest: Parameters[0]; + closePullRequest: Parameters[0]; + }; + linear: { + comment: { issueId: string; body: string }; + createIssue: Parameters[0]; + updateIssue: { issueId: string; args: Parameters[1] }; + }; +} + +type OneKey = { [K in keyof T]: Pick & Partial, never>> }[keyof T]; + +/** Helper sugar is removed before validation of the three kernel step types. */ +export type YamlHelperStepSpec = Pick & { + verification?: OutputVerificationSpec; + output?: JsonOutputSchema; +} & OneKey<{ [P in keyof YamlHelperParams]: OneKey }>; + +export type YamlFlowSpec = Omit & { steps: Array }; + /** * Reusable authoring declaration for an agent CLI/model pair. Both fields are * required so selecting a named agent can never inherit a host model. The diff --git a/packages/sdk/src/worker.ts b/packages/sdk/src/worker.ts index a931d5d65..8583ef36e 100644 --- a/packages/sdk/src/worker.ts +++ b/packages/sdk/src/worker.ts @@ -7,6 +7,8 @@ import type { KernelAgentStep } from './spec.js'; import { runAgentCli } from './worker-cli.js'; import { withWorkerLease } from './worker-lease.js'; import { workerInstruction } from './worker-input.js'; +import { helperCall } from './yaml-helpers.js'; +import { completeHelperDispatch } from './yaml-helper-effect.js'; export { MODEL_ENV, WAKE_CONTEXT_ENV } from './worker-cli.js'; @@ -96,6 +98,12 @@ export class AgentWorker extends EventEmitter { private async execute(dispatch: StepDispatchEvent): Promise { const spec = dispatch.spec as Partial; + const helper = helperCall(spec); + if (helper !== undefined) { + if (this.options.dataDir === undefined) throw new Error('Helper worker requires a data directory for durable receipts'); + await completeHelperDispatch(this.client, dispatch, helper, this.options.dataDir); + return; + } let humanIntervention = false; const completed: WorkerCliResult = await withWorkerLease(this.client, dispatch, signal => typeof spec.cli === 'string' && typeof spec.instruction === 'string' diff --git a/packages/sdk/src/yaml-helper-effect.ts b/packages/sdk/src/yaml-helper-effect.ts new file mode 100644 index 000000000..e9ddcc1a4 --- /dev/null +++ b/packages/sdk/src/yaml-helper-effect.ts @@ -0,0 +1,78 @@ +import { createHash } from 'node:crypto'; +import { statSync } from 'node:fs'; +import { join } from 'node:path'; +import { writeJsonFile } from '@relayfile/adapter-core/vfs-client'; +import type { RelayTransport } from '@relayfile/relay-helpers'; +import type { JournalClient } from './journal-client.js'; +import type { StepDispatchEvent } from './protocol.js'; +import { atomicJson, readSlackReceipt, receiptPath, slackWriteback } from './slack-writeback.js'; +import { invokeHelper, type HelperCall } from './yaml-helpers.js'; +import { withWorkerLease } from './worker-lease.js'; + +export function helperMount(provider: string, env = 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) return undefined; + try { return statSync(join(root, provider)).isDirectory() ? root : undefined; } + catch { return undefined; } +} + +export function helperReady(provider: string): boolean { + return (provider === 'slack' && process.env.RELAYFLOWS_SLACK_MOCK === '1') || helperMount(provider) !== undefined; +} + +async function writeback(call: HelperCall, dataDir: string, runId: string, stepId: string, signal: AbortSignal) { + // Exactly the TS surface's client, transport, idempotency stamp and receipt checks. + if (call.provider === 'slack') return slackWriteback(call, dataDir, runId, stepId, signal); + const mount = helperMount(call.provider); + if (mount === undefined) throw new Error(`${call.provider} helper requires a relayfile mount`); + const idempotencyKey = `${runId}:${stepId}`; + const transport: RelayTransport = { + async read() { throw new Error('Helper effect transport is write-only'); }, + async list() { throw new Error('Helper effect transport is write-only'); }, + async write(request) { + signal.throwIfAborted(); + const body = { ...request.body as Record, idempotencyKey }; + // Item updates keep the client's canonical path. Creates use a stable draft. + const path = request.path.endsWith('.json') ? request.path + : `${request.path}/draft-${createHash('sha256').update(idempotencyKey).digest('hex')}.json`; + const result = await writeJsonFile({ relayfileMountRoot: mount }, request.provider, + `write.${request.resource}`, path, body); + if (result.deliveryStatus !== 'confirmed' || !result.receipt) { + throw new Error(`${call.provider} writeback is pending; no delivery receipt`); + } + signal.throwIfAborted(); + return result; + }, + }; + return invokeHelper(call, transport); +} + +/** Existing lease/effect election protocol; provider verbs never enter the kernel. */ +export async function completeHelperDispatch( + client: JournalClient, dispatch: StepDispatchEvent, call: HelperCall, dataDir: string, +): Promise { + const surfacePath = `/${call.provider}`; + const 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, + 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 writeback(call, dataDir, dispatch.run_id, dispatch.step_id, signal); + await atomicJson(file, receipt); + } + signal.throwIfAborted(); + }); + if (receipt === undefined) receipt = await readSlackReceipt(file); + return { ...call, idempotencyKey: `${dispatch.run_id}:${dispatch.step_id}`, receipt }; + }); + 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: surfacePath, idempotency_key: dispatch.idempotency_key }] }); +} diff --git a/packages/sdk/src/yaml-helpers.ts b/packages/sdk/src/yaml-helpers.ts new file mode 100644 index 000000000..0d9b6339a --- /dev/null +++ b/packages/sdk/src/yaml-helpers.ts @@ -0,0 +1,131 @@ +import Ajv from 'ajv'; +import { githubClient, linearClient, slackClient, type RelayTransport } from '@relayfile/relay-helpers'; +import type { AgentStepSpec, KernelAgentStep, YamlHelperParams } from './spec.js'; +import { canonicalize } from './canonical.js'; +import { unknownKeyErrors } from './unknown-keys.js'; + +const string = { type: 'string', minLength: 1 }; +const text = { type: 'string' }; +const strings = { type: 'array', items: string }; +const object = (properties: Record, required = Object.keys(properties)) => + ({ type: 'object', properties, required, additionalProperties: false }); +const target = object({ owner: string, repo: string, number: { type: 'integer', minimum: 1 } }); +const issueChanges = { title: string, description: text, assigneeId: string, stateId: string, + projectId: string, labelIds: strings, addedLabelIds: strings, removedLabelIds: strings }; +const ajv = new Ajv({ allErrors: true }); + +function verb

(schema: object, invoke: (params: P, transport: RelayTransport) => Promise) { + const validate = ajv.compile(schema); + return { validate, invoke: (params: unknown, transport: RelayTransport) => invoke(params as P, transport) }; +} + +/** Closed catalog: only write verbs with a declared argument schema can execute. */ +const helpers = { + slack: { + post: verb(object({ channel: string, text, + opts: object({ replyTo: string }, []) }, ['channel', 'text']), + (p, transport) => slackClient({ transport }).post(p.channel, p.text as string, p.opts as { replyTo?: string } | undefined)), + dm: verb(object({ user: string, text }), + (p, transport) => slackClient({ transport }).dm(p.user, p.text)), + reply: verb(object({ channel: string, threadTs: string, text }), + (p, transport) => slackClient({ transport }).reply(p.channel, p.threadTs, p.text)), + react: verb(object({ channel: string, messageTs: string, emoji: string }), + (p, transport) => slackClient({ transport }).react(p.channel, p.messageTs, p.emoji)), + }, + github: { + comment: verb(object({ target, body: text }), + (p, transport) => githubClient({ transport }).comment(p.target, p.body)), + createIssue: verb(object({ owner: string, repo: string, + title: string, body: text, labels: strings }, ['owner', 'repo', 'title', 'body']), + (p, transport) => githubClient({ transport }).createIssue(p)), + createPullRequest: verb(object({ owner: string, + repo: string, title: string, head: string, base: string, body: text, draft: { type: 'boolean' }, + author: { enum: ['app', 'user'] } }, ['owner', 'repo', 'title', 'head', 'base']), + (p, transport) => githubClient({ transport }).createPullRequest(p)), + closePullRequest: verb(target, + (p, transport) => githubClient({ transport }).closePullRequest(p)), + }, + linear: { + comment: verb(object({ issueId: string, body: text }), + (p, transport) => linearClient({ transport }).comment(p.issueId, p.body)), + createIssue: verb(object({ teamId: string, title: string, + description: text, assigneeId: string, labelIds: strings, projectId: string, stateId: string }, ['teamId', 'title']), + (p, transport) => linearClient({ transport }).createIssue(p)), + updateIssue: verb(object({ issueId: string, + args: object(issueChanges, []) }), + (p, transport) => linearClient({ transport }).updateIssue(p.issueId, p.args)), + }, +}; + +export type HelperCall = { [P in keyof YamlHelperParams]: { [V in keyof YamlHelperParams[P]]: + { type: 'effect'; provider: P; verb: V; params: YamlHelperParams[P][V] } +}[keyof YamlHelperParams[P]] }[keyof YamlHelperParams]; + +const prefix = 'relayflows:helper:v1\n'; +const commonFields = ['id', 'dependsOn', 'maxIterations', 'verification', 'output'] as const; +const isObject = (value: unknown): value is Record => + value !== null && typeof value === 'object' && !Array.isArray(value); + +function descriptor(provider: string, name: string) { + if (!Object.hasOwn(helpers, provider)) throw new Error(`unknown helper provider "${provider}"`); + const verbs = helpers[provider as keyof typeof helpers]; + if (!Object.hasOwn(verbs, name)) throw new Error(`unknown ${provider} helper verb "${name}"`); + return (verbs as Record>)[name]!; +} + +function validateCall(value: unknown): HelperCall { + if (!isObject(value) || value.type !== 'effect' || typeof value.provider !== 'string' || typeof value.verb !== 'string') { + throw new Error('expected a helper effect call'); + } + const extra = unknownKeyErrors(value, ['type', 'provider', 'verb', 'params'], 'helper'); + if (extra.length) throw new Error(extra.join('; ')); + const entry = descriptor(value.provider, value.verb); + if (!entry.validate(value.params)) { + throw new Error(`${value.provider}.${value.verb}: ${ajv.errorsText(entry.validate.errors)}`); + } + return value as HelperCall; +} + +/** Runs on the inert JSON snapshot, before ordinary step validation. */ +export function expandYamlHelpers(spec: unknown): unknown { + if (!isObject(spec) || !Array.isArray(spec.steps)) return spec; + return { ...spec, steps: spec.steps.map((step, index) => { + if (!isObject(step)) return step; + const providers = Object.keys(helpers).filter(provider => Object.hasOwn(step, provider)); + if (!providers.length) { + // Recompiling a canonical envelope must fail with the same compile error + // contract as authoring it, before preflight or workers see it. + helperCall(step); + return step; + } + const at = `spec.steps[${index}]`; + try { + if (providers.length !== 1) throw new Error('expected exactly one helper provider per step'); + const provider = providers[0]!; + const errors = unknownKeyErrors(step, [...commonFields, provider], at); + if (errors.length) throw new Error(errors.join('; ')); + const verbs = step[provider]; + if (!isObject(verbs) || Object.keys(verbs).length !== 1) throw new Error(`${provider}: expected exactly one helper verb`); + const name = Object.keys(verbs)[0]!; + const call = validateCall({ type: 'effect', provider, verb: name, params: verbs[name] }); + const { [provider]: _, ...base } = step; + return { ...base, type: 'agent', instruction: prefix + canonicalize(call), + recoveryMode: 'reset', surfaces: { external: [`/${provider}`] } }; + } catch (error) { + throw new Error(`${at}: ${error instanceof Error ? error.message : String(error)}`); + } + }) }; +} + +/** The versioned instruction envelope survives canonical specs and journal resume. */ +export function helperCall(step: Partial): HelperCall | undefined { + if (step.type !== 'agent' || typeof step.instruction !== 'string' || !step.instruction.startsWith(prefix)) return undefined; + const call = validateCall(JSON.parse(step.instruction.slice(prefix.length))); + if (!step.surfaces?.external?.includes(`/${call.provider}`)) throw new Error('helper effect is missing its declared external surface'); + return call; +} + +export async function invokeHelper(call: HelperCall, transport: RelayTransport): Promise { + const validated = validateCall(call); + return (await descriptor(validated.provider, validated.verb).invoke(validated.params, transport)) ?? null; +} diff --git a/packages/sdk/tests/preflight.test.ts b/packages/sdk/tests/preflight.test.ts index 601b948d9..5670ea2e9 100644 --- a/packages/sdk/tests/preflight.test.ts +++ b/packages/sdk/tests/preflight.test.ts @@ -17,7 +17,7 @@ import { } from '../src/preflight.js'; import { preflight } from '../src/index.js'; import type { FlowSpec } from '../src/spec.js'; -import { compileSpec, toKernelSpec } from '../src/compile.js'; +import { compileSpec, compileYaml, toKernelSpec } from '../src/compile.js'; function flow(step: FlowSpec['steps'][number]): FlowSpec { return { version: '0.1.0', name: 'test', steps: [step] }; @@ -391,6 +391,10 @@ describe('preflight: CLI resolution and refusal predicates', () => { preflight(flow({ id: 'a', type: 'agent', instruction: 'i', cli: 'x', model: 'known-model' }), { models: ['known-model'], probes: probes({ cli: () => ({ exists: true, authenticated: true, modelAvailable: false }) }) }), preflight({ ...flow({ id: 'a', type: 'deterministic', command: 'x' }), triggers: [{ id: 't', executor: 'e' }] }, { probes: probes({ executor: () => false, command: () => false }) }), preflight(flow({ id: 'a', type: 'llm', prompt: 'p', cli: 'x' }), { probes: probes({ cli: () => { throw new Error('raw secret'); } }) }), + preflight( + compileYaml('version: 0.1.0\nsteps:\n - id: notify\n slack:\n post:\n channel: "#test"\n text: hi\n'), + { probes: probes({ helper: () => false }) }, + ), ]; const refusalKinds = scenarios.flatMap((result) => result.diagnostics) .filter((diagnostic) => diagnostic.severity === 'refusal') diff --git a/packages/sdk/tests/yaml-helper-effect.test.ts b/packages/sdk/tests/yaml-helper-effect.test.ts new file mode 100644 index 000000000..e75857505 --- /dev/null +++ b/packages/sdk/tests/yaml-helper-effect.test.ts @@ -0,0 +1,90 @@ +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { EventEmitter } from 'node:events'; +import { afterEach, expect, it, vi } from 'vitest'; +import { completeHelperDispatch } from '../src/yaml-helper-effect.js'; +import { compileYaml, toKernelSpec } from '../src/compile.js'; +import { helperCall } from '../src/yaml-helpers.js'; +import { receiptPath } from '../src/slack-writeback.js'; +import type { JournalClient } from '../src/journal-client.js'; +import type { StepDispatchEvent } from '../src/protocol.js'; +import { AgentWorker } from '../src/worker.js'; + +const dirs: string[] = []; +afterEach(() => { + for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); + vi.unstubAllEnvs(); +}); + +function fixture() { + vi.stubEnv('RELAYFLOWS_SLACK_MOCK', '1'); + const dataDir = mkdtempSync(join(tmpdir(), 'yaml-effect-')); + dirs.push(dataDir); + const spec = toKernelSpec(compileYaml(`version: 0.1.0 +steps: + - id: notify + slack: {post: {channel: "#test", text: hi}} +`)).steps[0]!; + if (spec.type !== 'agent') throw new Error('expected agent lowering'); + const call = helperCall(spec)!; + const dispatch = { run_id: 'run', step_id: 'notify', step_type: 'agent', attempt: 1, + idempotency_key: 'election', lease_id: 'lease', spec, pins: { workspace: [], streams: [] }, + lease_deadline_ms: Date.now() + 60_000 } as StepDispatchEvent; + const performEffect = vi.fn(async (_options, execute: () => Promise) => { await execute(); }); + const stepComplete = vi.fn().mockResolvedValue({}); + const client = Object.assign(new EventEmitter(), { performEffect, stepComplete, + workerAttach: vi.fn().mockResolvedValue({}), + stepHeartbeat: vi.fn().mockResolvedValue({ lease_deadline_ms: Date.now() + 60_000 }) }) as unknown as JournalClient; + return { dataDir, call, dispatch, client, performEffect, stepComplete }; +} + +it('dispatches the compiled helper from the SDK agent worker without a CLI', async () => { + const f = fixture(); + const worker = new AgentWorker(f.client, { workerId: 'helper-test', dataDir: f.dataDir, + pins: f.dispatch.pins }); + const errors = vi.fn(); + worker.on('error', errors); + await worker.attach(); + try { + f.client.emit('step.dispatch', f.dispatch); + await vi.waitFor(() => expect(f.stepComplete).toHaveBeenCalledTimes(1)); + expect(errors).not.toHaveBeenCalled(); + expect(f.performEffect).toHaveBeenCalledWith(expect.objectContaining({ surfacePath: '/slack' }), expect.any(Function)); + } finally { await worker.close(); } +}); + +it('records the provider effect and recovers its receipt after a crash before completion', async () => { + const f = fixture(); + f.stepComplete.mockRejectedValueOnce(new Error('crash before completion')); + await expect(completeHelperDispatch(f.client, f.dispatch, f.call, f.dataDir)).rejects.toThrow('crash before completion'); + const receipt = JSON.parse(readFileSync(receiptPath(f.dataDir, 'run', 'notify'), 'utf8')); + expect(receipt).toMatchObject({ channel: '#test', ts: 'mock-notify' }); + // A confirmed election skips provider I/O. The durable receipt still completes the step. + f.performEffect.mockImplementationOnce(async () => {}); + vi.stubEnv('RELAYFLOWS_SLACK_MOCK', '0'); + await completeHelperDispatch(f.client, f.dispatch, f.call, f.dataDir); + expect(f.stepComplete).toHaveBeenLastCalledWith('run', 'notify', 1, 'election', 'success', + expect.objectContaining({ output: { ...f.call, idempotencyKey: 'run:notify', receipt }, + effects: [{ surface_path: '/slack', idempotency_key: 'election' }] })); +}); + +it('reuses a saved receipt if the process died before effect confirmation', async () => { + const f = fixture(); + f.performEffect.mockImplementationOnce(async (_options, execute) => { + await execute(); + throw new Error('crash before confirmation'); + }); + await expect(completeHelperDispatch(f.client, f.dispatch, f.call, f.dataDir)).rejects.toThrow('crash before confirmation'); + expect(f.stepComplete).not.toHaveBeenCalled(); + vi.stubEnv('RELAYFLOWS_SLACK_MOCK', '0'); + await completeHelperDispatch(f.client, f.dispatch, f.call, f.dataDir); + expect(f.stepComplete).toHaveBeenCalledTimes(1); +}); + +it('does not report success when the effect election fails', async () => { + const f = fixture(); + f.performEffect.mockRejectedValueOnce(new Error('journal write failed')); + await expect(completeHelperDispatch(f.client, f.dispatch, f.call, f.dataDir)).rejects.toThrow('journal write failed'); + expect(f.stepComplete).not.toHaveBeenCalled(); +}); diff --git a/packages/sdk/tests/yaml-helper-live.test.ts b/packages/sdk/tests/yaml-helper-live.test.ts new file mode 100644 index 000000000..622911f3d --- /dev/null +++ b/packages/sdk/tests/yaml-helper-live.test.ts @@ -0,0 +1,77 @@ +import { spawnSync } from 'node:child_process'; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { afterEach, expect, it } from 'vitest'; +import { JournalClient } from '../src/journal-client.js'; + +const roots: string[] = []; +afterEach(() => { + for (const root of roots.splice(0)) { + const connection = join(root, 'data/connection.json'); + if (existsSync(connection)) { + const { pid } = JSON.parse(readFileSync(connection, 'utf8')); + try { process.kill(pid, 'SIGTERM'); } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error; + } + } + rmSync(root, { recursive: true, force: true }); + } +}); + +it('runs compiled YAML helpers through the built CLI and kernel effect journal', async () => { + const root = mkdtempSync(join(tmpdir(), 'yaml-helper-live-')); + roots.push(root); + const path = join(root, 'notify.yaml'); + writeFileSync(path, `version: 0.1.0 +steps: + - id: before + type: deterministic + command: "true" + - id: notify + dependsOn: [before] + slack: {post: {channel: "#test", text: hi}} + - id: after + dependsOn: [notify] + type: deterministic + command: "true" +`); + // If the caller (CI, or a developer with a prebuilt kernel) already picked + // the relayflowd binary, honor that verbatim. Falling through to `cargo + // metadata` here would fail on CI runners whose rustup has no default cargo + // configured — the binary is prebuilt and shipped via RELAYFLOWD_BIN in that + // shape, so the metadata query is only needed to LOCATE it otherwise. + let binary = process.env.RELAYFLOWD_BIN; + if (binary === undefined) { + const metadata = spawnSync('sh', [resolve('../../ops/cargo.sh'), 'metadata', '--format-version=1', '--no-deps', '--locked', '--offline'], + { cwd: resolve('../../kernel'), encoding: 'utf8' }); + expect(metadata.status, metadata.stderr).toBe(0); + binary = join(JSON.parse(metadata.stdout).target_directory, 'debug/relayflowd'); + } + const cli = process.env.FLOWS_TEST_CLI ?? resolve('dist/cli.js'); + const args = ['--json', '--no-observer-link', '--data-dir', join(root, 'data')]; + const invoke = (...command: string[]) => spawnSync(process.execPath, [cli, ...command, ...args], { + cwd: root, encoding: 'utf8', timeout: 30_000, + env: { ...process.env, RELAYFLOWD_BIN: binary, RELAYFLOWS_SLACK_MOCK: '1' }, + }); + const result = invoke('run', path, '--local-agent'); + expect(result.status, result.stdout + result.stderr).toBe(0); + const report = JSON.parse(result.stdout); + expect(report).toMatchObject({ status: 'completed', completionReason: 'success', completedSteps: 3 }); + const client = new JournalClient(report.socketPath); + try { + await client.connect(); + await client.hello('yaml-helper-test'); + const entries = (await client.journalRead(report.runId, 1)).entries as Array<{ entry_type: string }>; + expect(entries).toContainEqual(expect.objectContaining({ entry_type: 'step.completed', step_id: 'notify', + payload: expect.objectContaining({ completionReason: 'success', output: expect.objectContaining({ + type: 'effect', provider: 'slack', verb: 'post', + receipt: { channel: '#test', ts: 'mock-notify', ref: 'mock-ref-notify' }, + }) }) })); + expect(entries.filter(entry => entry.entry_type === 'effect.recorded')).toHaveLength(1); + const resumed = invoke('resume', report.runId); + expect(resumed.status, resumed.stdout + resumed.stderr).toBe(0); + const after = (await client.journalRead(report.runId, 1)).entries as Array<{ entry_type: string }>; + expect(after.filter(entry => entry.entry_type === 'effect.recorded')).toHaveLength(1); + } finally { client.close(); } +}); diff --git a/packages/sdk/tests/yaml-helpers.test.ts b/packages/sdk/tests/yaml-helpers.test.ts new file mode 100644 index 000000000..18a5de3a1 --- /dev/null +++ b/packages/sdk/tests/yaml-helpers.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, it, vi } from 'vitest'; +import { PreviewTransport } from '@relayfile/relay-helpers'; +import { CompileError, compileSpec, compileYaml, compileYamlToCanonicalJson, kernelToAuthoring, toKernelSpec } from '../src/compile.js'; +import { helperCall, invokeHelper } from '../src/yaml-helpers.js'; +import { preflight } from '../src/preflight.js'; +import type { YamlFlowSpec } from '../src/spec.js'; + +const yaml = `version: 0.1.0 +steps: + - id: notify + slack: + post: + channel: "#test" + text: hi +`; + +function compile(step: unknown) { + return compileSpec({ version: '0.1.0', steps: [step] }); +} + +describe('YAML helper expansion', () => { + it('lowers Slack into an ordinary effect-bearing agent step and round-trips canonically', () => { + const flow = compileYaml(yaml); + const step = flow.steps[0]!; + expect(step).toMatchObject({ id: 'notify', type: 'agent', maxIterations: 1, + recoveryMode: 'reset', surfaces: { external: ['/slack'] } }); + expect(step).not.toHaveProperty('slack'); + expect(helperCall(step as any)).toEqual({ type: 'effect', provider: 'slack', verb: 'post', + params: { channel: '#test', text: 'hi' } }); + const kernel = toKernelSpec(flow); + expect(kernel.steps[0]).not.toHaveProperty('slack'); + expect(toKernelSpec(compileSpec(kernelToAuthoring(kernel)))).toEqual(kernel); + expect(compileYamlToCanonicalJson(yaml)).toBe(compileYamlToCanonicalJson(yaml.replace(' channel: "#test"\n text: hi', ' text: hi\n channel: "#test"'))); + }); + + it('preserves identities, dependencies, retries and output verification without mutating input', () => { + const input: YamlFlowSpec = { version: '0.1.0', steps: [ + { id: 'before', type: 'deterministic', command: 'true' }, + { id: 'notify', dependsOn: ['before'], maxIterations: 3, + output: { type: 'object', properties: { receipt: { type: 'object' } }, required: ['receipt'] }, + slack: { post: { channel: '#test', text: 'hi' } } }, + ] }; + const snapshot = structuredClone(input); + const flow = compileSpec(input); + expect(flow.steps[1]).toMatchObject({ id: 'notify', dependsOn: ['before'], maxIterations: 3, + verification: { type: 'json_schema', schema: { required: ['receipt'] } } }); + expect(input).toEqual(snapshot); + expect(compileSpec(flow)).toEqual(flow); + }); + + it.each>([ + { slack: null }, { slack: [] }, { slack: {} }, + { slack: { post: {}, dm: {} } }, { slack: { unknown: {} } }, + { slack: { constructor: {} } }, { slack: { post: null } }, + { slack: { post: { channel: '#test' } } }, + { slack: { post: { channel: '#test', text: 42 } } }, + { slack: { post: { channel: '#test', text: 'hi', typo: true } } }, + { slack: { post: { channel: '#test', text: 'hi', opts: { typo: 'x' } } } }, + { github: { comment: { target: { owner: 'o', repo: 'r', number: '1' }, body: 'hi' } } }, + { linear: { updateIssue: { issueId: 'ENG-1', args: { typo: 'x' } } } }, + { slack: { dm: { user: 'u', text: 'hi' } }, linear: { comment: { issueId: 'i', body: 'hi' } } }, + { slack: { dm: { user: 'u', text: 'hi' } }, type: 'agent', instruction: 'ignored' }, + { slack: { dm: { user: 'u', text: 'hi' } }, command: 'false' }, + { slack: { dm: { user: 'u', text: 'hi' } }, depends_on: ['missing'] }, + { unknownProvider: { post: {} } }, + ])('rejects invalid helper authoring %j', fields => { + expect(() => compile({ id: 'invalid', ...fields })).toThrow(CompileError); + }); + + it('still validates dependencies and duplicate ids after expansion', () => { + expect(() => compile({ id: 'x', dependsOn: ['missing'], slack: { dm: { user: 'u', text: '' } } })).toThrow(/missing/); + expect(() => compileSpec({ version: '0.1.0', steps: [ + { id: 'x', slack: { dm: { user: 'u', text: '' } } }, + { id: 'x', type: 'deterministic', command: 'true' }, + ] })).toThrow(/duplicate/); + }); + + it.each([ + ['slack', 'post', { channel: '#test', text: 'hi', opts: { replyTo: 'parent' } }, 'messages', { text: 'hi', parentRef: 'parent' }], + ['slack', 'dm', { user: 'U1', text: 'hi' }, 'direct-messages', { text: 'hi' }], + ['slack', 'reply', { channel: 'C1', threadTs: '123', text: 'hi' }, 'replies', { text: 'hi' }], + ['slack', 'react', { channel: 'C1', messageTs: '123', emoji: 'eyes' }, 'reactions', { emoji: 'eyes' }], + ['github', 'comment', { target: { owner: 'o', repo: 'r', number: 1 }, body: 'hi' }, 'issue-comments', { body: 'hi' }], + ['github', 'createIssue', { owner: 'o', repo: 'r', title: 'Bug', body: 'hi', labels: ['bug'] }, 'issues', { title: 'Bug', labels: ['bug'] }], + ['github', 'createPullRequest', { owner: 'o', repo: 'r', title: 'Fix', head: 'fix', base: 'main', draft: true }, 'pull-requests', { title: 'Fix', draft: true }], + ['github', 'closePullRequest', { owner: 'o', repo: 'r', number: 1 }, 'close-pull-request', {}], + ['linear', 'comment', { issueId: 'ENG-1', body: 'hi' }, 'comments', { body: 'hi' }], + ['linear', 'createIssue', { teamId: 'team', title: 'Bug' }, 'issues', { teamId: 'team', title: 'Bug' }], + ['linear', 'updateIssue', { issueId: 'ENG-1', args: { title: 'Fixed' } }, 'issues', { title: 'Fixed' }], + ])('dispatches %s.%s through the pinned relay-helper client', async (provider, name, params, resource, body) => { + const step = compile({ id: 'call', [provider as string]: { [name as string]: params } }).steps[0]!; + const transport = new PreviewTransport(); + const write = vi.spyOn(transport, 'write'); + await invokeHelper(helperCall(step as any)!, transport); + expect(write).toHaveBeenCalledTimes(1); + expect(write).toHaveBeenCalledWith(expect.objectContaining({ provider, resource, + body: expect.objectContaining(body as object) })); + }); + + it('checks helper mounts without resolving an agent CLI', () => { + const cli = vi.fn(); + const helper = vi.fn().mockReturnValue(true); + const probes = { cli, helper, command: () => true, executor: () => true }; + expect(preflight(compileYaml(yaml), { probes })).toMatchObject({ ok: true, resolutions: [] }); + expect(helper).toHaveBeenCalledWith('slack'); + expect(cli).not.toHaveBeenCalled(); + helper.mockReturnValue(false); + expect(preflight(compileYaml(yaml), { probes })).toMatchObject({ ok: false, + diagnostics: [expect.objectContaining({ kind: 'helper_mount_required' })] }); + helper.mockImplementation(() => { throw new Error('probe unavailable'); }); + expect(preflight(compileYaml(yaml), { probes })).toMatchObject({ ok: false, + diagnostics: [expect.objectContaining({ kind: 'probe_failed' })] }); + }); +}); diff --git a/packages/sdk/tsconfig.slice-v.tmp.json b/packages/sdk/tsconfig.slice-v.tmp.json new file mode 100644 index 000000000..fe6e5639b --- /dev/null +++ b/packages/sdk/tsconfig.slice-v.tmp.json @@ -0,0 +1 @@ +{"extends": "./tsconfig.tests.json", "include": ["src/**/*.ts", "tests/yaml-helpers.test.ts", "tests/yaml-helper-effect.test.ts", "tests/yaml-helper-live.test.ts"]} \ No newline at end of file