From ea4a54044655e849829e5e9227d1c62a6d8079c6 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Thu, 17 Sep 2026 15:34:10 -0700 Subject: [PATCH 1/7] =?UTF-8?q?docs(relayflows):=20audit=20against=20flows?= =?UTF-8?q?=20v2.0.16=20=E2=80=94=20corrections=20and=20missing=20hosted?= =?UTF-8?q?=20verbs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every claim in web/content/docs/relayflows/*.mdx was checked against AgentWorkforce/flows fe8d7606 (v2.0.16): the CLI USAGE block, the surface types, SURFACE.md / CLOUD.md / BUDGET.md, and by extracting every code sample and running `tsc` plus `flows check` against a built 2.0.16 surface. Wrong now, fixed: - `flows check` does accept `.flow.ts` (and `--watch`); quickstart and cli said it did not. - `flows run --cloud` accepts an authored `.flow.ts` with `--input`; cloud said only declarative flows. - `f.done` verdicts are success / step_failed / needs_human / declined; `canceled` and `budget_exceeded` are refused at runtime (`unsupported_completion`). introduction, build, multi-agent and five marketing samples used `canceled`; they now use `declined`, which shipped in 2.0.16. - A missing CLI refuses `cli_unresolved`, not `invalid_spec`. - `budget` is a TypeScript header too (`"$5/run"`, `"$20/day"`, `{ tokens, dollars, wallclock }`); build.mdx called it YAML-only and listed the legacy `maxTokensIn/Out/maxDollars` envelope. - `f.memory.recall`/`why` return entry arrays, not strings; `learn` refuses in 2.0.16. The sample interpolated `[object Object]`. - `f.github.createPullRequest` takes `{ owner, repo, title, head, base, body }`; the software-factory marketing sample passed `bodyPath` and no title (a type error). - The onboarding comment justifying a wall-clock budget cited the #421 refusal, fixed in 2.0.13; the comment now states the current reason. Missing, added: the whole hosted surface (`--sync-code`, `flows sync`, `flows deploy --repo --on --approver`, `deployments`, `undeploy`), the login-store credential fallback, `replay`, `build` / digest `deploy`, `serve-webhook`, `--reuse-from`, `--allow-human-influenced`, exit-code detail (`run_not_found` → 2, `declined` → 0), verification kinds (`exit_code`, the four named gates, postfix `.gate(config)`), `flows.json` `models` allowlist, and where deterministic steps actually run (the daemon's cwd). Not yet shipped, now marked as such instead of implied available: callback `.gate(fn)` and `r.artifacts` (flows#449), PR-event listeners and hosted v2 schedules. Marketing samples keep their `.gate(fn)` / `artifacts` shape on purpose. Co-Authored-By: Claude Opus 5 (1M context) --- web/app/flows/flow-examples.ts | 12 +-- web/content/docs/relayflows/build.mdx | 31 ++++--- web/content/docs/relayflows/cli.mdx | 81 +++++++++++++++---- web/content/docs/relayflows/cloud.mdx | 81 ++++++++++++++----- web/content/docs/relayflows/introduction.mdx | 4 +- .../relayflows/memory-and-integrations.mdx | 27 ++++--- web/content/docs/relayflows/multi-agent.mdx | 6 +- web/content/docs/relayflows/quickstart.mdx | 4 +- web/content/docs/relayflows/reliability.mdx | 6 +- web/lib/flow-onboarding.ts | 8 +- 10 files changed, 184 insertions(+), 76 deletions(-) diff --git a/web/app/flows/flow-examples.ts b/web/app/flows/flow-examples.ts index 0009d32..860ffa7 100644 --- a/web/app/flows/flow-examples.ts +++ b/web/app/flows/flow-examples.ts @@ -5,7 +5,7 @@ export const flowExamples = [ "title": "Turn a ticket into a pull request.", "description": "Plan the change, write the code, and run the tests before opening a PR.", "filename": "software-factory.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n repo: string;\n ticket: string;\n};\n\nexport default flow(\n \"software-factory\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"planner\", {\n task: `${input.ticket} Write a plan to plan.md.`,\n }).gate((r) => r.artifacts.includes(\"plan.md\"));\n\n await f.agent(\"implementer\", {\n task: \"Read plan.md. Implement it on branch flow/fix. \" +\n \"Write the PR description to summary.md.\",\n }).gate((r) => r.artifacts.includes(\"summary.md\"));\n\n // The tests run outside the agent.\n // The agent cannot lie about the exit code.\n await f.run(\"git checkout flow/fix && npm test\");\n\n await f.agent(\"reviewer\", {\n task: \"Review the diff against main. \" +\n \"Write review.passed only if ready for a PR.\",\n }).gate((r) => r.artifacts.includes(\"review.passed\"));\n\n // Deterministic step, not an agent decision.\n await f.github.createPullRequest({\n repo: input.repo,\n head: \"flow/fix\",\n base: \"main\",\n bodyPath: \"summary.md\",\n });\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n repo: string;\n ticket: string;\n};\n\nexport default flow(\n \"software-factory\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"planner\", {\n task: `${input.ticket} Write a plan to plan.md.`,\n }).gate((r) => r.artifacts.includes(\"plan.md\"));\n\n await f.agent(\"implementer\", {\n task: \"Read plan.md. Implement it on branch flow/fix. \" +\n \"Write the PR description to summary.md.\",\n }).gate((r) => r.artifacts.includes(\"summary.md\"));\n\n // The tests run outside the agent.\n // The agent cannot lie about the exit code.\n await f.run(\"git checkout flow/fix && npm test\");\n\n await f.agent(\"reviewer\", {\n task: \"Review the diff against main. \" +\n \"Write review.passed only if ready for a PR.\",\n }).gate((r) => r.artifacts.includes(\"review.passed\"));\n\n // Deterministic step, not an agent decision.\n const [owner, repo] = input.repo.split(\"/\");\n await f.github.createPullRequest({\n owner, repo,\n title: input.ticket,\n head: \"flow/fix\",\n base: \"main\",\n body: await f.run(\"cat summary.md\"),\n });\n f.done(\"success\");\n },\n);" }, { "id": "pr-review", @@ -21,7 +21,7 @@ export const flowExamples = [ "title": "Draft the reply. Let a human decide.", "description": "Draft a response to a support thread, get approval, and reply in Slack.", "filename": "support-triage.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n message: string;\n channel: string;\n threadTs: string;\n approver: string;\n};\n\nexport default flow(\n \"support-triage\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Classify this request: ${input.message}. ` +\n \"Write the category and urgency to triage.md.\",\n }).gate((r) => r.artifacts.includes(\"triage.md\"));\n\n await f.agent(\"writer\", {\n task: `Read triage.md and draft a reply to: ` +\n `${input.message}. Write only the reply to reply.md.`,\n }).gate((r) => r.artifacts.includes(\"reply.md\"));\n\n const reply = await f.run(\"cat reply.md\");\n const approved = await f.human(\n `Send this reply?\\n\\n${reply}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n // Send only after a human approves.\n await f.slack.reply(input.channel, input.threadTs, reply);\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n message: string;\n channel: string;\n threadTs: string;\n approver: string;\n};\n\nexport default flow(\n \"support-triage\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Classify this request: ${input.message}. ` +\n \"Write the category and urgency to triage.md.\",\n }).gate((r) => r.artifacts.includes(\"triage.md\"));\n\n await f.agent(\"writer\", {\n task: `Read triage.md and draft a reply to: ` +\n `${input.message}. Write only the reply to reply.md.`,\n }).gate((r) => r.artifacts.includes(\"reply.md\"));\n\n const reply = await f.run(\"cat reply.md\");\n const approved = await f.human(\n `Send this reply?\\n\\n${reply}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n // Send only after a human approves.\n await f.slack.reply(input.channel, input.threadTs, reply);\n f.done(\"success\");\n },\n);" }, { "id": "content-pipeline", @@ -29,7 +29,7 @@ export const flowExamples = [ "title": "Put a fact-check between draft and publish.", "description": "Research, draft, and check a post before a human approves sharing it in Slack.", "filename": "content-pipeline.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n topic: string;\n channel: string;\n approver: string;\n};\n\nexport default flow(\n \"content-pipeline\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"researcher\", {\n task: `Research ${input.topic}. Cite your sources. ` +\n \"Write research.md.\",\n }).gate((r) => r.artifacts.includes(\"research.md\"));\n\n await f.agent(\"writer\", {\n task: \"Use research.md to write post.md. \" +\n \"Keep the source links with each claim.\",\n }).gate((r) => r.artifacts.includes(\"post.md\"));\n\n await f.agent(\"fact-checker\", {\n task: \"Check post.md against its sources. \" +\n \"Write checked.passed only if all claims hold up.\",\n }).gate((r) => r.artifacts.includes(\"checked.passed\"));\n\n const post = await f.run(\"cat post.md\");\n const approved = await f.human(\n `Publish this post?\\n\\n${post}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n await f.slack.post(input.channel, post);\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n topic: string;\n channel: string;\n approver: string;\n};\n\nexport default flow(\n \"content-pipeline\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"researcher\", {\n task: `Research ${input.topic}. Cite your sources. ` +\n \"Write research.md.\",\n }).gate((r) => r.artifacts.includes(\"research.md\"));\n\n await f.agent(\"writer\", {\n task: \"Use research.md to write post.md. \" +\n \"Keep the source links with each claim.\",\n }).gate((r) => r.artifacts.includes(\"post.md\"));\n\n await f.agent(\"fact-checker\", {\n task: \"Check post.md against its sources. \" +\n \"Write checked.passed only if all claims hold up.\",\n }).gate((r) => r.artifacts.includes(\"checked.passed\"));\n\n const post = await f.run(\"cat post.md\");\n const approved = await f.human(\n `Publish this post?\\n\\n${post}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n await f.slack.post(input.channel, post);\n f.done(\"success\");\n },\n);" }, { "id": "repo-migration", @@ -45,7 +45,7 @@ export const flowExamples = [ "title": "Turn a voicemail into a callback brief.", "description": "Use a transcript to prepare the callback, then send the approved brief to a teammate.", "filename": "voicemail-follow-up.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n transcript: string;\n approver: string;\n callbackOwner: string;\n};\n\nexport default flow(\n \"voicemail-follow-up\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Read this voicemail: ${input.transcript}. ` +\n \"Identify urgency and the caller's request. \" +\n \"Write triage.md.\",\n }).gate((r) => r.artifacts.includes(\"triage.md\"));\n\n await f.agent(\"callback-writer\", {\n task: \"Read triage.md. Prepare a callback brief \" +\n \"with the questions we need to answer. \" +\n \"Write callback.md.\",\n }).gate((r) => r.artifacts.includes(\"callback.md\"));\n\n const brief = await f.run(\"cat callback.md\");\n const approved = await f.human(\n `Ready for a callback?\\n\\n${brief}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n await f.slack.dm(input.callbackOwner, brief);\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n transcript: string;\n approver: string;\n callbackOwner: string;\n};\n\nexport default flow(\n \"voicemail-follow-up\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Read this voicemail: ${input.transcript}. ` +\n \"Identify urgency and the caller's request. \" +\n \"Write triage.md.\",\n }).gate((r) => r.artifacts.includes(\"triage.md\"));\n\n await f.agent(\"callback-writer\", {\n task: \"Read triage.md. Prepare a callback brief \" +\n \"with the questions we need to answer. \" +\n \"Write callback.md.\",\n }).gate((r) => r.artifacts.includes(\"callback.md\"));\n\n const brief = await f.run(\"cat callback.md\");\n const approved = await f.human(\n `Ready for a callback?\\n\\n${brief}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n await f.slack.dm(input.callbackOwner, brief);\n f.done(\"success\");\n },\n);" }, { "id": "research-report", @@ -61,7 +61,7 @@ export const flowExamples = [ "title": "Choose what the summarizer gets to see.", "description": "Select the fields needed for the task before putting a record into the agent prompt.", "filename": "redacted-summary.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n record: { category: string; status: string; email: string };\n approver: string;\n};\n\nexport default flow(\n \"redacted-summary\",\n { budget: \"$5/run\" },\n async (f, input) => {\n // Select fields in code before building the prompt.\n // The email address is excluded from this input.\n const selected = {\n category: input.record.category,\n status: input.record.status,\n };\n\n await f.agent(\"summarizer\", {\n task: `Summarize this record: ` +\n `${JSON.stringify(selected)}. Write summary.md.`,\n }).gate((r) => r.artifacts.includes(\"summary.md\"));\n\n const summary = await f.run(\"cat summary.md\");\n const approved = await f.human(\n `Review this summary before sharing:\\n\\n${summary}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n record: { category: string; status: string; email: string };\n approver: string;\n};\n\nexport default flow(\n \"redacted-summary\",\n { budget: \"$5/run\" },\n async (f, input) => {\n // Select fields in code before building the prompt.\n // The email address is excluded from this input.\n const selected = {\n category: input.record.category,\n status: input.record.status,\n };\n\n await f.agent(\"summarizer\", {\n task: `Summarize this record: ` +\n `${JSON.stringify(selected)}. Write summary.md.`,\n }).gate((r) => r.artifacts.includes(\"summary.md\"));\n\n const summary = await f.run(\"cat summary.md\");\n const approved = await f.human(\n `Review this summary before sharing:\\n\\n${summary}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n f.done(\"success\");\n },\n);" }, { "id": "ci-repair", @@ -77,7 +77,7 @@ export const flowExamples = [ "title": "Send each issue to the right specialist.", "description": "Route bugs to an implementer and documentation requests to a writer. Ask a human when the category is uncertain.", "filename": "issue-routing.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = { issue: string; approver: string };\n\nexport default flow(\"issue-routing\", async (f, input) => {\n const classification = await f.agent(\"router\", {\n cli: \"claude\",\n task: `Classify this issue: ${input.issue}. ` +\n \"Return only bugfix, docs, or uncertain as your summary. \" +\n \"Choose uncertain if the request is ambiguous.\",\n });\n let route = classification.summary.trim().toLowerCase();\n\n // Unknown output never silently selects a specialist.\n if (route !== \"bugfix\" && route !== \"docs\") {\n const approved = await f.human(\n `Needs manual triage: ${input.issue}\\n` +\n \"Route this to the bugfix specialist?\",\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n route = \"bugfix\";\n }\n\n if (route === \"bugfix\") {\n await f.agent(\"implementer\", {\n cli: \"codex\",\n task: `Fix this issue and add regression coverage: ${input.issue}`,\n });\n await f.run(\"npm test\");\n } else {\n await f.agent(\"docs-writer\", {\n cli: \"claude\",\n task: `Update the documentation for: ${input.issue}. ` +\n \"Check examples against the implementation.\",\n });\n }\n f.done(\"success\");\n});" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = { issue: string; approver: string };\n\nexport default flow(\"issue-routing\", async (f, input) => {\n const classification = await f.agent(\"router\", {\n cli: \"claude\",\n task: `Classify this issue: ${input.issue}. ` +\n \"Return only bugfix, docs, or uncertain as your summary. \" +\n \"Choose uncertain if the request is ambiguous.\",\n });\n let route = classification.summary.trim().toLowerCase();\n\n // Unknown output never silently selects a specialist.\n if (route !== \"bugfix\" && route !== \"docs\") {\n const approved = await f.human(\n `Needs manual triage: ${input.issue}\\n` +\n \"Route this to the bugfix specialist?\",\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n route = \"bugfix\";\n }\n\n if (route === \"bugfix\") {\n await f.agent(\"implementer\", {\n cli: \"codex\",\n task: `Fix this issue and add regression coverage: ${input.issue}`,\n });\n await f.run(\"npm test\");\n } else {\n await f.agent(\"docs-writer\", {\n cli: \"claude\",\n task: `Update the documentation for: ${input.issue}. ` +\n \"Check examples against the implementation.\",\n });\n }\n f.done(\"success\");\n});" }, { "id": "competing-implementations", diff --git a/web/content/docs/relayflows/build.mdx b/web/content/docs/relayflows/build.mdx index 1356799..46773f9 100644 --- a/web/content/docs/relayflows/build.mdx +++ b/web/content/docs/relayflows/build.mdx @@ -71,34 +71,47 @@ Reach for YAML when you want the whole flow readable at a glance and checkable i Both `edit` steps above name their own `cli` and `model` directly, the same way in either language ([flows#310](https://github.com/AgentWorkforce/flows/issues/310)). Neither is required in TypeScript: omit them and the step falls back to the flow's `cli`, then the nearest `flows.json`'s project-wide default, the same resolution [Introduction](/docs/relayflows/introduction#a-whole-flow) covers. - `recoveryMode`, `permissions`, `surfaces`, and `budget`, the richer - fields covered further down, are YAML/JSON fields only today. - TypeScript's `Ctx.agent` doesn't have them. If a step needs them, - author it in YAML and either run it directly or have a TypeScript - flow reach it with `f.dispatch`. + `recoveryMode`, `permissions`, and `surfaces`, the richer step fields + covered further down, are YAML/JSON fields only today. TypeScript's + `f.agent` takes `{ task, workspace?, cli?, model? }`, and a `workspace` + with a `: readonly` / `: readwrite` annotation is refused, because nothing + enforces it yet. `budget` is available in both: the TypeScript form is the + flow header, `flow('name', { budget: '$5/run' }, async (f) => …)`. ## The context a flow body gets ```ts interface Ctx { - run(command: string): Step; + run(command: string, options?: { timeout?: string | number }): Step; // lease: default 30s, max 15m llm(strings: TemplateStringsArray, ...values: unknown[]): Step; + llm(prompt: string, options: { output: JsonSchema; cli?: string; model?: string }): Step; agent(name: string, options: { task: string; workspace?: string; cli?: string; model?: string }): Step<{ summary: string; artifacts: string[] }>; human(question: string, options: { to: string }): Promise; dispatch(flow: string, input: unknown): Promise; - done(reason: 'success' | 'step_failed' | 'canceled' | 'budget_exceeded'): void; + done(reason: 'success' | 'step_failed' | 'needs_human' | 'declined'): void; + slack: SlackHelper; github: GithubHelper; /* …every generated helper */ + memory: MemoryHelper; + mcp: Record Step>>; } ``` This is the whole kernel-level vocabulary a step body speaks: `run`, `llm`, `agent` for work, `human`, `dispatch`, `done` for control. `human` parks the run on a durable await instead of a blocking call — it's a wait the journal can survive a restart across. `dispatch` hands work to a named child flow and returns its typed result. +`done` takes one of four authored verdicts. `success` completes the run; `step_failed` says the flow's own checks did not pass (the adversarial review found problems, the tests went red) and exits `1`; `needs_human` parks the run (exit `3`); `declined` records a deliberate decision not to act on the input — a ticket that turned out not to be work — and exits `0` with a `DECLINED` diagnostic. `canceled` and `budget_exceeded` are in the `FlowCompletionReason` type but refused at runtime with `unsupported_completion`: they are kernel facts, recorded when the kernel cancels a run or exhausts its budget, not verdicts a body can declare. + +`f.run` returns the command's output; a step's `.summary` on `f.agent` is the CLI's final text. `artifacts` is always empty in 2.0.16 — populating it from what the agent actually wrote lands in the next release ([flows#449](https://github.com/AgentWorkforce/flows/pull/449)). + ## Verification Every step's exit code is checked automatically. On top of that: +- **`exit_code`** — a deterministic step's process exit code (always checked; declaring it is only meaningful on `deterministic` steps). - **`output_contains`** — an opt-in string match against the step's output. Fast to write, good for a smoke check. -- **`json_schema`** on an `llm` step — the model's reply must parse and validate against the schema before anything downstream sees it. +- **`json_schema`** on an `llm` step — the model's reply must parse and validate against the schema before anything downstream sees it. A schema of `{}` or `true` accepts anything and is flagged `vacuous_gate`. +- **Named gates** — `references_input`, `regex_match`, `word_count_bounds`, and `subprocess_gate` (run a shell command against the output; exit `0` passes). Each lowers to a deterministic gate step in the same journal, so a resume replays the recorded verdict instead of re-judging. + +In TypeScript the same named gates attach postfix: `f.agent('review', {…}).gate({ type: 'subprocess_gate', command: 'test -s review.md' })`. A callback gate, `.gate((r) => r.artifacts.includes('review.md'))`, is refused in 2.0.16 (`unsupported_gate`: a closure can't be journaled); the next release runs it after the step and journals the verdict ([flows#449](https://github.com/AgentWorkforce/flows/pull/449)). A step that fails its check is recorded as `verification_failed`, with exactly which check failed. The agent insisting it went fine doesn't override that. @@ -130,7 +143,7 @@ An `agent` step declares what it's allowed to touch — `fileGlobs`, `accessPres - **`inspect`** — the next attempt starts inside the dirty workspace, with the failed attempt's trajectory tail injected as context, and decides whether to continue or redo. - **`manual`** — parks the run as `needs_human` with a diff of the pinned revision against whatever's actually there. -`maxIterations` caps how many attempts a step gets before one of those three outcomes has to happen. A run-level `budget` (`maxTokensIn`, `maxTokensOut`, `maxDollars`) caps the whole flow the same way, declared once and enforced by the kernel instead of tracked by hand. +`maxIterations` caps how many attempts a step gets before one of those three outcomes has to happen. A run-level `budget` caps the whole flow the same way, declared once and enforced by the kernel instead of tracked by hand. Write it as `"$5/run"` or `"$20/day"` (dollars, priced from a frozen per-model table), or as `{ tokens?, dollars?, wallclock? }` — `{ dollars: 5, wallclock: "45m" }` bounds both. Tokens and wallclock apply to every step; dollars apply to steps whose model has a frozen price. A Codex step, which picks its own model, is reported as `budget_unmetered` under a dollar budget and runs; it counts toward tokens and wallclock but cannot cross the dollar limit. Crossing a limit lets the running step finish and refuses the next one with `budget_exceeded`. ## Next diff --git a/web/content/docs/relayflows/cli.mdx b/web/content/docs/relayflows/cli.mdx index 9d9a90e..78d8795 100644 --- a/web/content/docs/relayflows/cli.mdx +++ b/web/content/docs/relayflows/cli.mdx @@ -1,50 +1,82 @@ --- title: 'CLI' -description: 'Check a spec, run it, resume it, watch it — the flows command surface.' +description: 'Check a flow, run it locally or in Cloud, resume it, deploy it as a listener — the flows command surface.' --- +The full surface of `relayflows` 2.0.16, as `flows --help` prints it: + ```text -flows check [--json] -flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir ] -flows run --cloud [--json] [--wait] +flows check [--watch] [--json] +flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir ] [--local-agent] [--reuse-from ] flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir ] [--local-agent] --input -flows resume [--json] [--no-spawn] [--no-observer-link] [--data-dir ] +flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--data-dir ] [--local-agent] +flows replay [--allow-human-influenced] [--json] [--data-dir ] [--at ] + +flows run --cloud [--json] [--wait] [--sync-code] +flows run --cloud [--json] [--wait] [--sync-code] --input +flows sync [--json] [--dir ] +flows deploy --repo --on [:key=value,...] [--on ...] --approver [--agents claude[,codex]] [--name ] [--draft] [--json] +flows deployments [--json] +flows undeploy [--json] + +flows build [--out ] +flows build --verify +flows deploy @sha256: --to +flows run @sha256: [--bucket ] [--data-dir ] [--json] +flows add + +flows serve-webhook --data-dir --port

[--allow [,]] flows tick start --schedule-id --interval-ms [--epoch-ms ] [--max-catch-up ] [--poll-interval-ms ] [--data-dir

] flows hn-monitor start [--data-dir ] [--poll-interval-ms ] flows observer [--data-dir ] ``` +Every verb refuses an unknown or duplicated flag with `REFUSED [invalid_invocation]` and the usage above, before anything runs. + ## Check ```bash flows check my-flow.flow.yaml +flows check my-flow.flow.ts +flows check --watch my-flow.flow.ts # re-check on every save ``` -Validates the spec: schema, step graph, verification blocks, and whether every declared CLI actually exists and is authenticated. Nothing runs and nothing is spawned. This is the same preflight a run does before its first step, exposed on its own so a broken spec fails in CI instead of at minute 27 of a real run. +Validates the flow: schema, step graph, verification blocks, helper and trigger declarations, and whether every declared CLI actually exists and is authenticated (a real `claude -p --model …` / `codex exec …` probe, not a version check). Nothing runs and nothing is spawned — `check` never opens the daemon socket, and refuses `--data-dir` for that reason. It prints one `GATE` line per verification and one `RESOLVED` line per `llm`/`agent` step naming the CLI and model and where they came from (step, named agent, flow, or `flows.json`). -`check` takes a declarative `flow.yaml` or `spec.json` — it validates data, so an authored `.flow.ts` file isn't a valid argument here. A TypeScript flow gets the same preflight automatically, run inline at the top of `flows run`. +An authored `.flow.ts` is checked too: its declared surface — agents, helpers, `tools`, triggers, the `use:` graph — is preflighted; its control flow is not, so a check pass says the declarations are sound, not that the body is. The same preflight runs again at the top of `flows run`. + +If a `flows.json` is in scope and declares `models`, every model a step names must be on that list (`model_unknown` otherwise); without a `models` list the model is only probed for access. ## Run ```bash flows run my-flow.flow.ts --local-agent --input '{}' # TypeScript flow, local CLI flows run workflow.yaml # YAML/spec.json flow -flows run --cloud --wait workflow.yaml # dispatch to the hosted engine, block for the result +flows run --cloud --wait workflow.yaml # hosted engine, block for the result +flows run --cloud --wait my-flow.flow.ts --input '{}' # hosted authored flow ``` -`--local-agent` runs each declared CLI (`claude`, `codex`, …) using whatever login it already has on your machine — no separate credentials to configure. It's what makes an authored TypeScript flow's `agent` steps runnable at all. +An authored `.flow.ts` requires `--input`: an existing JSON file, otherwise inline JSON (up to 1 MiB), handed to the body as its second argument. + +`--local-agent` attaches the SDK's agent and LLM workers to the daemon, running each declared CLI (`claude`, `codex`, or a custom wrapper) with whatever login it already has on your machine. Without it, the first `llm` or `agent` step parks the run (exit `3`) until a worker is attached. Only stream-only agent steps run this way; a step that declares a `workspace` surface needs a worker holding its revision pins. -By default `run` spawns a daemon for the run if one isn't already up. `--no-spawn` (or `FLOWS_NO_SPAWN=1` for a whole environment) asserts a daemon is already present instead — the lever CI uses to fail loudly on a missing daemon rather than silently start one. +By default `run` spawns a daemon for the data directory if one isn't already up. `--no-spawn` (or `FLOWS_NO_SPAWN=1` for a whole environment) asserts a daemon is already present instead — the lever CI uses to fail loudly on a missing daemon rather than silently start one. `--data-dir ` points at the journal's storage directory; it defaults to `.relayflowd` in the current directory. `--reuse-from ` (YAML only) reuses completed steps of an earlier run instead of re-executing them; the report says how many were reused. -`--data-dir ` points at the journal's storage directory; it defaults to `.relayflowd` in the current project. +Deterministic steps run in the **daemon's** working directory and environment — the directory you first ran `flows run` from — not in the flow file's directory. A `./script` in a step command is resolved and probed there. -## Resume +## Cloud + +`flows run --cloud` submits the flow to hosted infrastructure; the receipt prints `ACCEPTED ` and, with `--wait`, the validated completion. `--sync-code` uploads the invoking directory first so the hosted run executes inside your working tree, and `flows sync ` brings the run's file changes back as a patch applied to your checkout, uncommitted. `flows deploy --repo … --on …` turns a flow into a Cloud listener that launches one run per matching ticket; `flows deployments` lists them and `flows undeploy` removes one. See [Cloud](/docs/relayflows/cloud) for the details and credentials. + +## Resume and replay ```bash flows resume +flows resume --local-agent # an authored run started with --local-agent needs it again +flows replay --at # read-only reconstruction up to a step ``` -Picks a run back up from its journal. Completed steps aren't re-executed; only the work that never finished, or never got a recorded outcome, runs again. +`resume` picks a run back up from its journal. Completed steps aren't re-executed; only the work that never finished, or never got a recorded outcome, runs again. A `run_not_found` target exits `2`; any other failure exits `1`, because the journal may already have changed. `replay` walks the journal without executing anything. Both take `--allow-human-influenced` to proceed past a run whose journal records a human intervention. ## Observer links @@ -52,17 +84,32 @@ Picks a run back up from its journal. Completed steps aren't re-executed; only t flows observer ``` -Mints a read-only link for watching a run's activity in real time. Minting is best-effort — a failed mint never fails the run — so `--no-observer-link` is there for anyone who wants to skip it outright (CI, for instance). +Mints a read-only link for watching a run's activity in real time, using the workspace key from `RELAYCAST_WORKSPACE_KEY` or your `agent-relay cloud login`. `run` and `resume` mint one automatically and print it after the `RUN` line; minting is best-effort and never fails the run. `--no-observer-link` or `FLOWS_NO_OBSERVER=1` skips it (CI, for instance). -## Triggers +## Triggers and event sources ```bash +flows serve-webhook --data-dir .relayflowd --port 8787 --allow slack,github flows tick start --schedule-id daily --interval-ms 86400000 spec.json flows hn-monitor start spec.json ``` -`tick` is a durable schedule. Each interval gets a unique ID, so a restart can't fire the same interval twice, and `--max-catch-up` bounds how many missed intervals get replayed if the process was down. `hn-monitor` is a narrower, named trigger built the same way. Both report whether they're still alive, instead of quietly going dark for weeks without anyone noticing. +`serve-webhook` receives provider events (`POST /providers/slack`, `POST /providers/github`, or a generic `POST /`) and writes them to the daemon's inbox for a bound trigger spec. `tick` is a durable local schedule: each interval gets a unique ID, so a restart can't fire the same interval twice, and `--max-catch-up` bounds how many missed intervals get replayed if the process was down. `hn-monitor` is a narrower, named source built the same way. All of them are processes that must stay running; hosted schedules for v2 flows are not available yet. + +## Bundles + +```bash +flows build my-flow.flow.ts --out dist +flows deploy my-flow@sha256: --to file:///srv/flows +flows run my-flow@sha256: --bucket file:///srv/flows +``` + +`build` seals a flow into a content-addressed bundle (canonical spec, compiled TypeScript with pinned dependencies, preflight declaration); the digest form of `deploy` copies it into a file bucket, and `run @sha256:…` executes it from there with no checkout. Only `file://` buckets are supported today. ## `--json` -`check`, `run`, and `resume` accept `--json` for structured output instead of the human-readable progress line — the shape a CI step or another program should read, not the terminal renderer. `tick start`, `hn-monitor start`, and `observer` don't take it. +`check`, `run`, `resume`, `replay`, `build`, `sync`, `deploy` (listener form), `deployments`, and `undeploy` accept `--json` for a single machine-readable object on stdout instead of the human-readable lines — the shape a CI step or another program should read. `tick start`, `hn-monitor start`, `serve-webhook`, and `observer` don't take it. + +## Exit codes + +`0` completed with `success` (a deliberate `done("declined")` also exits `0`, with a `DECLINED` diagnostic) · `1` failed with a declared reason, or the outcome is unknown · `2` refused before any journal write · `3` parked. [Reliability](/docs/relayflows/reliability) has the contract. diff --git a/web/content/docs/relayflows/cloud.mdx b/web/content/docs/relayflows/cloud.mdx index eeedf4f..c58e16d 100644 --- a/web/content/docs/relayflows/cloud.mdx +++ b/web/content/docs/relayflows/cloud.mdx @@ -1,57 +1,96 @@ --- title: 'Cloud' -description: 'Run a flow on hosted infrastructure instead of your own machine — the same verification, the same journal.' +description: 'Run a flow on hosted infrastructure, sync your working tree up and the changes back, or deploy it as a listener — the same verification, the same journal.' --- -A flow doesn't need your laptop up to run. `flows run --cloud` submits the exact same spec to hosted infrastructure. The Rust runtime still executes and verifies every step; only where it runs is different. +A flow doesn't need your laptop up to run. `flows run --cloud` submits the same flow to hosted infrastructure; the Rust runtime still executes and verifies every step, only where it runs is different. `flows deploy` goes one step further and leaves the flow listening for tickets. ## Run it ```bash flows run --cloud examples/ship-feature.flow.yaml flows run --cloud --wait --json examples/ship-feature.flow.yaml +flows run --cloud --wait ship-feature.flow.ts --input '{"ticket":"ENG-42"}' ``` Without `--wait`, exit `0` means the run was **accepted**. You get a run ID back and the run continues on its own; it hasn't completed yet. With `--wait`, exit `0` means Cloud reported the run `completed` with a validated `success` reason. A failed or cancelled run, or an observation failure, exits `1`. -## From the SDK +An authored `.flow.ts` takes `--input` exactly as a local run does (an existing JSON file, otherwise inline JSON) and travels as one self-contained source: `use:` dependencies and sibling imports are refused before any HTTP call, since the hosted runner loads the flow from the request, not from a checkout. -```ts -import { runInCloud, waitForCloudFlowRun } from '@relayflows/sdk'; +## Bring your working tree -const accepted = await runInCloud( - { path: './flow.yaml' }, - { token: process.env.FLOWS_CLOUD_TOKEN } -); -console.log(accepted.runId); // accepted, not completed +```bash +cd my-repo +flows run --cloud --sync-code --wait review.flow.ts --input '{"pr": 7}' +flows sync # apply the run's changes to this checkout +``` -const finished = await waitForCloudFlowRun(accepted.runId); -console.log(finished.status, finished.completionReason); +`--sync-code` uploads the invoking directory before submission, so every `f.run` and `f.agent` in the hosted run executes inside your tree. In a Git checkout the upload is exactly `git ls-files --cached --others --exclude-standard`: `.gitignore` governs, untracked files ride along, `.git` and `node_modules` never do, executable bits survive. A checkout whose `git` fails for any other reason is refused rather than uploaded without its ignore rules. The limit is 256 MiB uncompressed. + +`flows sync ` fetches the diff the run left behind and applies it with `git apply` after a `--check` pass — a conflict leaves your tree untouched (`patch_conflict`, exit `2`). It lands **uncommitted**, with every touched path listed, so you review it with `git diff` before keeping any of it. + +## Deploy it as a listener + +```bash +flows deploy software-factory.flow.ts \ + --repo acme/api \ + --on linear:team=ENG \ + --approver you +flows deployments +flows undeploy ``` -`FLOWS_CLOUD_TOKEN` needs a Cloud API token; the `flows` CLI never logs in for you. Provision one from the [Cloud dashboard](https://cloud.agentrelay.com/dashboard/settings), the same place you'd create a deployment token or a Relayfile agent key: +`flows deploy ` is the command-line form of the [Flows onboarding](/flows) deploy step. Cloud stores the source and creates a listener whose watch rules match the chosen ticket sources; there is no webhook to register — your workspace's GitHub App installation or Slack, Linear, Jira, or Shortcut connection is the ingress. Each matching ticket launches one run of the stored source, cloned from `--repo`'s default branch onto a fresh `relayflow/-` branch, with `{ approver, issue, event }` as the flow's input. The flow must therefore be the default body, `flow(name, header, async (f, input) => …)`, reading `input.issue`. + +`--on [:key=value,…]` takes `github` (`repository`, `labels`, `contains`), `slack` (`channel`, `contains`), `linear` (`team`, `contains`), `jira` (`project`, `contains`) or `shortcut` (`workspace`, `contains`), each at most once; a GitHub source without `repository` is scoped to `--repo`. `--agents` names the coding-agent harnesses the flow uses (default `claude`); activation checks their credentials are connected and refuses with `flow_model_not_connected` otherwise. `--draft` saves without activating. Refusals are named — `flow_repository_not_connected`, `flow_name_taken`, … — rather than reported as a bare status. + + + Today a GitHub listener wakes on `issues.opened` and `issues.labeled`. + Pull-request events (`--on github:events=pull_request`) and hosted + schedules for v2 flows are in progress and not in 2.0.16; the local + `flows tick` schedule still works in the meantime. + + +## Credentials + +Every hosted verb resolves its credential the same way: the SDK's `token` option, then `FLOWS_CLOUD_TOKEN`, then the `agent-relay cloud login` store (`~/.agentworkforce/relay/cloud-auth.json`). Once you've run `agent-relay cloud login`, no environment variable is needed; the login's API URL is also the default base, so a login against one deployment never sends its token to another, and an expired login is refused with the re-login remedy instead of sent. + +For CI, or a host with no browser, provision a token from the [Cloud dashboard](https://cloud.agentrelay.com/dashboard/settings): 1. Open **Settings → Workspace API tokens**. 2. Under **Purpose**, pick **Flows Cloud token**. 3. Name it, set an expiry, and create it. -4. Copy the one-time `cld_at_...` value — it's shown once — and export it, replacing the placeholder below with what you copied: +4. Copy the one-time `cld_at_...` value — it's shown once — and export it: ```bash export FLOWS_CLOUD_TOKEN="cld_at_paste-your-copied-token-here" ``` -This mints a token scoped to exactly `workflow:invoke:read`, `workflow:invoke:write`, `workflow:runs:read`, and `workflow:logs:read` — nothing else — which covers both submitting and polling a run on this page. It's a workspace-level credential: anyone with dashboard access to the workspace can create or revoke one, no CLI or special access needed. +That token is scoped to `workflow:invoke:read`, `workflow:invoke:write`, `workflow:runs:read`, and `workflow:logs:read`, which covers `run --cloud`, `--sync-code`, and `sync`. Deploying, listing, and removing listeners need the interactive `cli:auth` credential the login produces; with a deployment token the CLI says so (`session_required`) rather than failing opaquely. `FLOWS_CLOUD_URL` points at a different Cloud deployment if you're not using the default. + +## From the SDK + +```ts +import { runInCloud, waitForCloudFlowRun } from '@relayflows/sdk'; + +const accepted = await runInCloud( + { path: './flow.yaml' }, + { token: process.env.FLOWS_CLOUD_TOKEN } +); +console.log(accepted.runId); // accepted, not completed -Prefer a CLI instead (headless/SSH host, no browser to click through)? `agent-relay cloud login` (`--device` for headless) plus `agent-relay cloud session --json --reveal-token` gets you a token too, though that one carries a broader `cli:auth` scope rather than the four scopes above — Cloud's workflow-run endpoints accept either. +const finished = await waitForCloudFlowRun(accepted.runId); +console.log(finished.status, 'completionReason' in finished ? finished.completionReason : undefined); +``` -`FLOWS_CLOUD_URL` points at a different Cloud deployment if you're not using the default. +`runInCloud` also takes `input` for an authored flow and `syncCode: { root }` to upload a tree; `deployToCloud`, `listCloudDeployments`, `undeployFromCloud`, `downloadCloudPatch`, and `applyCloudPatch` back the corresponding verbs. ## What's different about a cloud run -- **Only declarative flows.** `--cloud` accepts `flow.yaml` / `spec.json`, not an authored `.flow.ts` file — the SDK refuses those before making an HTTP call rather than uploading code that can't run there. -- **Accepted isn't completed.** An interruption after submission but before the acceptance receipt reports `admission_unknown` — the server may already have started a non-idempotent run. Don't resubmit blindly; check the run ID you already have first. -- **One-hour execution ceiling.** Cloud's current executor has a one-hour deadline per run, independent of any local timeout you'd otherwise configure. -- **You get the completion reason, not the step-by-step journal.** It's validated against the same closed vocabulary as a local run, but this API doesn't expose per-step detail yet. +- **Accepted isn't completed.** An interruption during the submission request itself reports `admission_unknown` — the server may already have started a non-idempotent run; check the run ID before resubmitting. An interruption earlier, while preparing or uploading a synced tree, reports `submission_aborted`: nothing was admitted and rerunning is safe. +- **One-hour execution ceiling.** Cloud's executor has a one-hour deadline per run, independent of any local timeout you'd otherwise configure. +- **You get the completion reason, not the step-by-step journal.** It's validated against the same closed vocabulary as a local run, but this API doesn't expose per-step output. +- **Pinned runtime.** Cloud runs a pinned build of the flows runtime, promoted separately from the npm release, so a brand-new CLI feature can be published before the hosted runtime that honours it inside the sandbox. ## Next diff --git a/web/content/docs/relayflows/introduction.mdx b/web/content/docs/relayflows/introduction.mdx index dab46ec..14fd5ea 100644 --- a/web/content/docs/relayflows/introduction.mdx +++ b/web/content/docs/relayflows/introduction.mdx @@ -52,9 +52,9 @@ steps: ``` -`f.run` (a `deterministic` step in YAML) executes a shell command and returns its output. `f.agent` (an `agent` step) hands a task to a coding agent and returns a summary rather than a raw transcript. `f.done` finishes the run with one reason from a closed set: `success`, `step_failed`, `canceled`, or `budget_exceeded`. There's no fifth option to guess about. +`f.run` (a `deterministic` step in YAML) executes a shell command and returns its output. `f.agent` (an `agent` step) hands a task to a coding agent and returns a summary rather than a raw transcript. `f.done` finishes the run with one verdict from a closed set: `success`, `step_failed`, `needs_human`, or `declined`. There's no fifth option to guess about — `canceled` and `budget_exceeded` exist in the journal vocabulary, but only the kernel records them. -Both steps above name their own `cli` and `model` directly — `Ctx.agent`'s options are `{ task, workspace?, cli?, model? }`, matching the YAML step's fields ([flows#310](https://github.com/AgentWorkforce/flows/issues/310)). Neither is required: omit `cli` and a step falls back to its flow's `cli`, then the nearest `flows.json`'s project-wide default; omit `model` and it just runs whatever model its resolved CLI defaults to, since there's no flow or project default for that. Without a `cli` at step, flow, or project level, `flows run` refuses before anything is journaled — exit `2`, `REFUSED [invalid_spec]`. +Both steps above name their own `cli` and `model` directly — `f.agent`'s options are `{ task, workspace?, cli?, model? }`, matching the YAML step's fields ([flows#310](https://github.com/AgentWorkforce/flows/issues/310)). Neither is required: omit `cli` and a step falls back to its flow's `cli`, then the nearest `flows.json`'s project-wide default; omit `model` and Claude runs its adapter default (`claude-opus-5`) while Codex picks its own. Without a `cli` at step, flow, or project level, `flows check` and `flows run` refuse before anything is journaled — exit `2`, `REFUSED [cli_unresolved]`. If that `flows.json` also lists `models`, every model a step names must be on the list. ## Verification diff --git a/web/content/docs/relayflows/memory-and-integrations.mdx b/web/content/docs/relayflows/memory-and-integrations.mdx index 53c3d32..ae1caa6 100644 --- a/web/content/docs/relayflows/memory-and-integrations.mdx +++ b/web/content/docs/relayflows/memory-and-integrations.mdx @@ -10,15 +10,17 @@ A flow doesn't carry its own database or its own Slack client. Two existing syst A step declares what it needs from prior runs, and whatever comes back is charged to that step's own budget, itemized in its own journal entry. There's no shared pool it draws from: ```ts -export default flow('triage', async (f) => { - const priorArt = await f.memory.recall('similar bugs in the billing module'); - const why = await f.memory.why('fix the billing race condition'); +import { flow } from '@relayflows/surface'; + +export default flow('triage', { memory: { script: true } }, async (f) => { + const priorArt = await f.memory.recall('similar bugs in the billing module'); // HistoryEntry[] + const why = await f.memory.why('fix the billing race condition'); // TrajectoryEntry[] const fix = await f.agent('fixer', { - task: `Fix the race condition.\n\nRelevant history:\n${priorArt}`, + task: `Fix the race condition.\n\nRelevant history:\n${priorArt.map((e) => e.prompt).join('\n')}`, }); - await f.memory.learn(`Root cause: ${fix.summary}`); + // learn() is declared but refuses in 2.0.16, pending the journal-backed write. f.done('success'); }); ``` @@ -28,10 +30,11 @@ Under the hood this is [Relayloop](/docs/loop)'s `ai-hist pack` — the same sea What this feature is judged on is retrieval quality, not the plumbing: an agent visibly avoiding a mistake a prior run already made, citation - included. Today's shipped provider is a fixed stub while real `ai-hist - pack` retrieval lands. The `memory.injected` journal entry and its - per-step budget accounting are real regardless of which provider answers - the query. + included. In 2.0.16, `recall` and `why` read the local `ai-hist` SQLite + database (`AI_HIST_DB` overrides the default path) with no journal step; + `learn` and `memory: { agent: true }` refuse until the journal-backed + write lands. The `memory: { script: true }` header makes `flows check` + verify the database is reachable before the run starts. ## Integrations @@ -39,7 +42,9 @@ Under the hood this is [Relayloop](/docs/loop)'s `ai-hist pack` — the same sea Reaching Slack, GitHub, or Linear from a flow doesn't mean writing a provider SDK call and hoping the token scope is right. [Relayfile](/docs/file) mounts each provider as a directory a flow reads and writes: ```ts -export default flow('release-note', async (f) => { +import { flow } from '@relayflows/surface'; + +export default flow('release-note', { tools: { slack: true } }, async (f) => { const diff = await f.run('git diff main'); const note = await f.llm`One-line release note for: ${diff}`; @@ -48,7 +53,7 @@ export default flow('release-note', async (f) => { }); ``` -`f.slack`, `f.github`, `f.linear`, and every other generated helper compile to a write against Relayfile's [adapter catalog](/docs/file/adapters-and-providers) — the same 50 providers Relayfile ships today, each turned into a flow-native verb. The receipt a helper returns *is* the journaled effect record, so if a step retries and posts the same Slack message twice, the second write gets deduped instead of double-sending. +`f.slack`, `f.github`, `f.linear`, and every other generated helper compile to a write against Relayfile's [adapter catalog](/docs/file/adapters-and-providers) — the same 50 providers Relayfile ships today, each turned into a flow-native verb. The receipt a helper returns *is* the journaled effect record, so if a step retries and posts the same Slack message twice, the second write gets deduped instead of double-sending. A helper needs a Relayfile mount for its provider (Cloud provides one for connected providers); locally, `flows check` refuses `helper_slack.mount_required` until one exists, and `RELAYFLOWS_SLACK_MOCK=1` records the effect instead of sending it. ## Next diff --git a/web/content/docs/relayflows/multi-agent.mdx b/web/content/docs/relayflows/multi-agent.mdx index 63dc8e7..ed24c49 100644 --- a/web/content/docs/relayflows/multi-agent.mdx +++ b/web/content/docs/relayflows/multi-agent.mdx @@ -11,7 +11,7 @@ A flow isn't locked to one CLI or one model. Name as many agents as the task nee version: '0.1.0' name: ship-feature agents: - planner: { cli: claude, model: claude-opus-4-6 } + planner: { cli: claude, model: claude-opus-5 } implementer: { cli: codex, model: gpt-5.6-codex } reviewer: { cli: claude, model: claude-sonnet-4-6 } @@ -61,14 +61,14 @@ export default flow('ship-feature', async (f) => { }); const ok = await f.human(`Ship this?\n${plan.summary}`, { to: 'khaliq' }); - if (!ok) return f.done('canceled'); + if (!ok) return f.done('declined'); // a decision not to act, not a kernel cancellation const pr = await f.dispatch('garden/implement', plan); f.done('success'); }); ``` -`f.human` parks the run on a durable wait — nothing sits there blocking a thread, and the wait survives a restart exactly like a crash mid-step does. `f.dispatch` hands the plan to a named child flow and returns its typed result, so one large flow decomposes into several smaller ones instead of a script that tries to do everything. +`f.human` parks the run on a durable wait — nothing sits there blocking a thread, and the wait survives a restart exactly like a crash mid-step does. A "no" ends the run with `declined`, the verdict for choosing not to proceed; `canceled` is reserved for the kernel. `f.dispatch` hands the plan to a named child flow and returns its typed result, so one large flow decomposes into several smaller ones instead of a script that tries to do everything. ## Next diff --git a/web/content/docs/relayflows/quickstart.mdx b/web/content/docs/relayflows/quickstart.mdx index 3fc315d..fc19a9a 100644 --- a/web/content/docs/relayflows/quickstart.mdx +++ b/web/content/docs/relayflows/quickstart.mdx @@ -62,6 +62,8 @@ npx flows run hello.flow.ts --input '{}' npx flows check hello.flow.yaml # preflight only — nothing runs npx flows run hello.flow.yaml ``` + +`flows check` works on the TypeScript flow too — `npx flows check hello.flow.ts` — and `flows run` repeats the same preflight before its first step. This is a real, captured run: @@ -77,7 +79,7 @@ RUN 01M26HYJNS6A4K5Q64FH6D0SG9 completed (2 steps) completionReason: success ``` -Every step's outcome is in the journal now, keyed by that run ID. `flows check` only takes a `flow.yaml` or `spec.json` — a TypeScript flow gets the same preflight for free, as the first thing `flows run` does. +Every step's outcome is in the journal now, keyed by that run ID. ## 4. If it gets interrupted diff --git a/web/content/docs/relayflows/reliability.mdx b/web/content/docs/relayflows/reliability.mdx index a1e53f1..d146978 100644 --- a/web/content/docs/relayflows/reliability.mdx +++ b/web/content/docs/relayflows/reliability.mdx @@ -8,9 +8,9 @@ This page covers what makes a run's completion trustworthy: not an agent's own r ## A run's exit code is a contract ```text -0 completionReason: success +0 completionReason: success (a deliberate done("declined") also exits 0, with a DECLINED diagnostic) 1 a declared completionReason failure, or an unknown outcome from a transport/runtime/protocol error -2 refused before any journal write: invalid input, failed preflight, unreachable daemon +2 refused before any journal write: invalid input, failed preflight, unreachable daemon, a run_not_found resume target 3 parked — waiting on an llm/agent worker or a needs_human recovery wait ``` @@ -27,7 +27,7 @@ Step: success · verification_failed · retries_exhausted · lease_expired · Run: success · step_failed · canceled · budget_exceeded ``` -Handle every value in that list and there's no case left over to surprise you later. A step that fails verification is recorded as `verification_failed`, with exactly which check failed. The agent reporting that it went fine doesn't change the outcome. +An authored TypeScript body declares its verdict with `f.done`: `success`, `step_failed`, `needs_human` (parks), or `declined` (a deliberate decision not to act; the kernel records `success` and the CLI adds a `DECLINED` diagnostic). `canceled` and `budget_exceeded` are the kernel's to record, never a body's. Handle every value in those lists and there's no case left over to surprise you later. A step that fails verification is recorded as `verification_failed`, with exactly which check failed. The agent reporting that it went fine doesn't change the outcome. ## Crashing mid-step diff --git a/web/lib/flow-onboarding.ts b/web/lib/flow-onboarding.ts index af5eebb..a1abd5e 100644 --- a/web/lib/flow-onboarding.ts +++ b/web/lib/flow-onboarding.ts @@ -97,9 +97,11 @@ export function cloudConnectionsHref(draft: FactoryDraft, handoffId: string, jou } export function factoryCodeSections(draft: FactoryDraft, target: 'cloud' | 'local' = 'cloud') { - // A wall-clock budget for every target: a dollar budget makes the Relayflow - // runtime refuse any agent step without a frozen-priced model (Codex has no - // default model), which stops the run before the first Codex step. + // A wall-clock budget for every target. Since relayflows 2.0.13 a dollar + // budget no longer refuses a model-less Codex step (AgentWorkforce/flows#421); + // such a step runs unmetered, so a dollar cap cannot bound it. Wall-clock is + // enforced on every step regardless of pricing, which is why it stays the + // default here; `{ dollars, wallclock }` together is also valid. const budget = '{ wallclock: "1h" }'; if (!draft.sources.length) return [{ id: 'empty', code: `import { flow } from "@relayflows/surface"; From 556c0ac08c61a5842fc88809722837b2a4319def Mon Sep 17 00:00:00 2001 From: Khaliq Date: Thu, 17 Sep 2026 15:58:50 -0700 Subject: [PATCH 2/7] docs(relayflows): TypeScript-first for every flow sample; YAML behind the language switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convention: every flow example is shown in TypeScript, with the YAML form reachable only through the docs' Language dropdown (``, first fence `typescript TypeScript`, second `yaml YAML`). - introduction.mdx §Verification: bare YAML sample wrapped; TS twin uses postfix `.gate({ type: 'regex_match' })`. - multi-agent.mdx §Named agents: bare YAML sample wrapped; TS twin spreads per-agent `{ cli, model }` objects, the idiom until an `agents:` header exists (flows#300); the Note rewritten around that. - build.mdx §Permissions and recovery: YAML step fragment wrapped as a full flow with a TS twin that states what the TS body gets by default. - quickstart.mdx, build.mdx: the "YAML is canonical" / "YAML describes…" sentences no longer lead; TypeScript is named the default first. All three new TS samples pass `tsc` and `flows check` against 2.0.16. Co-Authored-By: Claude Opus 5 (1M context) --- web/content/docs/relayflows/build.mdx | 67 ++++++++++++++------ web/content/docs/relayflows/introduction.mdx | 23 ++++++- web/content/docs/relayflows/multi-agent.mdx | 45 ++++++++++--- web/content/docs/relayflows/quickstart.mdx | 2 +- 4 files changed, 102 insertions(+), 35 deletions(-) diff --git a/web/content/docs/relayflows/build.mdx b/web/content/docs/relayflows/build.mdx index 46773f9..bc14a7b 100644 --- a/web/content/docs/relayflows/build.mdx +++ b/web/content/docs/relayflows/build.mdx @@ -21,7 +21,7 @@ The rest of this page is what the skill encodes — worth reading so you can tel ## Two ways to author the same thing -**YAML** describes a fixed set of steps and their dependencies as data, so `flows check` or a CI gate can read and validate it without running anything. **TypeScript** calls the same primitives imperatively, as ordinary code. Both compile down to the same journal. +**TypeScript** calls the primitives imperatively, as ordinary code. **YAML** describes the same fixed set of steps and their dependencies as data, so `flows check` or a CI gate can read and validate it without running anything. Both compile down to the same journal; every sample on these pages is shown in TypeScript, with the YAML form behind the language switch. ```typescript TypeScript @@ -117,27 +117,52 @@ A step that fails its check is recorded as `verification_failed`, with exactly w ## Permissions and recovery -An `agent` step declares what it's allowed to touch — `fileGlobs`, `accessPreset` — and what happens if it crashes mid-edit: - -```yaml -- id: edit - type: agent - dependsOn: [greet] - instruction: 'Produce the hello artifact through the deterministic test stub.' - recoveryMode: reset - maxIterations: 2 - surfaces: - workspace: - - surface: repo - streams: - - stream: agent-notes - permissions: - fileGlobs: ['artifacts/**'] - accessPreset: readwrite - verification: - type: output_contains - value: agent-ok +An `agent` step declares what it's allowed to touch — `fileGlobs`, `accessPreset` — and what happens if it crashes mid-edit. These are YAML step fields; the TypeScript body reaches the same step through `f.dispatch`, or leaves recovery to the kernel's default (`reset`): + + +```typescript TypeScript +import { flow } from '@relayflows/surface'; + +export default flow('hello-agent', async (f) => { + await f.run('printf hello'); + + // No recoveryMode/permissions here: a crashed attempt resets to the + // pinned revision (the default), and the workspace is the daemon's cwd. + await f.agent('edit', { + task: 'Produce the hello artifact and print agent-ok when it exists.', + cli: 'claude', + }).gate({ type: 'regex_match', pattern: 'agent-ok' }); + + f.done('success'); +}); ``` +```yaml YAML +version: '0.1.0' +name: hello-agent +steps: + - id: greet + type: deterministic + command: 'printf hello' + + - id: edit + type: agent + dependsOn: [greet] + instruction: 'Produce the hello artifact through the deterministic test stub.' + recoveryMode: reset + maxIterations: 2 + surfaces: + workspace: + - surface: repo + streams: + - stream: agent-notes + permissions: + fileGlobs: ['artifacts/**'] + accessPreset: readwrite + verification: + type: output_contains + value: agent-ok +``` + - **`reset`** (the default) — the next attempt starts fresh from the pinned workspace revision, with no half-finished edit left behind. - **`inspect`** — the next attempt starts inside the dirty workspace, with the failed attempt's trajectory tail injected as context, and decides whether to continue or redo. diff --git a/web/content/docs/relayflows/introduction.mdx b/web/content/docs/relayflows/introduction.mdx index 14fd5ea..9322424 100644 --- a/web/content/docs/relayflows/introduction.mdx +++ b/web/content/docs/relayflows/introduction.mdx @@ -60,7 +60,23 @@ Both steps above name their own `cli` and `model` directly — `f.agent`'s optio An agent rarely fails by crashing. It fails by returning something plausible and wrong, which a plain retry-on-error never catches. So a step's completion is decided by a check the kernel runs against the real output, not by the agent's own account of what happened. -```yaml + +```typescript TypeScript +import { flow } from '@relayflows/surface'; + +export default flow('hello-agent', async (f) => { + await f.run('printf hello') + .gate({ type: 'regex_match', pattern: '^hello' }); + + await f.agent('edit', { + task: 'Produce the hello artifact and print agent-ok when it exists.', + cli: 'claude', + }).gate({ type: 'regex_match', pattern: 'agent-ok' }); + + f.done('success'); +}); +``` +```yaml YAML version: '0.1.0' name: hello-agent steps: @@ -84,10 +100,11 @@ steps: type: output_contains value: agent-ok ``` + -The exit code is always checked. On top of that, `output_contains` adds an opt-in string match, and an `llm` step can require its output to match a `json_schema` instead. A step that fails verification is recorded as `verification_failed` in the journal — it doesn't get to report success on its own say. +The exit code is always checked. On top of that, `output_contains` (YAML) or a postfix `.gate({ … })` (TypeScript) adds an opt-in check against the real output, and an `llm` step can require its output to match a `json_schema` instead. A step that fails verification is recorded as `verification_failed` in the journal — it doesn't get to report success on its own say. -`recoveryMode: reset` governs what happens if this step dies mid-edit: the next attempt starts over from the pinned workspace revision instead of picking up whatever half-finished state got left behind. `permissions` limits what that attempt is allowed to touch while it runs. +`recoveryMode: reset` governs what happens if this step dies mid-edit: the next attempt starts over from the pinned workspace revision instead of picking up whatever half-finished state got left behind. `permissions` limits what that attempt is allowed to touch while it runs. Both are declared per step in YAML today; see [Build a flow](/docs/relayflows/build#permissions-and-recovery). ## Resumable by construction diff --git a/web/content/docs/relayflows/multi-agent.mdx b/web/content/docs/relayflows/multi-agent.mdx index ed24c49..3f779c0 100644 --- a/web/content/docs/relayflows/multi-agent.mdx +++ b/web/content/docs/relayflows/multi-agent.mdx @@ -7,7 +7,35 @@ A flow isn't locked to one CLI or one model. Name as many agents as the task nee ## Named agents -```yaml + +```typescript TypeScript +import { flow } from '@relayflows/surface'; + +// Each call names its own cli and model; a plain object keeps them in one place. +const planner = { cli: 'claude', model: 'claude-opus-5' }; +const implementer = { cli: 'codex', model: 'gpt-5.6-codex' }; +const reviewer = { cli: 'claude', model: 'claude-sonnet-4-6' }; + +export default flow('ship-feature', async (f) => { + const plan = await f.agent('plan', { + ...planner, + task: 'Plan the implementation for: add OAuth2 support', + }); + + await f.agent('implement', { + ...implementer, + task: `Implement this plan:\n${plan.summary}`, + }); + + await f.agent('review', { + ...reviewer, + task: 'Review the diff for correctness and security. End with APPROVED or BLOCKED.', + }).gate({ type: 'regex_match', pattern: 'APPROVED' }); + + f.done('success'); +}); +``` +```yaml YAML version: '0.1.0' name: ship-feature agents: @@ -36,18 +64,15 @@ steps: type: output_contains value: 'APPROVED' ``` + -Each step's `agent:` selector resolves to that named `{ cli, model }` pair at compile time, from an explicit declaration. `flows check` also flags a named agent nobody ever selects, and one that a step overrides without using, so a stale declaration doesn't quietly rot in the spec. +In TypeScript every `f.agent` call names its own `cli` and `model` ([flows#310](https://github.com/AgentWorkforce/flows/issues/310)); the `name` argument labels the step in the journal. In YAML the `agents:` map declares each `{ cli, model }` pair once and a step's `agent:` selector resolves to it at compile time, and `flows check` flags a named agent nobody selects, or one a step overrides without using, so a stale declaration doesn't quietly rot in the spec. - The named-agent **map** (`agents:` plus a step's `agent:` selector, - reused across steps by name) is YAML/JSON authoring only today. In - TypeScript, `f.agent(name, options)` takes `{ task, workspace?, cli?, - model? }` ([flows#310](https://github.com/AgentWorkforce/flows/issues/310)) - — so a call can set its own `cli`/`model` directly, but the `name` - argument still just labels the call in the journal; there's no map to - select a declared pair from, or to reuse across multiple calls, the - way YAML's `agent:` selector does ([flows#300](https://github.com/AgentWorkforce/flows/issues/300)). + The reusable named-agent **map** is YAML/JSON authoring only today. + TypeScript has no `agents:` header to select a declared pair from + ([flows#300](https://github.com/AgentWorkforce/flows/issues/300)); an + ordinary object spread, as above, is the idiom until it does. ## Asking a human, then handing off diff --git a/web/content/docs/relayflows/quickstart.mdx b/web/content/docs/relayflows/quickstart.mdx index fc19a9a..927f3e2 100644 --- a/web/content/docs/relayflows/quickstart.mdx +++ b/web/content/docs/relayflows/quickstart.mdx @@ -50,7 +50,7 @@ steps: ``` -YAML is canonical — the compiler can check it without running anything. TypeScript is the power tool: the same primitives, called imperatively, with ordinary `if`/`for` control flow around each `await`. +TypeScript is the default way to write a flow: the same primitives, called imperatively, with ordinary `if`/`for` control flow around each `await`. The YAML form (pick it from the language switch) is the canonical data the compiler checks without running anything — the shape a CI gate or a generator emits. ## 3. Run it From 1d6a5c1cb93431a595ed807509f85156218804b6 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Thu, 17 Sep 2026 17:56:44 -0700 Subject: [PATCH 3/7] docs(relayflows): plain-spoken intro, add Cookbook page and nav entry Rewrites the introduction's opening in the same register as flows PR #453 ("Stop babysitting agents. Script them.") and adds a new Cookbook page linking every verified recipe in AgentWorkforce/flows-cookbook, plus a real "Deploy Flow" badge asset (the reused launch-agent badge always rendered "Launch Agent" regardless of alt text). Co-Authored-By: Claude Sonnet 5 --- web/content/docs/relayflows/cookbook.mdx | 62 ++++++++++++++++++++ web/content/docs/relayflows/introduction.mdx | 10 +++- web/lib/product-docs-nav.ts | 4 ++ web/public/deploy-flow_small.svg | 23 ++++++++ 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 web/content/docs/relayflows/cookbook.mdx create mode 100644 web/public/deploy-flow_small.svg diff --git a/web/content/docs/relayflows/cookbook.mdx b/web/content/docs/relayflows/cookbook.mdx new file mode 100644 index 0000000..3bca185 --- /dev/null +++ b/web/content/docs/relayflows/cookbook.mdx @@ -0,0 +1,62 @@ +--- +title: 'Cookbook' +description: 'Copy-paste flows for the automations people actually build first — verified end to end, not just typechecked.' +--- + +**We cook. Everybody eats.** [`AgentWorkforce/flows-cookbook`](https://github.com/AgentWorkforce/flows-cookbook) is a growing collection of single-file recipes — copy one, point it at your repo, run it locally or deploy it on our infrastructure at [agentrelay.com/flows](https://agentrelay.com/flows). Every recipe was run for real before it was published: see its README for the run link and exactly what was exercised. + +```bash +npm install -g relayflows +npm install --save-dev @relayflows/surface +``` + +## Getting started + + + + One step, one gate — confirms your setup works before you build on it. + + + Generate a message with an LLM, post it to Slack. + + + +## Automations + + + + Triage every open issue on a schedule — stale vs. needs-attention, not just "quiet for N days" — and post a Slack digest. + + + Same shape, for pull requests: ready-to-merge, needs-review, needs-author, or abandoned. + + + +## Review + + + + Reads a PR, fixes what's mechanical, and only says "ready for a human" when the agent, the tests, and GitHub all agree. + + + +## Software Factory + +An issue becomes a pull request: implementation agent → deterministic tests, run outside the agent → adversarial review agent that must write an explicit verdict → PR for a human. + +```bash +flows deploy software-factory/software-factory.flow.ts \ + --repo acme/api --on linear:team=ENG --approver you +``` + +[![Deploy Flow](https://agentrelay.com/deploy-flow_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows-cookbook%2Fblob%2Fmain%2Fsoftware-factory%2Fsoftware-factory.flow.ts&on=linear%3Ateam%3DENG) + +`--on` also takes `github:labels=agent`, `jira:project=OPS`, `shortcut:workspace=…` or `slack:channel=#eng`. `flows deployments` lists what's listening; `flows undeploy ` stops it. + +## Coming soon + +Four more recipes are real but currently blocked on upstream work in [flows](https://github.com/AgentWorkforce/flows) — the cookbook's README tracks each one against its blocking issue instead of quietly leaving it out: a two-sandbox dependency upgrade bot, a three-lens PR review pipeline, a research-then-fact-checked social post pipeline, and a multi-model research fan-out. + + + Browse every recipe, its verification evidence, and what's still blocked. + diff --git a/web/content/docs/relayflows/introduction.mdx b/web/content/docs/relayflows/introduction.mdx index 9322424..7ae0968 100644 --- a/web/content/docs/relayflows/introduction.mdx +++ b/web/content/docs/relayflows/introduction.mdx @@ -1,9 +1,11 @@ --- title: 'Flows' -description: 'A deterministic script over agentic primitives: steps you can inspect, verify, and resume.' +description: 'Stop babysitting agents. Script them — steps you can inspect, verify, and resume.' --- -Flows turns a coding-agent task into steps you can inspect and verify. A flow combines shell commands, model calls, and coding agents with a journal that records what each step did and why it completed. +**Stop babysitting agents. Script them.** + +Define complex sequences of tasks for agents instead of hoping they follow the rules in your prompt. A flow combines shell commands, model calls, and coding agents with a journal that records what each step did and why it completed — predictable, auditable, and dependable. Every effect is journaled before it's treated as real. If a step's journal write fails, the step fails. There's no silent fallback and no "it probably worked." Flows makes that trade everywhere: less magic, more evidence. @@ -139,6 +141,10 @@ This is a real run, captured directly from the terminal. Every step carries a `c + + Don't start from a blank file — the cookbook has verified, copy-paste recipes for the automations people build first. + + See how far the ladder goes: named agents, cloud dispatch, memory, and integrations. diff --git a/web/lib/product-docs-nav.ts b/web/lib/product-docs-nav.ts index 554ba66..41d2095 100644 --- a/web/lib/product-docs-nav.ts +++ b/web/lib/product-docs-nav.ts @@ -241,6 +241,10 @@ export const relayflowsSection: ProductDocSection = { { title: 'Reliability', slug: 'reliability' }, ], }, + { + title: 'Cookbook', + items: [{ title: 'Recipes', slug: 'cookbook' }], + }, ], }; diff --git a/web/public/deploy-flow_small.svg b/web/public/deploy-flow_small.svg new file mode 100644 index 0000000..b8ec8a4 --- /dev/null +++ b/web/public/deploy-flow_small.svg @@ -0,0 +1,23 @@ + + Deploy Flow on Agent Relay + + + + + + + + + + + + + + + Deploy Flow + + + + + + From ced4c271d538f84fe7735d3b47833dd98675e602 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Fri, 18 Sep 2026 04:49:10 -0700 Subject: [PATCH 4/7] docs(relayflows): mark f.human / f.dispatch as not yet shipped Both verbs are declared in @relayflows/surface and pass `flows check`, but the 2.0.16 authored executor refuses them at runtime (`unsupported_verb`, authored-flow-executor.ts). They are being implemented on flows `feat/f-human` and land in the release after 2.0.17. A Note where each is introduced (introduction, build, multi-agent) says so and names the shipped human gate, `f.done('needs_human')` + `flows resume`. Sample shapes are unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- web/content/docs/relayflows/build.mdx | 9 ++++++++- web/content/docs/relayflows/introduction.mdx | 2 +- web/content/docs/relayflows/multi-agent.mdx | 7 +++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/web/content/docs/relayflows/build.mdx b/web/content/docs/relayflows/build.mdx index bc14a7b..bface21 100644 --- a/web/content/docs/relayflows/build.mdx +++ b/web/content/docs/relayflows/build.mdx @@ -98,6 +98,13 @@ interface Ctx { This is the whole kernel-level vocabulary a step body speaks: `run`, `llm`, `agent` for work, `human`, `dispatch`, `done` for control. `human` parks the run on a durable await instead of a blocking call — it's a wait the journal can survive a restart across. `dispatch` hands work to a named child flow and returns its typed result. + + `f.human` and `f.dispatch` are typed and preflighted in 2.0.16 but refused + at runtime with `unsupported_verb`; both land in the release after 2.0.17. + Until then the shipped human gate is `f.done('needs_human')`: the run parks + (exit `3`), a person acts, and `flows resume ` continues it. + + `done` takes one of four authored verdicts. `success` completes the run; `step_failed` says the flow's own checks did not pass (the adversarial review found problems, the tests went red) and exits `1`; `needs_human` parks the run (exit `3`); `declined` records a deliberate decision not to act on the input — a ticket that turned out not to be work — and exits `0` with a `DECLINED` diagnostic. `canceled` and `budget_exceeded` are in the `FlowCompletionReason` type but refused at runtime with `unsupported_completion`: they are kernel facts, recorded when the kernel cancels a run or exhausts its budget, not verdicts a body can declare. `f.run` returns the command's output; a step's `.summary` on `f.agent` is the CLI's final text. `artifacts` is always empty in 2.0.16 — populating it from what the agent actually wrote lands in the next release ([flows#449](https://github.com/AgentWorkforce/flows/pull/449)). @@ -117,7 +124,7 @@ A step that fails its check is recorded as `verification_failed`, with exactly w ## Permissions and recovery -An `agent` step declares what it's allowed to touch — `fileGlobs`, `accessPreset` — and what happens if it crashes mid-edit. These are YAML step fields; the TypeScript body reaches the same step through `f.dispatch`, or leaves recovery to the kernel's default (`reset`): +An `agent` step declares what it's allowed to touch — `fileGlobs`, `accessPreset` — and what happens if it crashes mid-edit. These are YAML step fields; a TypeScript body leaves recovery to the kernel's default (`reset`) today, and will reach a YAML step through `f.dispatch` once that verb ships: ```typescript TypeScript diff --git a/web/content/docs/relayflows/introduction.mdx b/web/content/docs/relayflows/introduction.mdx index 7ae0968..1db3a31 100644 --- a/web/content/docs/relayflows/introduction.mdx +++ b/web/content/docs/relayflows/introduction.mdx @@ -16,7 +16,7 @@ Every flow is built from the same small set of rungs, and you only climb as high 1. **`run`** — a shell command. No model involved. 2. **`llm`** — a bare model call. Prompt in, verified output out, no workspace, no tool use. 3. **`agent`** — a harnessed coding agent in a workspace. Returns an artifact, not just text. -4. **`human` / `dispatch` / `done`** — durable approval, handing work to a child flow, and a typed finish. +4. **`human` / `dispatch` / `done`** — durable approval, handing work to a child flow, and a typed finish. `human` and `dispatch` are declared in the surface and pass `flows check`, but the 2.0.16 runtime refuses them (`unsupported_verb`); they ship in the release after 2.0.17. Today the shipped human gate is `f.done('needs_human')`, which parks the run for a person to resume. ## Simple Example Flow diff --git a/web/content/docs/relayflows/multi-agent.mdx b/web/content/docs/relayflows/multi-agent.mdx index 3f779c0..692f98f 100644 --- a/web/content/docs/relayflows/multi-agent.mdx +++ b/web/content/docs/relayflows/multi-agent.mdx @@ -95,6 +95,13 @@ export default flow('ship-feature', async (f) => { `f.human` parks the run on a durable wait — nothing sits there blocking a thread, and the wait survives a restart exactly like a crash mid-step does. A "no" ends the run with `declined`, the verdict for choosing not to proceed; `canceled` is reserved for the kernel. `f.dispatch` hands the plan to a named child flow and returns its typed result, so one large flow decomposes into several smaller ones instead of a script that tries to do everything. + + Both verbs typecheck and pass `flows check` on 2.0.16 but are refused at + runtime (`unsupported_verb`); they ship in the release after 2.0.17. The + shipped human gate today is `f.done('needs_human')` — park, let a person + act, `flows resume`. + + ## Next From 39fa1419ff7a4334c5c1bdda29a72b1618c296ec Mon Sep 17 00:00:00 2001 From: kjgbot Date: Fri, 18 Sep 2026 05:16:06 -0700 Subject: [PATCH 5/7] docs(relayflows): fix flow-examples.ts gates/f.human, verify every remaining page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit flow-examples.ts (the public flows carousel): every example used the predicate-form .gate((r) => r.artifacts.includes(...)), which is refused at runtime (unsupported_gate). Replaced all 9 occurrences with the real subprocess_gate config form. 5 examples also called f.human, which throws unsupported_verb — annotated each call rather than silently leaving broken example code as the first thing a visitor sees about flow syntax. Ran a systematic, CLI-verified pass over every remaining relayflows docs page (cli.mdx, cloud.mdx, build.mdx, multi-agent.mdx, memory-and-integrations.mdx, reliability.mdx) rather than trust what was already written. reliability.mdx held up completely. Found and fixed real "typechecks but doesn't execute" gaps elsewhere: - f.human/f.dispatch presented as working in build.mdx and multi-agent.mdx (real: unsupported_verb on both, filed nowhere as clearly before now) - workspace: 'x: readonly' claimed to be the problem; a bare workspace value fails identically under --local-agent, so the annotation was never the actual cause - flows check claimed to verify ai-hist memory reachability; only flows run actually does - flows build doesn't work for any flow()-pattern authored TS flow, with no documented alternative shape - flows run --cloud --input is broken (HTTP 400 / misrouted into the declarative loader) in both cli.mdx and cloud.mdx's examples, independently reproduced in each Also: quickstart.mdx's flows check note was rendering as an unlabeled third CodeGroup tab instead of prose; the run ID in "if it gets interrupted" had no stated source; added agentrelay.com/cloud as the no-install path in introduction.mdx and quickstart.mdx; added Deploy Flow badges to every cookbook.mdx recipe; removed GitHub issue/PR hyperlinks site-wide per policy, keeping the reference as plain text. Co-Authored-By: Claude Sonnet 5 --- web/AGENTS.md | 9 ++++++++ web/CLAUDE.md | 1 + web/app/flows/flow-examples.ts | 20 ++++++++--------- web/content/docs/file/review-bot.mdx | 4 ++-- web/content/docs/relayflows/build.mdx | 22 ++++++++++++------- web/content/docs/relayflows/cli.mdx | 17 ++++++++++---- web/content/docs/relayflows/cloud.mdx | 6 +++-- web/content/docs/relayflows/cookbook.mdx | 8 +++++++ web/content/docs/relayflows/introduction.mdx | 6 ++++- .../relayflows/memory-and-integrations.mdx | 8 +++++-- web/content/docs/relayflows/multi-agent.mdx | 8 ++++--- web/content/docs/relayflows/quickstart.mdx | 10 ++++++--- web/next-env.d.ts | 3 ++- 13 files changed, 86 insertions(+), 36 deletions(-) create mode 100644 web/AGENTS.md create mode 100644 web/CLAUDE.md diff --git a/web/AGENTS.md b/web/AGENTS.md new file mode 100644 index 0000000..643577d --- /dev/null +++ b/web/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/web/CLAUDE.md b/web/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/web/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/web/app/flows/flow-examples.ts b/web/app/flows/flow-examples.ts index 860ffa7..2085ae8 100644 --- a/web/app/flows/flow-examples.ts +++ b/web/app/flows/flow-examples.ts @@ -5,7 +5,7 @@ export const flowExamples = [ "title": "Turn a ticket into a pull request.", "description": "Plan the change, write the code, and run the tests before opening a PR.", "filename": "software-factory.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n repo: string;\n ticket: string;\n};\n\nexport default flow(\n \"software-factory\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"planner\", {\n task: `${input.ticket} Write a plan to plan.md.`,\n }).gate((r) => r.artifacts.includes(\"plan.md\"));\n\n await f.agent(\"implementer\", {\n task: \"Read plan.md. Implement it on branch flow/fix. \" +\n \"Write the PR description to summary.md.\",\n }).gate((r) => r.artifacts.includes(\"summary.md\"));\n\n // The tests run outside the agent.\n // The agent cannot lie about the exit code.\n await f.run(\"git checkout flow/fix && npm test\");\n\n await f.agent(\"reviewer\", {\n task: \"Review the diff against main. \" +\n \"Write review.passed only if ready for a PR.\",\n }).gate((r) => r.artifacts.includes(\"review.passed\"));\n\n // Deterministic step, not an agent decision.\n const [owner, repo] = input.repo.split(\"/\");\n await f.github.createPullRequest({\n owner, repo,\n title: input.ticket,\n head: \"flow/fix\",\n base: \"main\",\n body: await f.run(\"cat summary.md\"),\n });\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n repo: string;\n ticket: string;\n};\n\nexport default flow(\n \"software-factory\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"planner\", {\n task: `${input.ticket} Write a plan to plan.md.`,\n }).gate({ type: \"subprocess_gate\", command: `test -s plan.md` });\n\n await f.agent(\"implementer\", {\n task: \"Read plan.md. Implement it on branch flow/fix. \" +\n \"Write the PR description to summary.md.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s summary.md` });\n\n // The tests run outside the agent.\n // The agent cannot lie about the exit code.\n await f.run(\"git checkout flow/fix && npm test\");\n\n await f.agent(\"reviewer\", {\n task: \"Review the diff against main. \" +\n \"Write review.passed only if ready for a PR.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s review.passed` });\n\n // Deterministic step, not an agent decision.\n const [owner, repo] = input.repo.split(\"/\");\n await f.github.createPullRequest({\n owner, repo,\n title: input.ticket,\n head: \"flow/fix\",\n base: \"main\",\n body: await f.run(\"cat summary.md\"),\n });\n f.done(\"success\");\n },\n);" }, { "id": "pr-review", @@ -13,7 +13,7 @@ export const flowExamples = [ "title": "Give every PR a second opinion.", "description": "Review a diff for security, correctness, and performance in parallel. Then reconcile the findings.", "filename": "pr-review.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\nexport default flow(\n \"pr-review\",\n { budget: \"$5/run\" },\n async (f) => {\n const diff = await f.run(\"git diff main...HEAD\");\n const lenses = [\"security\", \"correctness\", \"performance\"];\n\n await Promise.all(lenses.map((lens) =>\n f.agent(`${lens}-reviewer`, {\n task: `Review this diff for ${lens}: ${diff}. ` +\n `Write findings to review/${lens}.md.`,\n workspace: \"review/: readwrite\",\n }).gate((r) =>\n r.artifacts.includes(`review/${lens}.md`)\n )\n ));\n\n // Resolve disagreements between reviewers.\n await f.agent(\"reconciler\", {\n task: \"Read the three reviews in review/. \" +\n \"Resolve disagreements, flag unresolved issues, \" +\n \"and write review/consensus.md.\",\n workspace: \"review/: readwrite\",\n }).gate((r) =>\n r.artifacts.includes(\"review/consensus.md\")\n );\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\nexport default flow(\n \"pr-review\",\n { budget: \"$5/run\" },\n async (f) => {\n const diff = await f.run(\"git diff main...HEAD\");\n const lenses = [\"security\", \"correctness\", \"performance\"];\n\n await Promise.all(lenses.map((lens) =>\n f.agent(`${lens}-reviewer`, {\n task: `Review this diff for ${lens}: ${diff}. ` +\n `Write findings to review/${lens}.md.`,\n workspace: \"review/: readwrite\",\n }).gate({ type: \"subprocess_gate\", command: `test -s review/${lens}.md` })\n ));\n\n // Resolve disagreements between reviewers.\n await f.agent(\"reconciler\", {\n task: \"Read the three reviews in review/. \" +\n \"Resolve disagreements, flag unresolved issues, \" +\n \"and write review/consensus.md.\",\n workspace: \"review/: readwrite\",\n }).gate({ type: \"subprocess_gate\", command: `test -s review/consensus.md` });\n f.done(\"success\");\n },\n);" }, { "id": "support-triage", @@ -21,7 +21,7 @@ export const flowExamples = [ "title": "Draft the reply. Let a human decide.", "description": "Draft a response to a support thread, get approval, and reply in Slack.", "filename": "support-triage.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n message: string;\n channel: string;\n threadTs: string;\n approver: string;\n};\n\nexport default flow(\n \"support-triage\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Classify this request: ${input.message}. ` +\n \"Write the category and urgency to triage.md.\",\n }).gate((r) => r.artifacts.includes(\"triage.md\"));\n\n await f.agent(\"writer\", {\n task: `Read triage.md and draft a reply to: ` +\n `${input.message}. Write only the reply to reply.md.`,\n }).gate((r) => r.artifacts.includes(\"reply.md\"));\n\n const reply = await f.run(\"cat reply.md\");\n const approved = await f.human(\n `Send this reply?\\n\\n${reply}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n // Send only after a human approves.\n await f.slack.reply(input.channel, input.threadTs, reply);\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n message: string;\n channel: string;\n threadTs: string;\n approver: string;\n};\n\nexport default flow(\n \"support-triage\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Classify this request: ${input.message}. ` +\n \"Write the category and urgency to triage.md.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s triage.md` });\n\n await f.agent(\"writer\", {\n task: `Read triage.md and draft a reply to: ` +\n `${input.message}. Write only the reply to reply.md.`,\n }).gate({ type: \"subprocess_gate\", command: `test -s reply.md` });\n\n const reply = await f.run(\"cat reply.md\");\n // f.human is declared but not yet executed by the SDK (flows#400) \u2014 this\n // shows the intended approval gate, not a runnable one, until it lands.\n const approved = await f.human(\n `Send this reply?\\n\\n${reply}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n // Send only after a human approves.\n await f.slack.reply(input.channel, input.threadTs, reply);\n f.done(\"success\");\n },\n);" }, { "id": "content-pipeline", @@ -29,7 +29,7 @@ export const flowExamples = [ "title": "Put a fact-check between draft and publish.", "description": "Research, draft, and check a post before a human approves sharing it in Slack.", "filename": "content-pipeline.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n topic: string;\n channel: string;\n approver: string;\n};\n\nexport default flow(\n \"content-pipeline\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"researcher\", {\n task: `Research ${input.topic}. Cite your sources. ` +\n \"Write research.md.\",\n }).gate((r) => r.artifacts.includes(\"research.md\"));\n\n await f.agent(\"writer\", {\n task: \"Use research.md to write post.md. \" +\n \"Keep the source links with each claim.\",\n }).gate((r) => r.artifacts.includes(\"post.md\"));\n\n await f.agent(\"fact-checker\", {\n task: \"Check post.md against its sources. \" +\n \"Write checked.passed only if all claims hold up.\",\n }).gate((r) => r.artifacts.includes(\"checked.passed\"));\n\n const post = await f.run(\"cat post.md\");\n const approved = await f.human(\n `Publish this post?\\n\\n${post}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n await f.slack.post(input.channel, post);\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n topic: string;\n channel: string;\n approver: string;\n};\n\nexport default flow(\n \"content-pipeline\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"researcher\", {\n task: `Research ${input.topic}. Cite your sources. ` +\n \"Write research.md.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s research.md` });\n\n await f.agent(\"writer\", {\n task: \"Use research.md to write post.md. \" +\n \"Keep the source links with each claim.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s post.md` });\n\n await f.agent(\"fact-checker\", {\n task: \"Check post.md against its sources. \" +\n \"Write checked.passed only if all claims hold up.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s checked.passed` });\n\n const post = await f.run(\"cat post.md\");\n // f.human is declared but not yet executed by the SDK (flows#400) \u2014 this\n // shows the intended approval gate, not a runnable one, until it lands.\n const approved = await f.human(\n `Publish this post?\\n\\n${post}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n await f.slack.post(input.channel, post);\n f.done(\"success\");\n },\n);" }, { "id": "repo-migration", @@ -37,7 +37,7 @@ export const flowExamples = [ "title": "Make the same change across repositories.", "description": "Apply a migration in each local checkout, test it, and open a PR.", "filename": "repo-migration.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n repos: { path: string; slug: string }[];\n instructions: string;\n};\n\nexport default flow(\n \"repo-migration\",\n { budget: \"$5/run\" },\n async (f, input) => {\n for (const repo of input.repos) {\n // Quote the local checkout path for shell commands.\n const dir = \"'\" + repo.path.replaceAll(\"'\", \"'\\\"'\\\"'\") + \"'\";\n\n await f.agent(`migrate-${repo.slug}`, {\n cwd: repo.path,\n task: `${input.instructions} ` +\n \"Use branch flow/migration and commit the change. \" +\n \"Write migration.md with a summary.\",\n }).gate((r) => r.artifacts.includes(\"migration.md\"));\n\n await f.run(`cd ${dir} && npm test`);\n\n await f.agent(`review-${repo.slug}`, {\n cwd: repo.path,\n task: \"Review this migration against main. \" +\n \"Write review.passed only if it is ready.\",\n }).gate((r) => r.artifacts.includes(\"review.passed\"));\n\n // Each checkout opens its own pull request.\n await f.run(\n `cd ${dir} && gh pr create --base main ` +\n '--head flow/migration --title \"Apply migration\" ' +\n '--body-file migration.md'\n );\n }\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n repos: { path: string; slug: string }[];\n instructions: string;\n};\n\nexport default flow(\n \"repo-migration\",\n { budget: \"$5/run\" },\n async (f, input) => {\n for (const repo of input.repos) {\n // Quote the local checkout path for shell commands.\n const dir = \"'\" + repo.path.replaceAll(\"'\", \"'\\\"'\\\"'\") + \"'\";\n\n await f.agent(`migrate-${repo.slug}`, {\n cwd: repo.path,\n task: `${input.instructions} ` +\n \"Use branch flow/migration and commit the change. \" +\n \"Write migration.md with a summary.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s migration.md` });\n\n await f.run(`cd ${dir} && npm test`);\n\n await f.agent(`review-${repo.slug}`, {\n cwd: repo.path,\n task: \"Review this migration against main. \" +\n \"Write review.passed only if it is ready.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s review.passed` });\n\n // Each checkout opens its own pull request.\n await f.run(\n `cd ${dir} && gh pr create --base main ` +\n '--head flow/migration --title \"Apply migration\" ' +\n '--body-file migration.md'\n );\n }\n f.done(\"success\");\n },\n);" }, { "id": "voicemail-follow-up", @@ -45,7 +45,7 @@ export const flowExamples = [ "title": "Turn a voicemail into a callback brief.", "description": "Use a transcript to prepare the callback, then send the approved brief to a teammate.", "filename": "voicemail-follow-up.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n transcript: string;\n approver: string;\n callbackOwner: string;\n};\n\nexport default flow(\n \"voicemail-follow-up\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Read this voicemail: ${input.transcript}. ` +\n \"Identify urgency and the caller's request. \" +\n \"Write triage.md.\",\n }).gate((r) => r.artifacts.includes(\"triage.md\"));\n\n await f.agent(\"callback-writer\", {\n task: \"Read triage.md. Prepare a callback brief \" +\n \"with the questions we need to answer. \" +\n \"Write callback.md.\",\n }).gate((r) => r.artifacts.includes(\"callback.md\"));\n\n const brief = await f.run(\"cat callback.md\");\n const approved = await f.human(\n `Ready for a callback?\\n\\n${brief}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n await f.slack.dm(input.callbackOwner, brief);\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n transcript: string;\n approver: string;\n callbackOwner: string;\n};\n\nexport default flow(\n \"voicemail-follow-up\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Read this voicemail: ${input.transcript}. ` +\n \"Identify urgency and the caller's request. \" +\n \"Write triage.md.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s triage.md` });\n\n await f.agent(\"callback-writer\", {\n task: \"Read triage.md. Prepare a callback brief \" +\n \"with the questions we need to answer. \" +\n \"Write callback.md.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s callback.md` });\n\n const brief = await f.run(\"cat callback.md\");\n // f.human is declared but not yet executed by the SDK (flows#400) \u2014 this\n // shows the intended approval gate, not a runnable one, until it lands.\n const approved = await f.human(\n `Ready for a callback?\\n\\n${brief}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n await f.slack.dm(input.callbackOwner, brief);\n f.done(\"success\");\n },\n);" }, { "id": "research-report", @@ -53,7 +53,7 @@ export const flowExamples = [ "title": "Bring independent research into one report.", "description": "Give researchers different angles, combine their findings, and ask another agent to check the citations.", "filename": "research-report.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n question: string;\n};\n\nexport default flow(\n \"research-report\",\n { budget: \"$5/run\" },\n async (f, input) => {\n const angles = [\"technical\", \"commercial\", \"risks\"];\n\n await Promise.all(angles.map((angle) =>\n f.agent(`research-${angle}`, {\n task: `Research ${input.question} from a ${angle} ` +\n `angle. Cite sources in research/${angle}.md.`,\n workspace: \"research/: readwrite\",\n }).gate((r) =>\n r.artifacts.includes(`research/${angle}.md`)\n )\n ));\n\n await f.agent(\"editor\", {\n task: \"Read research/*.md. Combine the findings, \" +\n \"keep disagreements visible, and cite sources. \" +\n \"Write report.md.\",\n }).gate((r) => r.artifacts.includes(\"report.md\"));\n\n await f.agent(\"citation-reviewer\", {\n task: \"Open every source in report.md and check \" +\n \"the claims it supports. Write citations.passed \" +\n \"only when every citation checks out.\",\n }).gate((r) => r.artifacts.includes(\"citations.passed\"));\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n question: string;\n};\n\nexport default flow(\n \"research-report\",\n { budget: \"$5/run\" },\n async (f, input) => {\n const angles = [\"technical\", \"commercial\", \"risks\"];\n\n await Promise.all(angles.map((angle) =>\n f.agent(`research-${angle}`, {\n task: `Research ${input.question} from a ${angle} ` +\n `angle. Cite sources in research/${angle}.md.`,\n workspace: \"research/: readwrite\",\n }).gate({ type: \"subprocess_gate\", command: `test -s research/${angle}.md` })\n ));\n\n await f.agent(\"editor\", {\n task: \"Read research/*.md. Combine the findings, \" +\n \"keep disagreements visible, and cite sources. \" +\n \"Write report.md.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s report.md` });\n\n await f.agent(\"citation-reviewer\", {\n task: \"Open every source in report.md and check \" +\n \"the claims it supports. Write citations.passed \" +\n \"only when every citation checks out.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s citations.passed` });\n f.done(\"success\");\n },\n);" }, { "id": "redacted-summary", @@ -61,7 +61,7 @@ export const flowExamples = [ "title": "Choose what the summarizer gets to see.", "description": "Select the fields needed for the task before putting a record into the agent prompt.", "filename": "redacted-summary.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n record: { category: string; status: string; email: string };\n approver: string;\n};\n\nexport default flow(\n \"redacted-summary\",\n { budget: \"$5/run\" },\n async (f, input) => {\n // Select fields in code before building the prompt.\n // The email address is excluded from this input.\n const selected = {\n category: input.record.category,\n status: input.record.status,\n };\n\n await f.agent(\"summarizer\", {\n task: `Summarize this record: ` +\n `${JSON.stringify(selected)}. Write summary.md.`,\n }).gate((r) => r.artifacts.includes(\"summary.md\"));\n\n const summary = await f.run(\"cat summary.md\");\n const approved = await f.human(\n `Review this summary before sharing:\\n\\n${summary}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n record: { category: string; status: string; email: string };\n approver: string;\n};\n\nexport default flow(\n \"redacted-summary\",\n { budget: \"$5/run\" },\n async (f, input) => {\n // Select fields in code before building the prompt.\n // The email address is excluded from this input.\n const selected = {\n category: input.record.category,\n status: input.record.status,\n };\n\n await f.agent(\"summarizer\", {\n task: `Summarize this record: ` +\n `${JSON.stringify(selected)}. Write summary.md.`,\n }).gate({ type: \"subprocess_gate\", command: `test -s summary.md` });\n\n const summary = await f.run(\"cat summary.md\");\n // f.human is declared but not yet executed by the SDK (flows#400) \u2014 this\n // shows the intended approval gate, not a runnable one, until it lands.\n const approved = await f.human(\n `Review this summary before sharing:\\n\\n${summary}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n f.done(\"success\");\n },\n);" }, { "id": "ci-repair", @@ -77,7 +77,7 @@ export const flowExamples = [ "title": "Send each issue to the right specialist.", "description": "Route bugs to an implementer and documentation requests to a writer. Ask a human when the category is uncertain.", "filename": "issue-routing.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = { issue: string; approver: string };\n\nexport default flow(\"issue-routing\", async (f, input) => {\n const classification = await f.agent(\"router\", {\n cli: \"claude\",\n task: `Classify this issue: ${input.issue}. ` +\n \"Return only bugfix, docs, or uncertain as your summary. \" +\n \"Choose uncertain if the request is ambiguous.\",\n });\n let route = classification.summary.trim().toLowerCase();\n\n // Unknown output never silently selects a specialist.\n if (route !== \"bugfix\" && route !== \"docs\") {\n const approved = await f.human(\n `Needs manual triage: ${input.issue}\\n` +\n \"Route this to the bugfix specialist?\",\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n route = \"bugfix\";\n }\n\n if (route === \"bugfix\") {\n await f.agent(\"implementer\", {\n cli: \"codex\",\n task: `Fix this issue and add regression coverage: ${input.issue}`,\n });\n await f.run(\"npm test\");\n } else {\n await f.agent(\"docs-writer\", {\n cli: \"claude\",\n task: `Update the documentation for: ${input.issue}. ` +\n \"Check examples against the implementation.\",\n });\n }\n f.done(\"success\");\n});" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = { issue: string; approver: string };\n\nexport default flow(\"issue-routing\", async (f, input) => {\n const classification = await f.agent(\"router\", {\n cli: \"claude\",\n task: `Classify this issue: ${input.issue}. ` +\n \"Return only bugfix, docs, or uncertain as your summary. \" +\n \"Choose uncertain if the request is ambiguous.\",\n });\n let route = classification.summary.trim().toLowerCase();\n\n // Unknown output never silently selects a specialist.\n if (route !== \"bugfix\" && route !== \"docs\") {\n // f.human is declared but not yet executed by the SDK (flows#400) \u2014 this\n // shows the intended approval gate, not a runnable one, until it lands.\n const approved = await f.human(\n `Needs manual triage: ${input.issue}\\n` +\n \"Route this to the bugfix specialist?\",\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n route = \"bugfix\";\n }\n\n if (route === \"bugfix\") {\n await f.agent(\"implementer\", {\n cli: \"codex\",\n task: `Fix this issue and add regression coverage: ${input.issue}`,\n });\n await f.run(\"npm test\");\n } else {\n await f.agent(\"docs-writer\", {\n cli: \"claude\",\n task: `Update the documentation for: ${input.issue}. ` +\n \"Check examples against the implementation.\",\n });\n }\n f.done(\"success\");\n});" }, { "id": "competing-implementations", @@ -93,6 +93,6 @@ export const flowExamples = [ "title": "Turn competing theories into an evidence-backed diagnosis.", "description": "Investigate logs, recent changes, and dependencies in parallel. Challenge each theory and keep unresolved questions visible in the report.", "filename": "root-cause-investigation.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = { incident: string };\n\n// Supply sanitized logs and a local reproduction environment.\nexport default flow(\"root-cause-investigation\", async (f, input) => {\n const angles = [\"logs\", \"recent-changes\", \"dependencies\"];\n await f.run(\"mkdir -p investigation\");\n\n const hypotheses = await Promise.all(angles.map((angle) =>\n f.agent(`investigate-${angle}`, {\n task: `Investigate this incident through ${angle}: ` +\n `${input.incident}. Use the local evidence. ` +\n \"Do not change application code. Return a hypothesis, \" +\n \"supporting evidence, and a way to disprove it.\",\n })\n ));\n\n await Promise.all(hypotheses.map((hypothesis, i) =>\n f.agent(`verify-${angles[i]}`, {\n cli: \"codex\",\n task: `Try to disprove this theory: ${hypothesis.summary}. ` +\n \"Run focused checks in the local reproduction environment. \" +\n \"Do not change application code. Record commands, actual \" +\n \"results, and whether the theory is supported, rejected, \" +\n `or unresolved in investigation/${angles[i]}.md.`,\n }).gate((r) =>\n r.artifacts.includes(`investigation/${angles[i]}.md`)\n )\n ));\n\n await f.agent(\"incident-editor\", {\n cli: \"claude\",\n task: \"Read investigation/*.md. Write diagnosis.md with \" +\n \"the strongest supported explanation and its evidence. \" +\n \"Include rejected theories and unresolved questions. \" +\n \"If no cause is supported, say so and propose the next check.\",\n }).gate((r) => r.artifacts.includes(\"diagnosis.md\"));\n // Success means the investigation report is ready.\n f.done(\"success\");\n});" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = { incident: string };\n\n// Supply sanitized logs and a local reproduction environment.\nexport default flow(\"root-cause-investigation\", async (f, input) => {\n const angles = [\"logs\", \"recent-changes\", \"dependencies\"];\n await f.run(\"mkdir -p investigation\");\n\n const hypotheses = await Promise.all(angles.map((angle) =>\n f.agent(`investigate-${angle}`, {\n task: `Investigate this incident through ${angle}: ` +\n `${input.incident}. Use the local evidence. ` +\n \"Do not change application code. Return a hypothesis, \" +\n \"supporting evidence, and a way to disprove it.\",\n })\n ));\n\n await Promise.all(hypotheses.map((hypothesis, i) =>\n f.agent(`verify-${angles[i]}`, {\n cli: \"codex\",\n task: `Try to disprove this theory: ${hypothesis.summary}. ` +\n \"Run focused checks in the local reproduction environment. \" +\n \"Do not change application code. Record commands, actual \" +\n \"results, and whether the theory is supported, rejected, \" +\n `or unresolved in investigation/${angles[i]}.md.`,\n }).gate({ type: \"subprocess_gate\", command: `test -s investigation/${angles[i]}.md` })\n ));\n\n await f.agent(\"incident-editor\", {\n cli: \"claude\",\n task: \"Read investigation/*.md. Write diagnosis.md with \" +\n \"the strongest supported explanation and its evidence. \" +\n \"Include rejected theories and unresolved questions. \" +\n \"If no cause is supported, say so and propose the next check.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s diagnosis.md` });\n // Success means the investigation report is ready.\n f.done(\"success\");\n});" } ]; diff --git a/web/content/docs/file/review-bot.mdx b/web/content/docs/file/review-bot.mdx index 266c96d..cbeeb96 100644 --- a/web/content/docs/file/review-bot.mdx +++ b/web/content/docs/file/review-bot.mdx @@ -6,7 +6,7 @@ description: 'End-to-end: connect GitHub with one command, add the rest of your A PR review bot is the shape Relayfile fits best: several agents, several providers, one shared state. This guide runs the whole flow end to end — from an empty machine to a bot whose orchestrator and specialists all read the same PR, coordinate through files, and post the finished review back to GitHub without any of them holding a provider token. - Every command, path, and payload below was run against a live workspace on `relayfile` **0.10.53**. The review in step 7 was posted by writing the file this guide tells you to write — [`agentrelay.com#59` review 5112118049](https://github.com/AgentWorkforce/agentrelay.com/pull/59#pullrequestreview-5112118049). Where a path or flag is version-dependent, it says so. + Every command, path, and payload below was run against a live workspace on `relayfile` **0.10.53**. The review in step 7 was posted by writing the file this guide tells you to write — `agentrelay.com#59` review 5112118049. Where a path or flag is version-dependent, it says so. ## What you're building @@ -62,7 +62,7 @@ relayfile setup \ `--no-open` prints the login and connect URLs instead of launching a browser — always pass it when an agent (or CI) is driving the command, since a headless browser launch burns the OAuth state. -The local mirror it leaves behind is for *you*: somewhere to `ls`, `cat` and `grep` the tree and see exactly what your agents will see. It is not how a deployed bot reads the workspace — cloud sandboxes mount per run in [step 5](#5-give-every-sandbox-the-same-workspace), and short-lived functions skip the mount entirely. `--local-dir` is required here only because `--skip-mount` still prompts for a directory ([relayfile#461](https://github.com/AgentWorkforce/relayfile/pull/461)). +The local mirror it leaves behind is for *you*: somewhere to `ls`, `cat` and `grep` the tree and see exactly what your agents will see. It is not how a deployed bot reads the workspace — cloud sandboxes mount per run in [step 5](#5-give-every-sandbox-the-same-workspace), and short-lived functions skip the mount entirely. `--local-dir` is required here only because `--skip-mount` still prompts for a directory (relayfile#461). Both URLs are short-lived. A Cloud device code expires in minutes and the Nango connect URL has its own TTL, so complete them while the command is still waiting. If it exits first, re-run the same line — a re-run reuses the workspace and only opens a new connect flow when the provider isn't connected yet. diff --git a/web/content/docs/relayflows/build.mdx b/web/content/docs/relayflows/build.mdx index bc14a7b..54f6d66 100644 --- a/web/content/docs/relayflows/build.mdx +++ b/web/content/docs/relayflows/build.mdx @@ -68,14 +68,20 @@ steps: Reach for YAML when you want the whole flow readable at a glance and checkable in CI. Reach for TypeScript when a step's next move depends on what a previous one returned: an `f.human` approval, ordinary `if`/`for` logic, an `f.dispatch` to a child flow. -Both `edit` steps above name their own `cli` and `model` directly, the same way in either language ([flows#310](https://github.com/AgentWorkforce/flows/issues/310)). Neither is required in TypeScript: omit them and the step falls back to the flow's `cli`, then the nearest `flows.json`'s project-wide default, the same resolution [Introduction](/docs/relayflows/introduction#a-whole-flow) covers. +Both `edit` steps above name their own `cli` and `model` directly, the same way in either language (flows#310). Neither is required in TypeScript: omit them and the step falls back to the flow's `cli`, then the nearest `flows.json`'s project-wide default, the same resolution [Introduction](/docs/relayflows/introduction#a-whole-flow) covers. `recoveryMode`, `permissions`, and `surfaces`, the richer step fields covered further down, are YAML/JSON fields only today. TypeScript's - `f.agent` takes `{ task, workspace?, cli?, model? }`, and a `workspace` - with a `: readonly` / `: readwrite` annotation is refused, because nothing - enforces it yet. `budget` is available in both: the TypeScript form is the + `f.agent` takes `{ task, workspace?, cli?, model? }`, but under + `--local-agent` any `workspace` value is refused — not just an annotated + one. Verified: `workspace: 'repo: readonly'` and a bare `workspace: 'repo'` + both fail identically with `unsupported_workspace_permission: The local + agent worker accepts stream-only steps. Remove workspace or attach a + worker that holds its revision pins.` The local worker holds no revision + pins at all — omit `workspace` entirely for a step you run with + `--local-agent`; it's a real field only against a worker that supports it + (Cloud's). `budget` is available in both: the TypeScript form is the flow header, `flow('name', { budget: '$5/run' }, async (f) => …)`. @@ -96,11 +102,11 @@ interface Ctx { } ``` -This is the whole kernel-level vocabulary a step body speaks: `run`, `llm`, `agent` for work, `human`, `dispatch`, `done` for control. `human` parks the run on a durable await instead of a blocking call — it's a wait the journal can survive a restart across. `dispatch` hands work to a named child flow and returns its typed result. +This is the whole kernel-level vocabulary a step body speaks: `run`, `llm`, `agent` for work, `human`, `dispatch`, `done` for control. **`human` and `dispatch` are declared and typecheck, but neither runs in 2.0.16** — verified: a flow that reaches `f.human` fails with `unsupported_verb: the initial authored executor does not lower f.human`, and the same for `f.dispatch`. Once wired, `human` is meant to park the run on a durable await instead of a blocking call — a wait the journal can survive a restart across — and `dispatch` is meant to hand work to a named child flow and return its typed result. Until then, treat both as the intended shape, not something to ship on. `done` takes one of four authored verdicts. `success` completes the run; `step_failed` says the flow's own checks did not pass (the adversarial review found problems, the tests went red) and exits `1`; `needs_human` parks the run (exit `3`); `declined` records a deliberate decision not to act on the input — a ticket that turned out not to be work — and exits `0` with a `DECLINED` diagnostic. `canceled` and `budget_exceeded` are in the `FlowCompletionReason` type but refused at runtime with `unsupported_completion`: they are kernel facts, recorded when the kernel cancels a run or exhausts its budget, not verdicts a body can declare. -`f.run` returns the command's output; a step's `.summary` on `f.agent` is the CLI's final text. `artifacts` is always empty in 2.0.16 — populating it from what the agent actually wrote lands in the next release ([flows#449](https://github.com/AgentWorkforce/flows/pull/449)). +`f.run` returns the command's output; a step's `.summary` on `f.agent` is the CLI's final text. `artifacts` is always empty in 2.0.16 — populating it from what the agent actually wrote lands in the next release (flows#449). ## Verification @@ -111,13 +117,13 @@ Every step's exit code is checked automatically. On top of that: - **`json_schema`** on an `llm` step — the model's reply must parse and validate against the schema before anything downstream sees it. A schema of `{}` or `true` accepts anything and is flagged `vacuous_gate`. - **Named gates** — `references_input`, `regex_match`, `word_count_bounds`, and `subprocess_gate` (run a shell command against the output; exit `0` passes). Each lowers to a deterministic gate step in the same journal, so a resume replays the recorded verdict instead of re-judging. -In TypeScript the same named gates attach postfix: `f.agent('review', {…}).gate({ type: 'subprocess_gate', command: 'test -s review.md' })`. A callback gate, `.gate((r) => r.artifacts.includes('review.md'))`, is refused in 2.0.16 (`unsupported_gate`: a closure can't be journaled); the next release runs it after the step and journals the verdict ([flows#449](https://github.com/AgentWorkforce/flows/pull/449)). +In TypeScript the same named gates attach postfix: `f.agent('review', {…}).gate({ type: 'subprocess_gate', command: 'test -s review.md' })`. A callback gate, `.gate((r) => r.artifacts.includes('review.md'))`, is refused in 2.0.16 (`unsupported_gate`: a closure can't be journaled); the next release runs it after the step and journals the verdict (flows#449). A step that fails its check is recorded as `verification_failed`, with exactly which check failed. The agent insisting it went fine doesn't override that. ## Permissions and recovery -An `agent` step declares what it's allowed to touch — `fileGlobs`, `accessPreset` — and what happens if it crashes mid-edit. These are YAML step fields; the TypeScript body reaches the same step through `f.dispatch`, or leaves recovery to the kernel's default (`reset`): +An `agent` step declares what it's allowed to touch — `fileGlobs`, `accessPreset` — and what happens if it crashes mid-edit. These are YAML step fields with no TypeScript equivalent; `f.dispatch` is not a working way to reach them today (see above — it doesn't execute in 2.0.16). Author the step in YAML if you need `permissions`/`recoveryMode`, or leave recovery to the kernel's default (`reset`) in TypeScript: ```typescript TypeScript diff --git a/web/content/docs/relayflows/cli.mdx b/web/content/docs/relayflows/cli.mdx index 78d8795..0b7e153 100644 --- a/web/content/docs/relayflows/cli.mdx +++ b/web/content/docs/relayflows/cli.mdx @@ -41,7 +41,7 @@ flows check my-flow.flow.ts flows check --watch my-flow.flow.ts # re-check on every save ``` -Validates the flow: schema, step graph, verification blocks, helper and trigger declarations, and whether every declared CLI actually exists and is authenticated (a real `claude -p --model …` / `codex exec …` probe, not a version check). Nothing runs and nothing is spawned — `check` never opens the daemon socket, and refuses `--data-dir` for that reason. It prints one `GATE` line per verification and one `RESOLVED` line per `llm`/`agent` step naming the CLI and model and where they came from (step, named agent, flow, or `flows.json`). +Validates the flow: schema, step graph, verification blocks, helper and trigger declarations, and whether every declared CLI actually exists and is authenticated (a real `claude -p --model …` / `codex exec …` probe, not a version check — that probe is a real subprocess, the one thing `check` does spawn). No flow runs, no daemon starts, no worker attaches — `check` never opens the daemon socket, and refuses `--data-dir` for that reason. It prints one `GATE` line per verification and one `RESOLVED` line per `llm`/`agent` step naming the CLI and model and where they came from (step, named agent, flow, or `flows.json`). An authored `.flow.ts` is checked too: its declared surface — agents, helpers, `tools`, triggers, the `use:` graph — is preflighted; its control flow is not, so a check pass says the declarations are sound, not that the body is. The same preflight runs again at the top of `flows run`. @@ -53,10 +53,9 @@ If a `flows.json` is in scope and declares `models`, every model a step names mu flows run my-flow.flow.ts --local-agent --input '{}' # TypeScript flow, local CLI flows run workflow.yaml # YAML/spec.json flow flows run --cloud --wait workflow.yaml # hosted engine, block for the result -flows run --cloud --wait my-flow.flow.ts --input '{}' # hosted authored flow ``` -An authored `.flow.ts` requires `--input`: an existing JSON file, otherwise inline JSON (up to 1 MiB), handed to the body as its second argument. +An authored `.flow.ts` requires `--input`: an existing JSON file, otherwise inline JSON (up to 1 MiB), handed to the body as its second argument. **`flows run --cloud --input ...` is broken today** — verified for real, it fails immediately with `{"code":"http_error","message":"Cloud request failed with HTTP 400."}` against a flow that passes `flows check` cleanly, with or without `--wait`/`--sync-code` (flows#461). Only the YAML/`spec.json` form of `flows run --cloud` works; for an authored TypeScript flow, use `flows deploy` for a persistent listener instead. `--local-agent` attaches the SDK's agent and LLM workers to the daemon, running each declared CLI (`claude`, `codex`, or a custom wrapper) with whatever login it already has on your machine. Without it, the first `llm` or `agent` step parks the run (exit `3`) until a worker is attached. Only stream-only agent steps run this way; a step that declares a `workspace` surface needs a worker holding its revision pins. @@ -66,7 +65,7 @@ Deterministic steps run in the **daemon's** working directory and environment ## Cloud -`flows run --cloud` submits the flow to hosted infrastructure; the receipt prints `ACCEPTED ` and, with `--wait`, the validated completion. `--sync-code` uploads the invoking directory first so the hosted run executes inside your working tree, and `flows sync ` brings the run's file changes back as a patch applied to your checkout, uncommitted. `flows deploy --repo … --on …` turns a flow into a Cloud listener that launches one run per matching ticket; `flows deployments` lists them and `flows undeploy` removes one. See [Cloud](/docs/relayflows/cloud) for the details and credentials. +`flows run --cloud` submits the flow to hosted infrastructure; the receipt prints `ACCEPTED ` and, with `--wait`, the validated completion. `--sync-code` uploads the invoking directory first so the hosted run executes inside your working tree, and `flows sync ` brings the run's file changes back as a patch applied to your checkout, uncommitted. This one-shot form only works for a YAML/`spec.json` flow today — see **Run**, above, for the authored-TypeScript bug. `flows deploy --repo … --on …` turns a flow into a Cloud listener that launches one run per matching ticket, and that path works correctly regardless of language; `flows deployments` lists them and `flows undeploy` removes one. See [Cloud](/docs/relayflows/cloud) for the details and credentials. ## Resume and replay @@ -106,6 +105,16 @@ flows run my-flow@sha256: --bucket file:///srv/flows `build` seals a flow into a content-addressed bundle (canonical spec, compiled TypeScript with pinned dependencies, preflight declaration); the digest form of `deploy` copies it into a file bucket, and `run @sha256:…` executes it from there with no checkout. Only `file://` buckets are supported today. +**Verified for real, and this doesn't work the way the example above implies.** `flows build` on an ordinary authored `.flow.ts` — including a minimal, header-less one, and every flow shown elsewhere on this site — is refused: + +``` +REFUSED [bundle_invalid] TypeScript build requires Bun: ... +error: authored flow() requires an exported spec declaration for build-time preflight; +the body is not executed during build +``` + +`build` wants a separate exported `spec` declaration for build-time preflight, distinct from the `flow(name, header?, body)` default export every other example on this site uses; a flow with a non-empty header is refused outright (`unsupported authored flow header`). What exact shape `build` expects instead isn't documented anywhere in this site's docs or in `docs/SURFACE.md`. Until that's resolved, `flows build`/the digest-bundle path is unverified for a normal authored flow — treat this section as aspirational for TypeScript flows, not a working command to copy. + ## `--json` `check`, `run`, `resume`, `replay`, `build`, `sync`, `deploy` (listener form), `deployments`, and `undeploy` accept `--json` for a single machine-readable object on stdout instead of the human-readable lines — the shape a CI step or another program should read. `tick start`, `hn-monitor start`, `serve-webhook`, and `observer` don't take it. diff --git a/web/content/docs/relayflows/cloud.mdx b/web/content/docs/relayflows/cloud.mdx index c58e16d..28ad224 100644 --- a/web/content/docs/relayflows/cloud.mdx +++ b/web/content/docs/relayflows/cloud.mdx @@ -13,9 +13,9 @@ flows run --cloud --wait --json examples/ship-feature.flow.yaml flows run --cloud --wait ship-feature.flow.ts --input '{"ticket":"ENG-42"}' ``` -Without `--wait`, exit `0` means the run was **accepted**. You get a run ID back and the run continues on its own; it hasn't completed yet. With `--wait`, exit `0` means Cloud reported the run `completed` with a validated `success` reason. A failed or cancelled run, or an observation failure, exits `1`. +Without `--wait`, exit `0` means the run was **accepted**. You get a run ID back and the run continues on its own; it hasn't completed yet — verified for real against a YAML flow: `{"ok":true,"runId":"...","status":"pending",...}`, exit `0`. With `--wait`, exit `0` means Cloud reported the run `completed` with a validated `success` reason; a failed or cancelled run, or an observation failure, exits `1` — also verified: a `--wait` run that came back with an inconsistent terminal record (`invalid_response: Cloud terminal record lacks a valid, consistent run completionReason`) exited `1`, matching the documented observation-failure case. -An authored `.flow.ts` takes `--input` exactly as a local run does (an existing JSON file, otherwise inline JSON) and travels as one self-contained source: `use:` dependencies and sibling imports are refused before any HTTP call, since the hosted runner loads the flow from the request, not from a checkout. +An authored `.flow.ts` takes `--input` exactly as a local run does (an existing JSON file, otherwise inline JSON) and travels as one self-contained source: `use:` dependencies and sibling imports are refused before any HTTP call, since the hosted runner loads the flow from the request, not from a checkout. **Verified broken today**: the third command above — `flows run --cloud --input `, with or without `--wait` — fails immediately with `{"ok":false,"code":"http_error","message":"Cloud request failed with HTTP 400."}` against a real, `flows check`-passing authored flow. Only the declarative YAML/JSON form (the first two commands) actually submits a run. Use `flows deploy` for a TS flow today; the one-shot cloud path for authored TypeScript is tracked as broken (flows#461). ## Bring your working tree @@ -25,6 +25,8 @@ flows run --cloud --sync-code --wait review.flow.ts --input '{"pr": 7}' flows sync # apply the run's changes to this checkout ``` +**Verified broken today, same as above**: this exact command, run against a real git checkout with a `flows check`-passing `review.flow.ts`, fails with the same `http_error: HTTP 400` — `--sync-code` doesn't change the outcome, since the failure is in submitting a TS-authored flow to `run --cloud` at all, before code sync would even matter. `flows sync ` itself (and the `git apply`/`patch_conflict` mechanics below) could not be verified in this pass, since no TS `--cloud` run reached completion to sync from. + `--sync-code` uploads the invoking directory before submission, so every `f.run` and `f.agent` in the hosted run executes inside your tree. In a Git checkout the upload is exactly `git ls-files --cached --others --exclude-standard`: `.gitignore` governs, untracked files ride along, `.git` and `node_modules` never do, executable bits survive. A checkout whose `git` fails for any other reason is refused rather than uploaded without its ignore rules. The limit is 256 MiB uncompressed. `flows sync ` fetches the diff the run left behind and applies it with `git apply` after a `--check` pass — a conflict leaves your tree untouched (`patch_conflict`, exit `2`). It lands **uncommitted**, with every touched path listed, so you review it with `git diff` before keeping any of it. diff --git a/web/content/docs/relayflows/cookbook.mdx b/web/content/docs/relayflows/cookbook.mdx index 3bca185..27fa56f 100644 --- a/web/content/docs/relayflows/cookbook.mdx +++ b/web/content/docs/relayflows/cookbook.mdx @@ -21,6 +21,9 @@ npm install --save-dev @relayflows/surface +cloud-gates: [![Deploy Flow](https://agentrelay.com/deploy-flow_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows-cookbook%2Fblob%2Fmain%2Fcloud-gates%2Fcloud-gates.flow.yaml) +prospect-demo: [![Deploy Flow](https://agentrelay.com/deploy-flow_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows-cookbook%2Fblob%2Fmain%2Fprospect-demo%2Fdemo.flow.ts) + ## Automations @@ -32,6 +35,9 @@ npm install --save-dev @relayflows/surface +stale-issues: [![Deploy Flow](https://agentrelay.com/deploy-flow_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows-cookbook%2Fblob%2Fmain%2Fstale-issues%2Fstale-issues.flow.ts) +stale-prs: [![Deploy Flow](https://agentrelay.com/deploy-flow_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows-cookbook%2Fblob%2Fmain%2Fstale-prs%2Fstale-prs.flow.ts) + ## Review @@ -40,6 +46,8 @@ npm install --save-dev @relayflows/surface +[![Deploy Flow](https://agentrelay.com/deploy-flow_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows-cookbook%2Fblob%2Fmain%2Fpr-reviewer%2Fpr-reviewer.flow.ts) + ## Software Factory An issue becomes a pull request: implementation agent → deterministic tests, run outside the agent → adversarial review agent that must write an explicit verdict → PR for a human. diff --git a/web/content/docs/relayflows/introduction.mdx b/web/content/docs/relayflows/introduction.mdx index 7ae0968..9bed02f 100644 --- a/web/content/docs/relayflows/introduction.mdx +++ b/web/content/docs/relayflows/introduction.mdx @@ -9,6 +9,10 @@ Define complex sequences of tasks for agents instead of hoping they follow the r Every effect is journaled before it's treated as real. If a step's journal write fails, the step fails. There's no silent fallback and no "it probably worked." Flows makes that trade everywhere: less magic, more evidence. + + Don't want to install anything yet? Try Flows at agentrelay.com/cloud first — no local setup. + + ## The ladder Every flow is built from the same small set of rungs, and you only climb as high as the task needs: @@ -56,7 +60,7 @@ steps: `f.run` (a `deterministic` step in YAML) executes a shell command and returns its output. `f.agent` (an `agent` step) hands a task to a coding agent and returns a summary rather than a raw transcript. `f.done` finishes the run with one verdict from a closed set: `success`, `step_failed`, `needs_human`, or `declined`. There's no fifth option to guess about — `canceled` and `budget_exceeded` exist in the journal vocabulary, but only the kernel records them. -Both steps above name their own `cli` and `model` directly — `f.agent`'s options are `{ task, workspace?, cli?, model? }`, matching the YAML step's fields ([flows#310](https://github.com/AgentWorkforce/flows/issues/310)). Neither is required: omit `cli` and a step falls back to its flow's `cli`, then the nearest `flows.json`'s project-wide default; omit `model` and Claude runs its adapter default (`claude-opus-5`) while Codex picks its own. Without a `cli` at step, flow, or project level, `flows check` and `flows run` refuse before anything is journaled — exit `2`, `REFUSED [cli_unresolved]`. If that `flows.json` also lists `models`, every model a step names must be on the list. +Both steps above name their own `cli` and `model` directly — `f.agent`'s options are `{ task, workspace?, cli?, model? }`, matching the YAML step's fields (flows#310). Neither is required: omit `cli` and a step falls back to its flow's `cli`, then the nearest `flows.json`'s project-wide default; omit `model` and Claude runs its adapter default (`claude-opus-5`) while Codex picks its own. Without a `cli` at step, flow, or project level, `flows check` and `flows run` refuse before anything is journaled — exit `2`, `REFUSED [cli_unresolved]`. If that `flows.json` also lists `models`, every model a step names must be on the list. ## Verification diff --git a/web/content/docs/relayflows/memory-and-integrations.mdx b/web/content/docs/relayflows/memory-and-integrations.mdx index ae1caa6..43f207a 100644 --- a/web/content/docs/relayflows/memory-and-integrations.mdx +++ b/web/content/docs/relayflows/memory-and-integrations.mdx @@ -33,8 +33,12 @@ Under the hood this is [Relayloop](/docs/loop)'s `ai-hist pack` — the same sea included. In 2.0.16, `recall` and `why` read the local `ai-hist` SQLite database (`AI_HIST_DB` overrides the default path) with no journal step; `learn` and `memory: { agent: true }` refuse until the journal-backed - write lands. The `memory: { script: true }` header makes `flows check` - verify the database is reachable before the run starts. + write lands. The `memory: { script: true }` header makes the reachability + check happen — but verified for real, that check runs at `flows run` time, + not at `flows check`: a flow with an unreachable database passes `flows + check` cleanly and only fails when actually run, with `memory_unreachable: + Script memory requires the ai-hist Node SDK and a readable SQLite database + at AI_HIST_DB (or defaultDbPath()). Run ai-hist sync first.` ## Integrations diff --git a/web/content/docs/relayflows/multi-agent.mdx b/web/content/docs/relayflows/multi-agent.mdx index 3f779c0..298b7f8 100644 --- a/web/content/docs/relayflows/multi-agent.mdx +++ b/web/content/docs/relayflows/multi-agent.mdx @@ -66,17 +66,19 @@ steps: ``` -In TypeScript every `f.agent` call names its own `cli` and `model` ([flows#310](https://github.com/AgentWorkforce/flows/issues/310)); the `name` argument labels the step in the journal. In YAML the `agents:` map declares each `{ cli, model }` pair once and a step's `agent:` selector resolves to it at compile time, and `flows check` flags a named agent nobody selects, or one a step overrides without using, so a stale declaration doesn't quietly rot in the spec. +In TypeScript every `f.agent` call names its own `cli` and `model` (flows#310); the `name` argument labels the step in the journal. In YAML the `agents:` map declares each `{ cli, model }` pair once and a step's `agent:` selector resolves to it at compile time, and `flows check` flags a named agent nobody selects, or one a step overrides without using, so a stale declaration doesn't quietly rot in the spec. The reusable named-agent **map** is YAML/JSON authoring only today. TypeScript has no `agents:` header to select a declared pair from - ([flows#300](https://github.com/AgentWorkforce/flows/issues/300)); an + (flows#300); an ordinary object spread, as above, is the idiom until it does. ## Asking a human, then handing off +`Ctx` declares both verbs, and this typechecks and passes `flows check` — but neither runs yet in 2.0.16. Verified directly: `flows run` on a flow that reaches `f.human` fails with `unsupported_verb: the initial authored executor does not lower f.human`, and the same for `f.dispatch`. Treat the shape below as the intended design, not something to ship on today: + ```ts import { flow } from '@relayflows/surface'; @@ -93,7 +95,7 @@ export default flow('ship-feature', async (f) => { }); ``` -`f.human` parks the run on a durable wait — nothing sits there blocking a thread, and the wait survives a restart exactly like a crash mid-step does. A "no" ends the run with `declined`, the verdict for choosing not to proceed; `canceled` is reserved for the kernel. `f.dispatch` hands the plan to a named child flow and returns its typed result, so one large flow decomposes into several smaller ones instead of a script that tries to do everything. +Once wired, `f.human` is meant to park the run on a durable wait — nothing sits there blocking a thread, and the wait would survive a restart exactly like a crash mid-step does. A "no" ends the run with `declined`, the verdict for choosing not to proceed; `canceled` is reserved for the kernel. `f.dispatch` is meant to hand the plan to a named child flow and return its typed result, so one large flow decomposes into several smaller ones instead of a script that tries to do everything. Until then, do durable approval in YAML instead (`recoveryMode: manual` parks a step as `needs_human` with a diff for a person to resolve). ## Next diff --git a/web/content/docs/relayflows/quickstart.mdx b/web/content/docs/relayflows/quickstart.mdx index 927f3e2..31c8388 100644 --- a/web/content/docs/relayflows/quickstart.mdx +++ b/web/content/docs/relayflows/quickstart.mdx @@ -5,6 +5,8 @@ description: 'From zero to a verified, resumable run: write a flow, run it, and This walks you from a fresh install to a real run in a few commands, using nothing but the published `relayflows` CLI. No scaffolder, no project template. +Just want to see it work first, with nothing to install? Skip straight to [agentrelay.com/cloud](https://agentrelay.com/cloud) and run a flow in the browser — come back here when you're ready to author your own. + ## 1. Install @@ -59,12 +61,12 @@ TypeScript is the default way to write a flow: the same primitives, called imper npx flows run hello.flow.ts --input '{}' ``` ```bash YAML -npx flows check hello.flow.yaml # preflight only — nothing runs +npx flows check hello.flow.yaml # preflight only — no flow, daemon, or worker starts npx flows run hello.flow.yaml ``` + `flows check` works on the TypeScript flow too — `npx flows check hello.flow.ts` — and `flows run` repeats the same preflight before its first step. - This is a real, captured run: @@ -79,7 +81,7 @@ RUN 01M26HYJNS6A4K5Q64FH6D0SG9 completed (2 steps) completionReason: success ``` -Every step's outcome is in the journal now, keyed by that run ID. +Every step's outcome is in the journal now, keyed by that run ID — the `01M26JC2VPAGFVTCVWFT3GSCXQ` in the `RUN ... completed` line above. That's the ID everything below refers back to. ## 4. If it gets interrupted @@ -89,6 +91,8 @@ Every step's outcome is in the journal now, keyed by that run ID. flows resume 01M26JC2VPAGFVTCVWFT3GSCXQ ``` +That's the same run ID `flows run` printed in step 3 — copy it from your own terminal's `RUN ...` line (or, on a failure, from the `Inspect: flows replay ...` line the CLI prints). If you didn't capture it, there's no `flows list`-style lookup today: the journal for each run lives at `/runs/.sqlite3` (default data dir is `.relayflowd` in the directory you ran `flows` from), so `ls .relayflowd/runs/` recovers it by filename. + `resume` starts a fresh daemon if none is up, reads the journal from disk, and continues from the first step that never recorded a `completionReason` — the step already marked done doesn't run again. ## Next diff --git a/web/next-env.d.ts b/web/next-env.d.ts index 9edff1c..a419cbe 100644 --- a/web/next-env.d.ts +++ b/web/next-env.d.ts @@ -1,6 +1,7 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; +import "./.next/dev/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. From 03cde0590f320f84bfe0a53d7c365a47e405461b Mon Sep 17 00:00:00 2001 From: kjgbot Date: Fri, 18 Sep 2026 06:22:46 -0700 Subject: [PATCH 6/7] docs(relayflows): flows#461 is fixed in 2.0.17, not open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caught this right before merging the same content into the writing-relayflows skill: flows#461 (flows run --cloud broken for authored TS flows) was closed today by a real CLI fix. Re-verified directly against relayflows@2.0.17 rather than trust the tracker alone — flows run --cloud --wait --input, with and without --sync-code, now submits successfully and returns a real run id where 2.0.16 gave an immediate HTTP 400 or misrouted into the declarative loader. Root cause was a Surface version mismatch between Cloud and the CLI, badly reported. Co-Authored-By: Claude Sonnet 5 --- web/content/docs/relayflows/cli.mdx | 4 ++-- web/content/docs/relayflows/cloud.mdx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/content/docs/relayflows/cli.mdx b/web/content/docs/relayflows/cli.mdx index 0b7e153..0115d27 100644 --- a/web/content/docs/relayflows/cli.mdx +++ b/web/content/docs/relayflows/cli.mdx @@ -55,7 +55,7 @@ flows run workflow.yaml # YAML/spec.json flow flows run --cloud --wait workflow.yaml # hosted engine, block for the result ``` -An authored `.flow.ts` requires `--input`: an existing JSON file, otherwise inline JSON (up to 1 MiB), handed to the body as its second argument. **`flows run --cloud --input ...` is broken today** — verified for real, it fails immediately with `{"code":"http_error","message":"Cloud request failed with HTTP 400."}` against a flow that passes `flows check` cleanly, with or without `--wait`/`--sync-code` (flows#461). Only the YAML/`spec.json` form of `flows run --cloud` works; for an authored TypeScript flow, use `flows deploy` for a persistent listener instead. +An authored `.flow.ts` requires `--input`: an existing JSON file, otherwise inline JSON (up to 1 MiB), handed to the body as its second argument. `flows run --cloud --input ...` used to fail immediately with `{"code":"http_error","message":"Cloud request failed with HTTP 400."}` against any authored flow, on any released `2.0.16` or earlier CLI — a badly-reported Surface version mismatch between Cloud and the CLI (flows#461). Fixed in `2.0.17`: verified for real, it now submits successfully and returns a run ID. Update if you're still on `2.0.16` or earlier and see that error. `--local-agent` attaches the SDK's agent and LLM workers to the daemon, running each declared CLI (`claude`, `codex`, or a custom wrapper) with whatever login it already has on your machine. Without it, the first `llm` or `agent` step parks the run (exit `3`) until a worker is attached. Only stream-only agent steps run this way; a step that declares a `workspace` surface needs a worker holding its revision pins. @@ -65,7 +65,7 @@ Deterministic steps run in the **daemon's** working directory and environment ## Cloud -`flows run --cloud` submits the flow to hosted infrastructure; the receipt prints `ACCEPTED ` and, with `--wait`, the validated completion. `--sync-code` uploads the invoking directory first so the hosted run executes inside your working tree, and `flows sync ` brings the run's file changes back as a patch applied to your checkout, uncommitted. This one-shot form only works for a YAML/`spec.json` flow today — see **Run**, above, for the authored-TypeScript bug. `flows deploy --repo … --on …` turns a flow into a Cloud listener that launches one run per matching ticket, and that path works correctly regardless of language; `flows deployments` lists them and `flows undeploy` removes one. See [Cloud](/docs/relayflows/cloud) for the details and credentials. +`flows run --cloud` submits the flow to hosted infrastructure; the receipt prints `ACCEPTED ` and, with `--wait`, the validated completion. `--sync-code` uploads the invoking directory first so the hosted run executes inside your working tree, and `flows sync ` brings the run's file changes back as a patch applied to your checkout, uncommitted. This one-shot form works for both YAML/`spec.json` and authored TypeScript flows on `2.0.17`+ — see **Run**, above, for the version-mismatch bug that broke authored TypeScript on earlier releases. `flows deploy --repo … --on …` turns a flow into a Cloud listener that launches one run per matching ticket; `flows deployments` lists them and `flows undeploy` removes one. See [Cloud](/docs/relayflows/cloud) for the details and credentials. ## Resume and replay diff --git a/web/content/docs/relayflows/cloud.mdx b/web/content/docs/relayflows/cloud.mdx index 28ad224..6e5fd14 100644 --- a/web/content/docs/relayflows/cloud.mdx +++ b/web/content/docs/relayflows/cloud.mdx @@ -15,7 +15,7 @@ flows run --cloud --wait ship-feature.flow.ts --input '{"ticket":"ENG-42"}' Without `--wait`, exit `0` means the run was **accepted**. You get a run ID back and the run continues on its own; it hasn't completed yet — verified for real against a YAML flow: `{"ok":true,"runId":"...","status":"pending",...}`, exit `0`. With `--wait`, exit `0` means Cloud reported the run `completed` with a validated `success` reason; a failed or cancelled run, or an observation failure, exits `1` — also verified: a `--wait` run that came back with an inconsistent terminal record (`invalid_response: Cloud terminal record lacks a valid, consistent run completionReason`) exited `1`, matching the documented observation-failure case. -An authored `.flow.ts` takes `--input` exactly as a local run does (an existing JSON file, otherwise inline JSON) and travels as one self-contained source: `use:` dependencies and sibling imports are refused before any HTTP call, since the hosted runner loads the flow from the request, not from a checkout. **Verified broken today**: the third command above — `flows run --cloud --input `, with or without `--wait` — fails immediately with `{"ok":false,"code":"http_error","message":"Cloud request failed with HTTP 400."}` against a real, `flows check`-passing authored flow. Only the declarative YAML/JSON form (the first two commands) actually submits a run. Use `flows deploy` for a TS flow today; the one-shot cloud path for authored TypeScript is tracked as broken (flows#461). +An authored `.flow.ts` takes `--input` exactly as a local run does (an existing JSON file, otherwise inline JSON) and travels as one self-contained source: `use:` dependencies and sibling imports are refused before any HTTP call, since the hosted runner loads the flow from the request, not from a checkout. This path used to fail immediately with a bare `{"ok":false,"code":"http_error","message":"Cloud request failed with HTTP 400."}` against any real authored flow — root cause was Cloud pinning an older `@relayflows/surface` than the CLI authored against, badly reported (flows#461). Fixed in `2.0.17`: verified for real, the third command above now submits successfully and returns a run ID. Update to `2.0.17`+ if you still see that error. ## Bring your working tree @@ -25,7 +25,7 @@ flows run --cloud --sync-code --wait review.flow.ts --input '{"pr": 7}' flows sync # apply the run's changes to this checkout ``` -**Verified broken today, same as above**: this exact command, run against a real git checkout with a `flows check`-passing `review.flow.ts`, fails with the same `http_error: HTTP 400` — `--sync-code` doesn't change the outcome, since the failure is in submitting a TS-authored flow to `run --cloud` at all, before code sync would even matter. `flows sync ` itself (and the `git apply`/`patch_conflict` mechanics below) could not be verified in this pass, since no TS `--cloud` run reached completion to sync from. +Fixed in `2.0.17`, same as above — re-verified with `--sync-code` specifically: this exact command, run against a real git checkout with a `flows check`-passing `review.flow.ts`, now submits successfully and returns a run ID instead of the `http_error: HTTP 400` earlier releases gave. `flows sync ` itself (and the `git apply`/`patch_conflict` mechanics below) is still unverified in this pass — the run didn't reach completion inside this check's time budget, only submission was confirmed. `--sync-code` uploads the invoking directory before submission, so every `f.run` and `f.agent` in the hosted run executes inside your tree. In a Git checkout the upload is exactly `git ls-files --cached --others --exclude-standard`: `.gitignore` governs, untracked files ride along, `.git` and `node_modules` never do, executable bits survive. A checkout whose `git` fails for any other reason is refused rather than uploaded without its ignore rules. The limit is 256 MiB uncompressed. From b3b8da7498abc8913220ac80805e45a0497e7a19 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Fri, 18 Sep 2026 06:37:48 -0700 Subject: [PATCH 7/7] revert: next-env.d.ts dev-mode path was incidental, not needed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running next dev locally flipped this to .next/dev/types/... — a dev-mode codegen artifact, not something this PR's actual changes require. Reverted to the .next/types/... path already on main; it'll get regenerated correctly by whatever build step actually needs it. Co-Authored-By: Claude Sonnet 5 --- web/next-env.d.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/next-env.d.ts b/web/next-env.d.ts index a419cbe..9edff1c 100644 --- a/web/next-env.d.ts +++ b/web/next-env.d.ts @@ -1,7 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; -import "./.next/dev/types/root-params.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.