foldrun 0.8.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/bin/foldrun.mjs +77 -30
- package/docs/agents.md +66 -5
- package/docs/api-usage.md +20 -5
- package/docs/api.md +33 -16
- package/docs/backups-and-restore.md +35 -66
- package/docs/budgets-and-billing.md +5 -1
- package/docs/cli.md +45 -17
- package/docs/coding-agents.md +9 -3
- package/docs/deploying.md +5 -1
- package/docs/faq.md +5 -2
- package/docs/feature-flags.md +63 -0
- package/docs/flows.md +24 -5
- package/docs/hooks.md +139 -0
- package/docs/notifications.md +7 -1
- package/docs/providers.md +28 -0
- package/docs/runs.md +7 -2
- package/docs/security.md +14 -0
- package/docs/tools.md +46 -6
- package/docs/versions.md +64 -2
- package/docs/workspaces.md +87 -0
- package/package.json +4 -4
- package/src/commands.mjs +1206 -39
- package/src/guide.mjs +1 -1
- package/src/terminal.mjs +85 -0
package/docs/cli.md
CHANGED
|
@@ -33,21 +33,26 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
33
33
|
| | |
|
|
34
34
|
|---|---|
|
|
35
35
|
| `foldrun init [dir]` | create an account folder with one workspace in it — `--workspace <name>` names it, `--flat` makes the older single-folder shape |
|
|
36
|
-
| `foldrun new <name>` | another workspace in this account |
|
|
36
|
+
| `foldrun new <name>` | another workspace in this account — blank: an `AGENTS.md` and empty `agents/` and `flows/`. `--starter` adds the example researcher, writer and `publish` flow; `--from <template>` copies a template |
|
|
37
37
|
| `foldrun agent new <name>` | one more agent inside a workspace, with the frontmatter the format requires — and every key explained beside it. Also `foldrun flow new <name>`, whose first step names an agent this workspace already has, and `foldrun tool new <name>`, which writes a `transport: script` folder with its program in a file beside the definition (`--transport script\|http\|mcp`, `--language javascript\|python\|bash`). In an account with more than one workspace, `--to <name>` says which; inside one, it is already known. The templates are the same ones the dashboard's New button writes, so what is scaffolded is what `foldrun check` accepts |
|
|
38
|
+
| `foldrun agent new <name> --platform` | the dashboard's **New agent** on the DEPLOYED workspace (`POST …/agents { name, description, model }`): the platform writes its own template as a revision, and nothing is written in this folder. `--to <workspace>` (or the folder you stand in), `--description "…"`, `--model fast\|default\|max`. Without `--platform`, `--to` keeps naming a workspace of this account folder. Also `flow new <name> --platform` (`POST …/flows { name, pattern }`, `--pattern pipeline\|review-loop\|fan-out\|debate\|router`, default `pipeline`) and `tool new <name> --platform` (`POST …/assets { kind: "tools" }` — a `script` tool as here unless `--transport`/`--template` says otherwise; `--template` also takes a gallery entry, `--language`). `foldrun pull` brings them down |
|
|
39
|
+
| `foldrun agent ls` | the agents deployed in a workspace — name, model, description (`GET …/agents`); `flow ls` the flows and how each fires (`GET …/flows`). `--to <workspace>`, `--json` for the list as the platform returns it |
|
|
38
40
|
| `foldrun guide` | the coding-agent rules, copied from what Next.js does: a short managed block in the account's `AGENTS.md`, between `<!-- BEGIN:foldrun-agent-rules -->` markers, telling a coding agent (Claude Code, Cursor, Codex) to read `foldrun docs` before it writes an agent, flow or tool, and a `CLAUDE.md` that imports it with `@AGENTS.md`. `foldrun init` writes them, `foldrun pull` refreshes them, and `foldrun check` re-adds them when a coding agent runs it and they are missing or old — as `next dev` does. Only the block is rewritten, in place; text around it is kept. The runtime strips the block, so the agents that run in the account never see it; the rest of `AGENTS.md` stays their shared context. `--check` exits 1 when the block is missing or old (for CI), `--print` writes it to stdout |
|
|
39
41
|
| `foldrun docs [page]` | these docs, from the copy that ships inside the CLI — the way Next.js ships `node_modules/next/dist/docs` — so a coding agent reads the version it is driving, offline. No page lists them; `foldrun docs flows` prints one; `--path` prints the folder. Design records and the platform operator's routes are left out |
|
|
40
|
-
| `foldrun check [dir]` | validate agents, flows, tools, evals, knowledge — offline. At an account root: every workspace, then the shared library. Includes each agent's colleagues: an `agents:` or `subagents:` name that is not an agent, a sub-agent with no `description:`, a sub-agent that would hold none of its tools inside its parent's. Signed in, it also asks the platform's open `/api/version` which Chrome its runner image has, and warns when a pinned `web.browse.user_agent:` claims another major ([why](tools#how-this-agents-browser-presents-itself)); it warns about the old `web.browse.session:` key too (now `vendor_session:`) |
|
|
42
|
+
| `foldrun check [dir]` | validate agents, flows, tools, evals, knowledge — offline. At an account root: every workspace, then the shared library. Includes each agent's colleagues: an `agents:` or `subagents:` name that is not an agent, a sub-agent with no `description:`, a sub-agent that would hold none of its tools inside its parent's. Signed in, it also asks the platform's open `/api/version` which Chrome its runner image has, and warns when a pinned `web.browse.user_agent:` claims another major ([why](tools#how-this-agents-browser-presents-itself)); it warns about the old `web.browse.session:` key too (now `vendor_session:`). Hooks: a `hooks/<name>/` folder that is not a valid hook (`on:`, `run.mjs` beside `hook.md`, `tool:` only on tool events, `timeout:` 1 to 120), and a `hooks:` name in an agent or an AGENTS.md that is in neither `hooks/` nor the library, are errors; a `tool.call` hook on an agent whose `web.search:` (or `web.fetch:`) the model provider runs on its own servers is a warning — those calls never reach the hook |
|
|
41
43
|
| `foldrun extract [dir]` | move each single-file script tool — a `tools/<name>.md` with its program in a fenced block — into a folder: `tools/<name>/tool.md` with `run:` pointing at `run.js`, `run.py` or `run.sh` beside it. Frontmatter is edited as text, never re-serialised, so the diff is only what moved. A tool that already has a folder is left for you; `--dry-run` lists what it would do |
|
|
42
44
|
| `foldrun check --to <ws>` | validate the copy that is DEPLOYED, rather than the folder in front of you — a workspace edited through the API, by an agent or by `foldrun source put`, has had no way to be checked short of spending a run. The files are fetched into a temporary folder and the ordinary check runs over them, so there is one copy of the rules; the output names the workspace and the platform it checked |
|
|
43
45
|
| `foldrun agent run <name>` | run one agent once on a platform, with no flow file: a flow of one step. `--task "…"` is the instruction and is required. Prints the run id and how to follow it; `--wait` holds on (in 25-second pieces, as `invoke` does) and then prints the same report `foldrun report` would; `--test` makes it a [test run](runs#test-runs) |
|
|
44
46
|
| `foldrun agent link <agent>` | add to an agent's team, in its own frontmatter — `--subagent <worker>` (`subagents:`, a worker it delegates to), `--consult <agent>` (`agents:`, a colleague it asks) or `--can-ask` (`ask` in `tools:`, so it may ask you mid-step). The edit is core's, the one the canvas's docks make: only the lines of that one list change, flow style stays flow style, comments and the prose stay byte for byte. A sub-agent without a `description:` needs one, because the model picks a sub-agent by it — asked for at a terminal, `--description "…"` otherwise, and written into the worker's file first. Says which flows use the agent ("used by 3 flows"), since the change reaches all of them. Prints the diff, and refuses an edit `foldrun check` would call an error; `--dry-run` writes nothing. In this folder by default (`--workspace <name>` in an account with several); `--to <workspace>` edits the deployed copy through `PATCH /api/workspaces/<ws>/agents/<name>`. In this folder it re-reads each file just before writing — after the description prompt, after `check` — and refuses, writing nothing to either file, when one changed since it was read |
|
|
47
|
+
| `foldrun agent import <workspace>/<agent>` | copy an agent you already have in another workspace into this one: its `agent.md` and own `skills/`, `scripts/`, `knowledge/` — never `memory/`, `outputs/` or `state/`. Into the workspace you stand in, or `--workspace <name>`; `--as <name>` imports it under another name (and rewrites `name:`), which a name clash requires. `--to <workspace>` does it on the platform (`POST /api/workspaces/<ws>/agents/import`) and lists the tools, skills, scripts and secrets the copy uses that the workspace lacks. See [Importing an agent](workspaces#importing-an-agent-you-already-have) |
|
|
48
|
+
| `foldrun agent import --list` | what could be imported into a deployed workspace: every other workspace this key may read, and each one's agents with their descriptions (`GET …/agents/import`). Imports nothing. `--to <workspace>`, `--json` |
|
|
45
49
|
| `foldrun agent unlink <agent>` | take one away: `--subagent <name>`, `--consult <name>` or `--can-ask`. An emptied list is written `key: []`, not deleted. In this folder, refused the same way, nothing written, when the file changed since it was read |
|
|
46
50
|
| `foldrun flow add <flow> <pattern>` | one palette block from the [flow canvas](flows#editing-on-the-canvas), as the same markdown edit the canvas writes (core's `flow-patterns`): `chain` (alias `step`: `--agent <name>`, `--instruction "…"`, `--after <n>` or `--before <n>`, default last), `parallel` (`--agent`, `--group <n>`), `router` (`--agent <triage>`, `--cases BUG=bugs,DOCS=docs`, `--else <agent>`, `--after`), `fan-out` (`--step`, `--each lines\|items\|"rows of <path>"`, `--max <n>`), `loop` (alias `evaluator`: `--step`, `--loop <n>` default 3, `--until <MARKER>` default APPROVED, `--judge "<claim>"` adds `verify: judge:`), `approval` (`--step`; the `!` marker, `approve: true` on a `?` step), `ask` (`--step`, `--question "…"`), `wait` (`--step`, `--wait 30m\|event`), `rescue` (`--step`, `--on-fail <agent>`), `subflow` (`--flow <other>`, `--after`). `--step` is the step's place in the file (1 = the first step line) or the agent it runs; `--off` takes a step pattern back off. Prints the diff, runs the rules `check` runs over the result and refuses — writing nothing — an edit that would add an error; one that was there before does not block it. `--dry-run` shows the diff and writes nothing. In this folder by default; `--to <workspace>` edits the deployed copy through the canvas's own route (`PATCH …/flows/<flow>` with `{ edit }`), and the sha256 of the file it read as `expect`: one edited meanwhile is refused (`409`), nothing written. In this folder it re-reads the file just before writing — after the y/N, after `check` — and refuses the same way, writing nothing, when it changed since it was read |
|
|
47
51
|
| `foldrun flow rm-step <flow> --step <n\|agent>` | delete one step, as the canvas's × does (core's `removeStep`): the step line and its indented options go, the groups after an emptied one renumber, prose and the agent's file are untouched. Says what else changes first (`case:`/`when:` steps that will route on the step before, the renumbering), prints the diff, refuses one `foldrun check` would call a new error, then asks — no terminal needs `--yes`. `--step` is the step's place in the file (1 = the first step line) or the agent it runs. `--dry-run` writes nothing; `--to <workspace>` sends the canvas's `PATCH …/flows/<flow>` with `{ edit: { op: "remove", step } }`, and the sha256 of the file it read as `expect`: one edited meanwhile is refused (`409`), nothing written. In this folder it re-reads the file just before writing — after the y/N, after `check` — and refuses the same way, writing nothing, when it changed since it was read |
|
|
48
52
|
| `foldrun flow dup-step <flow> --step <n\|agent>` | duplicate one step, as the canvas's Duplicate (Cmd/Ctrl+D) does (core's `duplicateStep`): a copy of the step line — its `?`/`!` marker and every indented option — goes directly under it with the same group number, so the copy runs in parallel with the original; nothing else moves. Prints the diff and refuses one `foldrun check` would call a new error; additive, so it does not ask. `--dry-run` writes nothing; `--to <workspace>` sends `PATCH …/flows/<flow>` with `{ edit: { op: "duplicate", step } }`, and the sha256 of the file it read as `expect`: one edited meanwhile is refused (`409`), nothing written. In this folder it re-reads the file just before writing — after the y/N, after `check` — and refuses the same way, writing nothing, when it changed since it was read |
|
|
49
53
|
| `foldrun flow show <flow>` | the flow as the canvas draws it, in a terminal: the trigger, then one block per group — parallel steps listed under their group number — each step with its chips (`approve`, `optional`, `when:`, `case:`/`else`, `each:` ×max, `loop … until`, `verify:`, `retry`, `wait`, `ask:`, `on-fail →`, `model`), and under it its agent's team: `↳ sub-agent`, `· consults`, `may ask you`. What `foldrun check` says about a step or its agent is printed on it, red for an error; exits 1 when there is one. Plain text, no colour, when the output is not a terminal. `--to <workspace>` shows the deployed copy |
|
|
50
54
|
| `foldrun flow run <flow>` | start a flow on the platform — the same command as `invoke`, spelled the way `agent run` is. `foldrun run` runs HERE; `flow run`/`invoke` run a flow on the platform; `agent run` runs one agent there |
|
|
55
|
+
| `foldrun flow hook <flow>` | the flow's webhook URL as it is now (`GET …/hooks/<flow>`), changing nothing — `--json` prints `{ flow, url, path }`. Needs `workspace:manage`, like the settings page that shows it; the URL is a credential |
|
|
51
56
|
| `foldrun flow rotate-hook <flow>` | a new webhook URL for the flow (`POST …/hooks/<flow>/rotate`). The old URL stops working the moment it returns, so it asks first — `--yes` with no terminal. Prints the new URL, which is itself a credential |
|
|
52
57
|
| `foldrun flow set <flow> --step <n\|agent> key=value …` | the step editor on the Flows page, from a terminal (core's `updateFlowStep`, and `updateFlowStepInstruction` for `instruction=`): `model`, `effort`, `retry`, `timeout`, `verify`, `when`, `case`, `else`, `loop`, `until`, `each`, `max`, `limits` — only the keys named are touched, `key=` clears one. `each=` takes `lines` only; the other fan-outs are `flow add … fan-out`. A value the rewriter would drop or clamp is refused, nothing written: `effort` one of low, medium, high, xhigh, max (or an alias core reads, e.g. `deep`); `timeout` a whole number of seconds (`timeout=900`, not `15m`); `retry` 0–5, `loop` 1–5, `max` 1–20, whole numbers. Shows the diff, refuses an edit `check` would call a new error, `--dry-run` writes nothing; `--to <workspace>` sends the editor's `PATCH …/flows/<flow> { step, options }` / `{ step, instruction }` with `expect` |
|
|
53
58
|
| `foldrun flow trigger <flow> manual\|webhook\|schedule` | the trigger picker on a flow card (core's `setFlowTrigger`): only the `trigger:`, `schedule:` and `timezone:` lines change. `--schedule "0 9 * * 1"` (5 fields, or `@daily`) and `--timezone <zone>` go with `schedule`. Diff, checked, `--dry-run`; `--to` sends `PATCH { trigger }` |
|
|
@@ -56,8 +61,13 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
56
61
|
| `foldrun flow paste <flow>` | the canvas's Paste: step markdown from stdin or `--file <path>` goes in as new groups after `--after <g>` (default last), or beside group `--group <g>`, renumbered to fit (core's `pasteSteps`) — `foldrun flow copy-step a --step 2 \| foldrun flow paste b`. Diff, checked, `--dry-run`; `--to` sends `PATCH { edit: { op: "paste" } }` |
|
|
57
62
|
| `foldrun flow draft "<what it should do>"` | **Draft with AI**, on the platform (`--to <workspace>`; `--flow <name>` redrafts an existing flow): `POST …/flows/draft` has a model write the flow and any agent files it needs, checks and repairs once, and the CLI prints the notes, every file as a diff and what the check still finds. Nothing is saved until you say yes — `--yes` on a pipe, `--dry-run` never; on a pipe without `--yes` it saves nothing and exits 1, though the draft call was made and charged; each file goes through `PUT …/source` with `ifMatch`, so one that changed meanwhile is refused. One model call, charged like a step |
|
|
58
63
|
| `foldrun tool test <name>` | exercise one tool, alone — no model, no run, no flow. The real thing runs: the HTTP request against the declared `base:`, the script with the workspace's secrets in its environment, the MCP handshake. Prints the transport, whether it exited cleanly, how long it took, everything the tool printed, and any secret it needed by NAME and never by value. Its arguments are `key=value` pairs, one per argument the tool declares, and `--path` is an http tool's probe path. It waits five minutes by default, because a tool that crawls takes as long as it takes — `--timeout <seconds>` waits longer, and a test given up on says so rather than claiming the tool failed |
|
|
64
|
+
| `foldrun hook ls` | the hooks in this workspace (`hooks/<name>/`) and the account library (`library/hooks/<name>/`): each one's `on:`, `tool:` and description, read the way a run reads them — a folder that is not a valid hook is listed with the reason and the command exits 1. A workspace hook shadows a library one of the same name. `--json` for a script. `--to <workspace>` lists the DEPLOYED workspace's hooks instead (`GET /api/workspaces/<ws>/hooks`, the dashboard's Hooks panel): who turns each on, and the denies, rewrites and failures they wrote into recent runs — `--runs <n>` reads further back (default 50, up to 500), `--all` shows every decision |
|
|
65
|
+
| `foldrun hook add <name> --on <event>` | a hook folder: `hook.md` (`on:` one of `tool.call`, `tool.result`, `step.start`, `step.end`; `tool:` from `--tool <pattern>` — `Bash`, `Write\|Edit`, `mcp__*` — on the tool events only) and a self-contained `run.mjs` that allows everything, with the whole contract in its comments: the event on stdin, one JSON answer on stdout, exit 2 to deny with stderr as the reason. `--library` writes it in the account library for every workspace; `--to <workspace>` picks one in an account. An agent turns it on with `hooks: [name]`; `hooks:` in a workspace's or the account's AGENTS.md turns it on for every agent under it |
|
|
66
|
+
| `foldrun hook test <name>` | run one hook HERE on one event, exactly as a step runs it (the event on stdin, a stripped environment, its `timeout:`), and print its answer — decision, reason, a rewritten input, context, reply or note — and how long it took. `--event '<json>'` or `--event @file`; without it, a sample event for the hook's `on:`. A deny is an answer (exit 0); a hook that crashes, times out or answers in the wrong shape is a failure (exit 1) — which, on a gate, refuses the call. `--to <workspace>` runs the DEPLOYED hook on the platform instead (`POST …/hooks/<name>/test { event }`) |
|
|
67
|
+
| `foldrun hook rm <name>` | delete a hook folder (`--library` for the account's). Says which agents and AGENTS.md files still turn it on — their steps are refused until it goes from their `hooks:` — and asks; `--yes` on a pipe |
|
|
59
68
|
| `foldrun run <target>` | run an agent or a flow HERE, on this machine (`name`, or `flow:name`) — `--test` for a [test run](runs#test-runs). On a platform: `flow run`/`invoke` for a flow, `agent run` for one agent |
|
|
60
69
|
| `foldrun eval [name]` | one eval, or all of them. `--to <workspace>` runs the DEPLOYED workspace's evals on the platform (`POST …/evals/<name>/run`), waiting for each verdict, and prints the same lines; exits 1 when a case fails |
|
|
70
|
+
| `foldrun eval new <name>` | the Evals page's New, on the deployed workspace (`POST …/evals { name }`): an eval whose `agent:` is the workspace's first agent and whose case is a placeholder — edit it (`source cat`/`put`), then `foldrun eval <name> --to <workspace>` runs it. `--to <workspace>` |
|
|
61
71
|
| `foldrun promote <run-id>` | keep a finished run as a regression case: its task and what the next run must still say go into `evals/<target>-regressions.md` (or `--eval <name>`), appended when the file exists. `--expect "contains: $34"` — repeatable, eval syntax — replaces the default judge line that holds the next run to the recorded conclusion; `--case <name>` names the case |
|
|
62
72
|
| `foldrun probe <model>` | can this model hold a tool loop here? A live check |
|
|
63
73
|
| `foldrun logs [run-id]` | recent runs in one workspace, or one run's whole event trail. `--local` reads this machine's store even when signed in; `foldrun runs --local` is the same command and prints the same thing |
|
|
@@ -67,9 +77,9 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
67
77
|
| `foldrun report <run-id> get <agent>/<path>` | download one file the run archived — whatever that agent left in `outputs/`, copied under the run when it ended. Writes it beside you under its own name; `--file <path>` names somewhere else (`-` for stdout), `--force` overwrites |
|
|
68
78
|
| `foldrun report <run-id> live` | the run page's live browser view: which agents' browsers have posted a frame, and when; `--agent <name>` saves the newest frame as a JPEG (`--file <path>`, `-` for stdout) and prints the page's address, the engine and whether the browser has closed (`GET …/runs/<id>/live`) |
|
|
69
79
|
| `foldrun approvals` | every gate waiting on a person, anywhere in the account: which workspace, flow and run, which step and agent, the question, how long it has been waiting and which files under `storage/` the gate previews — and, first, every question an agent is asking mid-step, with its choices. `--to <workspace>` narrows it |
|
|
70
|
-
| `foldrun approve <run-id>` | release a waiting gate. Approving is outward — the step behind it publishes, sends or posts — so it prints what it is about to release and asks you to type the run id back, unless `--yes`. `--note "…"` is read by the agent as guidance, `--step <n>` decides one gate of several |
|
|
80
|
+
| `foldrun approve <run-id>` | release a waiting gate. Approving is outward — the step behind it publishes, sends or posts — so it prints what it is about to release and asks you to type the run id back, unless `--yes`. `--note "…"` is read by the agent as guidance, `--step <n>` decides one gate of several. `--payload <json\|text\|@file>` releases a step parked on `wait: event` with that body instead, as its event URL would — the trace names you, not "an external event" |
|
|
71
81
|
| `foldrun reject <run-id>` | refuse one, failing the step and the run with it. `--note` is the reason, and it goes on the trace |
|
|
72
|
-
| `foldrun stop <run-id>` | kill a run in flight: the queued job is dropped and the sandbox the current step is spending in is destroyed. It prints which step is running and what has been spent, then asks you to type the run id back unless `--yes`. Finished steps keep their results, and costs already incurred stay on the bill. A run that has already finished is said so, not stopped. With no run id and a filter — `--status running,queued`, `--since 2h`, `--flow <name>` — it stops every run that matches (`POST …/runs/bulk`), in `--to <workspace>` or across the account: it lists what matches first, asks, and then stops exactly the runs it listed, so one that starts in between is not swept up. `--dry-run` lists and touches nothing. The route refuses more than 100 at once |
|
|
82
|
+
| `foldrun stop <run-id>` | kill a run in flight: the queued job is dropped and the sandbox the current step is spending in is destroyed. It prints which step is running and what has been spent, then asks you to type the run id back unless `--yes`. Finished steps keep their results, and costs already incurred stay on the bill. A run that has already finished is said so, not stopped. With no run id and a filter — `--status running,queued`, `--since 2h`, `--until 1h` (started before then: a span ago, or a date), `--flow <name>` — it stops every run that matches (`POST …/runs/bulk`), in `--to <workspace>` or across the account: it lists what matches first, asks, and then stops exactly the runs it listed, so one that starts in between is not swept up. `--dry-run` lists and touches nothing. The route refuses more than 100 at once |
|
|
73
83
|
| `foldrun answer <run-id> "…"` | answer the question an agent is asking mid-step (`tools: [ask]`). `--option <n>` picks one of the choices it offered; `--question <id>` one of several. The agent is waiting in its sandbox and carries on at once. `foldrun approvals` lists what is being asked |
|
|
74
84
|
| `foldrun message <run-id> "…"` | say something to a running agent. It reaches the model after its next tool call, as context — it changes none of the step's tools. `--step <n>` picks one when a parallel group has several running. Platform only |
|
|
75
85
|
| `foldrun secrets set NAME` | store a secret — also `ls`, `rm`, and `status` (OAuth grant health) |
|
|
@@ -80,39 +90,46 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
80
90
|
| `foldrun deploy [dir]` | push this account into an installation — every workspace and the shared library. `foldrun deploy <workspace>` pushes one. Never removes a workspace the platform has and the folder does not. It first prints where it is going and as whom (`deploying to <url> as <profile> (<email>)`); a deploy that would delete files the folder no longer has lists them and asks — what runs wrote (`storage/`, `state/`, the trigger log) is never deleted, so never listed. The confirmed list goes with the deploy; if the platform would now remove anything else it refuses and the CLI asks again. With no terminal (a script, a CI job, a coding agent) it refuses unless `--yes`. A deploy that only adds or changes files asks nothing |
|
|
81
91
|
| `foldrun help <command>` | that command's usage and options. `foldrun <command> --help` and `-h` print the same and never run the command |
|
|
82
92
|
| `foldrun pull [workspace]` | bring the platform's account down into this folder — refuses to overwrite a locally edited file, and names every one it would have taken (`--force` takes them) |
|
|
93
|
+
| `foldrun export [workspace]` | a deployed workspace as a `.zip` — agents, flows, tools, skills, scripts, evals, knowledge; never memory, state, storage or a secret's value. `--flow <name>` exports that flow with every flow and agent it runs and the tools, skills and scripts they grant; `--agent <name>` that agent and what it grants. Written under the name the platform gives it (`<ws>.zip`, `<ws>-flow-<name>.zip`); `--file <path>` (`-` for stdout), `--force` to replace (`GET …/export`) |
|
|
94
|
+
| `foldrun import <file.zip>` | take a package in: first shows what it adds, what is identical, what it would replace and what it still needs (secrets, tools, skills, scripts, agents, flows), then asks. Never deletes; replacing a file needs `--overwrite`. `--to <workspace>` (a name that does not exist makes it, from a workspace package), `--dry-run` previews only (`--json` for the plan), `--yes` with no terminal (`POST …/import`) |
|
|
83
95
|
| `foldrun status [workspace]` | per workspace: what is added, changed and only-on-the-platform, plus which files moved there since your last deploy from here. Reads only |
|
|
84
96
|
| `foldrun status --platform` | is the platform up: overall, each component (dashboard and API, run workers, scheduler, model providers, email, storage) and any incident or maintenance the operator posted, with its latest update. Reads the open `/api/status` — no key needed; the signed-in platform, `--url`, or app.foldrun.io. `--json` prints the answer as is. Exits 1, saying *status unavailable — the platform may be down*, when it does not answer. Plain `foldrun status` opens with the same one-line verdict |
|
|
85
|
-
| `foldrun
|
|
97
|
+
| `foldrun status --platform --history` | what the status page draws as bars: each component's uptime over the window and a mark per day — up, degraded or maintenance, an outage, or no samples — then the incidents and maintenance in it, from the open `GET /api/status/history?days=<n>`. `--days 1-90` (default 90), `--json` |
|
|
98
|
+
| `foldrun workspaces` | what exists here, what exists on the platform, which are both — also `new <name>`, and `rm <name>` (locally; `--platform --yes` deletes it there). `--json`: `{ platform, workspaces: [{ name, here, platform }], error? }`, exit 1 when the platform could not be read |
|
|
99
|
+
| `foldrun workspaces show <name>` | what is deployed in one workspace (`GET /api/workspaces/<name>`): its agents with model and description, its flows and how each fires, and its files counted by top-level folder. `--json` for the document |
|
|
100
|
+
| `foldrun workspaces new <name> --platform` | the dashboard's **Create workspace**: blank — an `AGENTS.md` and nothing to delete (`POST /api/workspaces { name, template: true }`) — or `--starter` for the example researcher, writer and publish flow (`{ name, starter: true }`). The plan's workspace count applies. Without `--platform` it makes the folder here, as `foldrun new` does |
|
|
86
101
|
| `foldrun workspaces demo` | **Try the demo pipeline**: makes the `demo-pipeline` workspace on the platform — a four-step flow with a parallel pair, an approval gate and a pretend CRM tool (`POST /api/workspaces { demo: true }`) |
|
|
87
|
-
| `foldrun library` | the account's shared library on the platform, as the Library page shows it: `ls [skills\|tools\|scripts\|knowledge]`, `cat <kind>/<path>`, `put <kind>/<path>` (from `--file`, else stdin), `rm <kind>/<path>` (asks). `deploy` still pushes the whole of it |
|
|
102
|
+
| `foldrun library` | the account's shared library on the platform, as the Library page shows it: `ls [skills\|tools\|hooks\|scripts\|knowledge\|memory]`, `cat <kind>/<path>`, `put <kind>/<path>` (from `--file`, else stdin), `new <kind>/<name>` (the Library page's New, `POST /api/library/<kind> { name, template, language }`: the platform's template — `--template` a transport or a gallery entry, `--language` for a script tool), `rm <kind>/<path>` (asks). `deploy` still pushes the whole of it |
|
|
88
103
|
| `foldrun find <words>` | the dashboard's ⌘K search: workspaces, agents, flows, tools, skills, knowledge, memory and runs across the account, ranked the same way (`GET /api/search`; `--json`) |
|
|
89
104
|
| `foldrun invoke <flow>` | start a flow on a running platform — `--watch` follows its trace here; `--test` makes it a [test run](runs#test-runs) `--once <key>` sends the key as the run's idempotency key, so a CI step that retries after a dropped response is answered with the run it already started instead of paying for a second. `--tag <t>`, repeatable, labels the run (the API's `tags`). `--inputs <name>` sends a saved input set as the task — a case of `evals/<flow>-inputs.md` or of an eval that runs the flow, as the platform has it (see [Run with saved inputs](flows#run-with-saved-inputs)). `foldrun flow run <flow>` is the same command |
|
|
90
|
-
| `foldrun source <verb>` | one workspace file on a platform: `ls [dir]`, `cat <path>`, `put <path>` (from `--file`, else stdin; `--message` goes on the revision), `mv <from> <to>`, `rm <path>` — the editor's own door, so every write is a revision. For a whole tree, `deploy` |
|
|
105
|
+
| `foldrun source <verb>` | one workspace file on a platform: `ls [dir]`, `cat <path>`, `put <path>` (from `--file`, else stdin; `--message` goes on the revision), `new <kind> <name>`, `mv <from> <to>`, `rm <path>` — the editor's own door, so every write is a revision. `new` is the dashboard's New button for any document (`POST …/assets { kind, name, agent?, template?, language? }`): `tools`, `knowledge`, `memory`, `skills`, `scripts` (and `agents`, `flows`, `evals`), the template written by the platform; `--agent <name>` puts a knowledge, memory, skill or script document inside that agent. For a whole tree, `deploy` |
|
|
91
106
|
| `foldrun open [page]` | the dashboard for this workspace, in the browser (`runs`, `agents`, `graph`, `repo`…) |
|
|
92
107
|
| `foldrun login` | sign this machine in from the browser — no key to copy |
|
|
93
108
|
| `foldrun login <site> --url <address>` | a browser window to sign in to a site by hand; the session is stored for an agent |
|
|
94
109
|
| `foldrun logout` | forget this machine's key, and revoke it if `login` made it |
|
|
95
110
|
| `foldrun whoami` | who you are on the platform: account, role, which workspaces |
|
|
111
|
+
| `foldrun team` | who is in the account (`GET /api/team`): each member's email, role (the owner marked), the workspaces they may open — a scoped key sees only the ones it shares — and when they joined. `--json`. Read-only on purpose: inviting, changing a role, removing someone and transferring the account are a signed-in person's, in Settings → Team, and the platform refuses them from a key |
|
|
96
112
|
| `foldrun onboarding` | the account's getting-started steps — workspace, agent, a run, a run that completes, a secret, a schedule or webhook, a teammate, billing where the plan asks for it, the CLI — each worked out from what is really there, with the next one first and where to do it (`--json`). The dashboard card is hidden per person from the dashboard |
|
|
97
113
|
| `foldrun preferences` | your own settings on the platform — today the theme (`system`, `light`, `dark`), kept with you on every browser; `preferences set theme dark` changes it (`/api/me/preferences`; a key acts for whoever minted it) |
|
|
98
114
|
| `foldrun api spec` | the platform's OpenAPI 3.1 document, from `GET /api/openapi.json` (no key needed) — to stdout, or `--out <file>`. Every other command sends `Foldrun-Version` (the API version this CLI was built for), an `Idempotency-Key` on each write (reused on its own retry), and waits out one `429` for `Retry-After` before saying so. See [Using the API](api-usage) |
|
|
99
|
-
| `foldrun api version` | Settings → API: the API version this account's requests get when they send no `Foldrun-Version`, whether it is pinned, every version still served and the
|
|
100
|
-
| `foldrun changelog` | What's new: the platform's release notes, newest first — features, fixes, docs — and which release this install runs (`GET /api/changelog`; `--limit <n>` up to 50, `--json`) |
|
|
115
|
+
| `foldrun api version` | Settings → API: the API version this account's requests get when they send no `Foldrun-Version`, whether it is pinned, every version still served, the rate limits, and the last 30 days: requests by version, and any deprecated route the account still calls — by which key or person, and when it stops answering. `api version pin <YYYY-MM-DD>` pins it, `unpin` goes back to current (`PATCH /api/account/api`; an admin's) |
|
|
116
|
+
| `foldrun changelog` | What's new: first the API — its current version, the breaking changes each dated version brought, and the routes deprecated with their sunset dates — then the platform's release notes, newest first — features, fixes, docs — and which release this install runs (`GET /api/changelog`; `--limit <n>` up to 50, `--json`) |
|
|
101
117
|
| `foldrun version` | this CLI's version, its `@foldrun/core`'s, and — when a platform is known (`--url`, `--profile`, `FOLDRUN_URL` or where you signed in) — that platform's: version, release time, API version, each component's commit and the browsers its runner image has (with versions where known: `chrome 154.0.8037.58`), from `GET /api/version` (no key needed). Warns when the platform was released with a newer CLI than this one. An unreachable platform is a dim line, not a failure. `--json` prints `{ cli, core, platform }`. `foldrun --version` (or `-v`) prints only the CLI's version, offline. See [Versions and releases](versions) |
|
|
102
118
|
| `foldrun doctor` | check the road to the platform, one line each: Node (22 or newer), the CLI and core versions, which of `FOLDRUN_URL`, `FOLDRUN_TOKEN`, `FOLDRUN_TIMEOUT`, `HTTPS_PROXY` are set (never their values), which model credential a local run would use (by its variable, never its value), the account it would act as, DNS for the platform's host, and a timed `GET /api/healthz`. The first thing to run when a command cannot reach the platform |
|
|
103
119
|
| `foldrun storage <verb>` | what a workspace PRODUCED, as opposed to what you wrote — `source` is the other half. `ls [prefix]` lists every file with its size, when it was written, how long ago and which run wrote it (`run:<id>`, `user:<email>` for an upload, `api-key` for a key), newest first, plus what the store holds against its quota. `cat <path>` prints a text file and refuses one that is not text; `cat <path> --preview` asks the platform to read it instead and prints what is in it — a PDF or Word document as paragraphs, a spreadsheet or CSV as a table, a deck as slides, an archive as its listing. `get <path>` downloads it beside you — `--file <path>` names somewhere else, `--force` overwrites. `put <file|folder>...` uploads any files, of any type, into storage under their own names, walking folders whole (`.git`, `node_modules` and `.DS_Store` are skipped); `--into <folder/>` puts them under a folder, `--as <path>` renames a single file, and large files go straight to the bucket by a presigned URL, so size is not capped by the platform's request limit; `rm <path>` removes one. `share <path>` mints a public link to one produced file (7 days; `--ttl <days>`, `--forever`) and prints the URL; `shares` lists the live links (`--all` includes expired and revoked); `unshare <token>` revokes one — the link answers 404 from then on and the token is never reused. `--to <workspace>` from anywhere; inside a workspace folder it is already known |
|
|
104
120
|
| `foldrun runtimes` | the environments a deployed workspace's agents need — each distinct `runtime:` (an agent's own merged with its tools'), the packages in it, which agents use it, and whether the platform has built it: ready, building, or failed with the installer's own error. `foldrun deploy` prints the same thing after a deploy and waits up to two minutes for builds in progress (`--no-runtimes` skips the wait); this asks again later, and `--wait` holds on while any is building. Exits 1 when one has failed. `--to <workspace>` from anywhere |
|
|
105
121
|
| `foldrun schedule` | every flow in the account whose `trigger:` is a clock: workspace, flow, the cron line, its timezone, how many steps, whether the scheduler can parse it, and the next three times it fires. The times are the point — `0 5 1-7 * 5` reads as "the first Friday" and fires eight times in twenty-eight days, because day-of-month and day-of-week are OR'd. Exits 1 if any line is unparseable, since an invalid schedule errors nowhere and simply never runs. `--to <workspace>` narrows it |
|
|
106
122
|
| `foldrun schedule tick` | Settings' **Run scheduler now**: fire whatever is due this minute and print which flows went (`POST /api/schedule`). They were due anyway; this only makes them go now |
|
|
107
|
-
| `foldrun rerun <run-id>` | the same flow again, from a step: `--from <n>` as the flow file numbers them, or `--agent <name>` for the first step that agent runs. Earlier steps are recorded as skipped. This is the debug loop from the terminal: fix the file, `deploy`, `rerun --from` the step that failed. `--wait` follows the new run like `invoke --wait`; `--to <workspace>` from anywhere. With no run id and a filter (`--status failed`, `--since 24h`, `--flow <name>`) it reruns every match from `--from <n>` (default 1), listing and asking first as a bulk `stop` does; `--dry-run` lists only |
|
|
123
|
+
| `foldrun rerun <run-id>` | the same flow again, from a step: `--from <n>` as the flow file numbers them, or `--agent <name>` for the first step that agent runs. Earlier steps are recorded as skipped. This is the debug loop from the terminal: fix the file, `deploy`, `rerun --from` the step that failed. `--wait` follows the new run like `invoke --wait`; `--to <workspace>` from anywhere. With no run id and a filter (`--status failed`, `--since 24h`, `--until 2h`, `--flow <name>`) it reruns every match from `--from <n>` (default 1), listing and asking first as a bulk `stop` does; `--dry-run` lists only |
|
|
108
124
|
| `foldrun triggers` | why nothing ran. One row per flow that fired in the window: its trigger, how many fires became runs, when it last ran, and every reason for the difference with a count — a duplicate delivery, a throttled or debounced burst, a fire the platform slept through, an `overlap: skip`, a flow switched off by `disable_after:`. A switched-off flow is a non-zero exit; one successful run clears it. `--since <days>` (default 7), `--to <workspace>` |
|
|
109
125
|
| `foldrun observe` | where the account fails, retries and spends: one row per workspace — runs, failed and what share, steps, cost — then the recent failures in their own words. `--to <workspace>` prints that workspace in full, as the Observe page does: per flow (runs, failed, p50/p95 duration, cost), per agent (failed, retried, skipped, p95), per tool (calls, errors, p95 latency, which agents use it). `--since <days>` (default 30), `--json` for the document |
|
|
110
126
|
| `foldrun usage` | what the account consumed — runs, steps, tokens, compute, storage, per workspace — and what it was charged over `--days <n>` (default 56) from the ledger, per workspace with the change against the period before. `--json` for both documents |
|
|
111
127
|
| `foldrun billing` | the account's balance, whether billing is on at all, what the platform is currently waiving, and the recent ledger entries with what each was for — a run names its workspace, flow and run id; an adjustment carries its own note. `--limit <n>` shows more. `statement [YYYY-MM]` is a month of the ledger (this month by default) — `--csv` for the file an accountant wants, to stdout or `--file <path>`. `wallet` is the burn, the runway and auto top-up; `wallet set auto-top-up --threshold <usd> --amount <usd>` turns it on (a refill of 5–500), `set auto-top-up off` turns it off, `set email <address>` is where receipts go. `details` is who invoices are made out to; `details set --name "…" --abn … --address "line1, city, state, postcode, AU" --email …` changes it, merged with what is there. `portal` prints a link to Stripe's billing portal. Changing the wallet, the details, and the portal are the account owner's; anyone else is told so |
|
|
112
128
|
| `foldrun billing plans` | the Plans page: every plan with its monthly fee, credits, runs at once and workspaces, which one the account is on and this cycle's credits left (`GET /api/billing/plans`). `billing plan <id>` chooses one — a first plan answers with Stripe's checkout page, a change on a live plan is prorated at Stripe at once (asks first; `--yes`); `plan cancel` stops it at the end of the cycle, `plan resume` undoes that until then (`POST`/`DELETE /api/billing/subscribe`) |
|
|
113
129
|
| `foldrun billing top-up <usd>` | the Payment page's top-up: Stripe's checkout page for that amount, opened in the browser (`--no-browser`, or no terminal, prints it). The credit lands when Stripe confirms the payment. `billing card` is the same for saving a card for auto top-up. Both need `billing:manage` |
|
|
130
|
+
| `foldrun billing credit <usd>` | put credit on the balance by hand (`POST /api/billing { usd, note }`, `--note "…"` on the ledger line) — only on an install without Stripe, a self-hosted or dev platform; once billing is live the platform refuses it and top-ups go through `billing top-up`. The dashboard's Billing → Add credit does the same while billing is off. Needs `billing:manage` |
|
|
114
131
|
| `foldrun gallery` | the tools the platform ships to every account (`web`), and for each whether this account keeps its own copy and whether that copy still matches. `pull` lays the shelf down under `~/.foldrun/gallery/<host>/` for offline runs (`foldrun run` and `foldrun eval` refresh it themselves before each run); `upgrade <tool>` replaces the account's copy with the current gallery version, the old one a revision back |
|
|
115
|
-
| `foldrun account` | the account's own defaults — the AGENTS.md frontmatter every workspace under it inherits: `timezone`, `notify`, `budget`, `concurrency`. `foldrun account set timezone Australia/Sydney`, `set notify email you@example.com` (`--events failed,awaiting-approval,completed`), `set notify url https://…`, `set budget 60/month`, `set concurrency 4`, and `clear <key>` to unset one. `set notify` merges against what is there, because the platform replaces the whole block. `providers` lists the model providers the account's files use and the last daily key check — `--check` asks now, and a dead key is a non-zero exit. `export` downloads everything the platform holds about the account — workspaces, runs, members and the whole ledger — as one JSON file (owner only; `--file <path>`, `--force`). Singular — `accounts` below is the list of logins on this machine |
|
|
132
|
+
| `foldrun account` | the account's own defaults — the AGENTS.md frontmatter every workspace under it inherits: `timezone`, `notify`, `budget`, `concurrency`. `foldrun account set timezone Australia/Sydney`, `set notify email you@example.com` (`--events failed,awaiting-approval,completed`), `set notify url https://…`, `set budget 60/month`, `set concurrency 4`, and `clear <key>` to unset one. `set notify` merges against what is there, because the platform replaces the whole block. `providers` lists the model providers the account's files use and the last daily key check — `--check` asks now, and a dead key is a non-zero exit. `model` is the account's own model key, what every step runs on unless a `provider:` block names another: `foldrun account model` shows it (never the key, only its last four; exit 1 when there is none and runs are refused), `model set <provider> --key <api key>` sets it — a preset name (anthropic, openrouter, openai, groq, …) or `custom --base-url https://… --format anthropic|openai`, `--key -` reads the key from stdin — and `model remove` removes it; a Claude login token is refused for any account not allowed the platform's own credential. `export` downloads everything the platform holds about the account — workspaces, runs, members and the whole ledger — as one JSON file (owner only; `--file <path>`, `--force`). Singular — `accounts` below is the list of logins on this machine |
|
|
116
133
|
| `foldrun workspace` | one deployed workspace's own settings (`--to <workspace>`): description, timezone, budget and notify, read from its `AGENTS.md` — unset means the account's. `set timezone <tz>`, `set budget 20/week`, `set notify email <address>` / `set notify url <https://…>` (`--events`; merged with what the file says), `set description "…"`, `set name <new>` — a rename changes every webhook URL, so it asks first and names the flows whose URLs changed — and `clear <key>`. Singular — `workspaces` is the list |
|
|
117
134
|
| `foldrun workspace vocabulary` | what the dashboard's editor completes as you type in a workspace (`--to`): agents, flows, skills, tools, secrets (names only), scripts, document types and linkable documents (`GET …/vocabulary`; `--json`) |
|
|
118
135
|
| `foldrun notify test` | send one test notification to whatever the workspace's `notify:` resolves to, and print what happened — the provider's own words when it failed ("domain not verified"). Exits 1 when it did not go. `--to <workspace>` |
|
|
@@ -120,12 +137,13 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
120
137
|
| `foldrun webhooks redeliver <id>` | send one delivery again now, with the same `X-Foldrun-Delivery` id; prints the attempt and exits 1 when it was refused. `--to <workspace>` |
|
|
121
138
|
| `foldrun notifications` | what mail you get, category by category: `always` for the required ones (security, invites, billing, account), `on`/`off` for the rest, and the workspaces where run alerts or approvals differ. `--json` |
|
|
122
139
|
| `foldrun notifications set <category> on\|off` | turn one category off, or back on — account-wide, or for one workspace with `--to <workspace>` (`run-alerts`, `approvals`). A required category is refused with the reason |
|
|
140
|
+
| `foldrun flags` | the feature flags for this account: each one `on` or `off` and why — `account` (set for this account), `global` (set for every account), `rollout` (a share of accounts, with its percentage) or `default` — and what it does. Read-only: only the platform's super admin changes a flag, in the console. `--json` for the API's answer as it came. See [Feature flags](feature-flags.md) |
|
|
123
141
|
| `foldrun history [path]` | every change to a deployed workspace, newest first: revision id, when, who, the message and the files. A path narrows it to one file; `--id <revision>` prints that revision as diffs; `--limit <n>`; `--to <workspace>` |
|
|
124
142
|
| `foldrun history restore <path> --id <revision>` | the History drawer's **Restore this version**: the file is rewritten with that revision's text as a new revision, `restored <rev> from history`, so the restore is itself undone the same way. Shows the diff against the file now and asks (`--yes`, `--dry-run`); sent with `ifMatch`, so a file that changed since is refused (`--to`) |
|
|
125
143
|
| `foldrun repo ls` | a workspace's repository: main, its branches and tags with their last commit, and the mirror. `repo diff <branch>` is what a branch changes against main; `repo deploy <commit\|branch\|tag>` makes that tree live (a rollback is a new commit on main, itself undoable); `repo merge <branch>` merges into main and deploys. Deploy and merge ask first (`--yes` with no terminal) and print the check issues or the run in flight that refused them |
|
|
126
144
|
| `foldrun repo rm-branch <branch>` | delete a branch (asks; `--yes`). `repo mirror <git-url>` sends every push to main there too, with the workspace's `MIRROR_TOKEN` (`repo mirror off` stops it); `repo mirror-now` pushes main now and prints what the remote said (`--to`) |
|
|
127
145
|
| `foldrun restore <workspace> --to <commit\|time\|age>` | put a workspace's source back as it was then — `--to` is a commit or its prefix, a branch or tag, a date/time (the last commit at or before it) or an age (`90m`, `12h`, `3d`, `2w`). Always shows the change first (`--dry-run`: the full diff and nothing else); the restore asks for the workspace's name typed back (`--yes` with no terminal) and sends the removals it showed, so a file added in between refuses it. A new commit on main, so it prints the `foldrun restore … --to <from>` that undoes it. Runs, `state/`, storage and secrets are not touched. See [Backups and restore](backups-and-restore.md) |
|
|
128
|
-
| `foldrun backups` | how the account is backed up: the schedule, how many are kept, the last snapshot (verified? copied off the server? encrypted?), the snapshots that hold the account, each marked encrypted or not, each workspace's last change, and earlier restore requests (`--limit`, `--json`). `backups request --what runs\|state\|storage\|everything --at "<when>" [--to <workspace>] [--backup <id>] [--note "…"]` asks the platform team for a restore from a snapshot — recorded and emailed; nothing is restored until they do it |
|
|
146
|
+
| `foldrun backups` | how the account is backed up: the schedule, how many are kept, the last snapshot (verified? copied off the server? encrypted?), the snapshots that hold the account, each marked encrypted or not, each workspace's last change, and earlier restore requests (`--limit`, `--json`). `backups request --what runs\|state\|storage\|everything --at "<when>" [--to <workspace>] [--backup <id>] [--note "…"]` asks the platform team for a restore from a snapshot — recorded and emailed; nothing is restored until they do it. `backups requests` lists every request this account has made, and where each stands (`GET /api/account/backups/requests`, `--json`) |
|
|
129
147
|
| `foldrun accounts` | every account signed in on this machine, and which one a bare command acts as. `foldrun profiles` is the same command |
|
|
130
148
|
| `foldrun use <name>` | act as one of them from here on. `foldrun switch <name>` is the same command |
|
|
131
149
|
| `foldrun keys ls` | the account's API keys, each with its role and scope, when it was created, when it was last used and from where (a /24 or /48), and when it expires — also `create <label>` (`--expires 30d\|90d\|365d\|never`), `rotate <id>` and `revoke <id>` |
|
|
@@ -139,13 +157,15 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
139
157
|
| `--workspace <dir>` | the workspace folder (default `.`). On `init` it names the first workspace instead |
|
|
140
158
|
| `--flat` | `init`: the single-folder shape — `agents/` and `flows/` at the root, no account around them |
|
|
141
159
|
| `--from <template>` | `init`: start from a shipped template, e.g. `templates/hello` |
|
|
160
|
+
| `--starter` | `new`, `workspaces new`: include the example researcher, writer and `publish` flow — a new workspace is otherwise blank |
|
|
161
|
+
| `--as <name>` | `agent import`: the name the copy gets in this workspace (rewrites its `name:`); `storage put`: the path one uploaded file lands at |
|
|
142
162
|
| `--transport <k>` | `tool new`: `script` (the default — a folder with its program beside it), `http` or `mcp` |
|
|
143
|
-
| `--language <l>` | `tool new`: the script's language — `javascript`, `python` or `bash` |
|
|
163
|
+
| `--language <l>` | `tool new`, `source new`, `library new`: the script's language — `javascript`, `python` or `bash` |
|
|
144
164
|
| `--task "<text>"` | the instruction for a manual run |
|
|
145
165
|
| `--test` | `run`, `invoke`, `agent run`: a test run — every step runs, nothing outward happens, `state/` and `storage/` are left alone, and the run page lists what would have happened. See [Test runs](runs#test-runs) |
|
|
146
166
|
| `--follow` | `logs`: keep tailing a live run — locally, or on the platform with `--url` |
|
|
147
167
|
| `--status <s>` | `runs`, and a bulk `stop` or `rerun`: only these statuses, comma-separated — `failed`, `completed`, `awaiting-approval`, `running`, `queued` |
|
|
148
|
-
| `--since <span>` | `runs`, and a bulk `stop` or `rerun`: only runs started within `24h`, `7d`, `90m`, `2w`. `observe` and `triggers` take a number of days instead |
|
|
168
|
+
| `--since <span>` | `runs`, and a bulk `stop` or `rerun`: only runs started within `24h`, `7d`, `90m`, `2w`. `observe` and `triggers` take a number of days instead (sent as `?days=`). `--until <span\|date>` on a bulk `stop` or `rerun`: only runs started before then |
|
|
149
169
|
| `--since <span>` (audit) | `audit`: entries from `24h`, `7d` (default), `30d` ago or a date; `--until <date>` ends the window |
|
|
150
170
|
| `--action <a>` | `audit`: one action (`apikey.created`) or a family (`apikey`, `secret`, `member`, `support`) |
|
|
151
171
|
| `--actor <who>` | `audit`: one person or key — an email, `key:<label>`, `support:<email>` |
|
|
@@ -164,15 +184,16 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
164
184
|
| `--csv` (audit) | `audit`: the log as CSV, for an auditor — `--file <path>` saves it |
|
|
165
185
|
| `--threshold`, `--amount` | `billing wallet set auto-top-up`: refill `--amount` (5–500 USD) when the balance falls below `--threshold` |
|
|
166
186
|
| `--name`, `--abn`, `--address`, `--email` | `billing details set`: the legal name, the ABN, `"line1, city, state, postcode, AU"` (the last part is the two-letter country), the receipt email |
|
|
167
|
-
| `--days <n>` | `usage`: the window the charges cover (default 56) |
|
|
187
|
+
| `--days <n>` | `usage`: the window the charges cover (default 56). `status --platform --history`: 1–90 days (default 90) |
|
|
168
188
|
| `--id <revision>` | `history`: one revision, as diffs |
|
|
169
189
|
| `--engine <e>` | `login <site>`: the browser to sign in with — `chrome` (default; it signs in with Chromium, so `chromium` is the same), `firefox` or `safari` (`webkit`). Not `lightpanda` or `obscura`, which keep no profile to sign in with — the full list is [Engines](tools#engines) |
|
|
170
190
|
| `--new-client` | `connect`: enter a new OAuth client even when one is saved for the secret |
|
|
171
191
|
| `--limit <n>` | `runs`, `billing`, `history`: how many rows, newest first (`runs` default 20, `billing` 15). Each workspace is asked for its own newest `n` before the merge, so one busy desk cannot crowd out a quiet one |
|
|
172
192
|
| `--step <n>` | `approve`, `reject`: decide one gate, numbered as `report` prints it. Without it, every step of that run which is waiting |
|
|
173
|
-
| `--
|
|
193
|
+
| `--payload <json\|text\|@file>` | `approve`: release a step waiting on `wait: event` with this body (`POST …/approve` `{ payload }`). JSON is sent as JSON, anything else as text; `@file` reads a file |
|
|
194
|
+
| `--note "<text>"` | `approve`: guidance the approved step reads in its prompt — "approve, but skip the Sydney batch". `reject`: the reason, recorded on the trace. `billing credit`: the ledger line's note |
|
|
174
195
|
| `--yes` | skip the confirmation, deliberately: `approve`, `stop`, `deploy` (when it would delete files), and a bulk `stop` or `rerun`, `flow rotate-hook`, `flow draft`, `history restore`, `restore`, `billing plan`, `repo deploy`/`merge`, `workspace set name`, and every delete — `runs rm`, `storage rm`, `storage unshare`, `source rm`, `secrets rm`, `keys revoke`, `workspaces rm`. Required when there is no terminal to ask; without it nothing is deleted |
|
|
175
|
-
| `--json` | `report`: the raw run record instead of the report. `observe`, `usage`: the raw document |
|
|
196
|
+
| `--json` | `report`: the raw run record instead of the report. `observe`, `usage`, `workspaces`, `workspaces show`, `agent ls`, `flow ls`, `agent import --list`, `backups requests`, `status --platform --history`, `team`: the raw document |
|
|
176
197
|
| `--events <a,b>` | `account set notify`: which events are notified on — `failed`, `awaiting-approval`, `completed` |
|
|
177
198
|
| `--watch` | `invoke`: follow the run's trace here as it happens; the exit code is the run's |
|
|
178
199
|
| `--print` | `open`: print the URL, do not open it |
|
|
@@ -192,6 +213,13 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
192
213
|
| `--force` | `deploy`: deploy while runs are in flight. `pull`, `storage get`: overwrite local files |
|
|
193
214
|
| `--no-runtimes` | `deploy`: do not wait for the workspace's environments to be built, or print them |
|
|
194
215
|
| `--platform` / `--yes` | `workspaces rm`: delete it on the platform, said twice because it cannot be undone |
|
|
216
|
+
| `--platform` | `workspaces new`, `agent new`, `flow new`, `tool new`: make it on the platform, not in this folder. `status`: the platform's own health (`--history` for the daily record) |
|
|
217
|
+
| `--description`, `--model`, `--pattern`, `--template` | `agent new --platform`: its description and model tier. `flow new --platform`: the shape to start from. `tool new --platform`, `source new`, `library new`: a transport or gallery entry to start from |
|
|
218
|
+
| `--list` | `agent import`: what could be imported, importing nothing |
|
|
219
|
+
| `--on <event>` | `hook add`: what the hook listens for — `tool.call`, `tool.result`, `step.start` or `step.end` |
|
|
220
|
+
| `--tool <pattern>` | `hook add`: which tools a `tool.call` or `tool.result` hook sees — `Bash`, `Write\|Edit`, `mcp__*`; all of them when unset |
|
|
221
|
+
| `--library` | `hook add`, `hook rm`: the account library's `library/hooks/`, not this workspace's |
|
|
222
|
+
| `--event <json\|@file>` | `hook test`: the event the hook reads on stdin (default: a sample for its `on:`) |
|
|
195
223
|
| `--path <p>` | `tool test`: the path an http tool should probe, appended to its `base:` — a path that escapes the base is refused, the same confinement the agent gets |
|
|
196
224
|
| `--timeout <s>` | how many seconds one request to the platform may take — or `FOLDRUN_TIMEOUT`. Default 30, and 300 for `tool test`, where the tool itself is the thing being waited on |
|
|
197
225
|
| `--local` | `deploy`, `secrets`, `logs`: the installation on this machine, even when signed in |
|
package/docs/coding-agents.md
CHANGED
|
@@ -85,12 +85,13 @@ api workspaces/$WS/flows/<flow>/run -X POST # start one
|
|
|
85
85
|
| `/runs/<id>/stream` | GET | live events, server-sent |
|
|
86
86
|
| `/runs/<id>/stop` · `/rerun` · `/approve` | POST | kill it; again from a step; release a gate |
|
|
87
87
|
| `/flows` · `/flows/<f>` · `/flows/<f>/run` | GET POST · POST DELETE PATCH · POST | list, create, edit, start |
|
|
88
|
-
| `/agents` · `/agents/<a>/run` | GET POST · POST | list agents, run one |
|
|
88
|
+
| `/agents` · `/agents/<a>/run` · `/agents/import` | GET POST · POST · GET POST | list agents, run one, copy one in from another workspace |
|
|
89
89
|
| `/evals` · `/evals/<e>/run` | GET POST · POST | list evals, run one |
|
|
90
90
|
| `/tools/<t>/test` | POST | exercise one tool alone |
|
|
91
91
|
| `/storage` · `/storage/download` · `/storage/upload-url` · `/storage/preview` · `/shares` | GET POST PUT DELETE · GET · POST · GET · POST GET DELETE | files agents produced and people uploaded, any type; a file parsed for reading; public links to them |
|
|
92
92
|
| `/triggers?since=` | GET | why nothing ran: fired vs started per flow, with reasons |
|
|
93
93
|
| `/hooks/<f>/rotate` | POST | new webhook token for a flow |
|
|
94
|
+
| `/export` · `/import` | GET · POST | the workspace, a flow (`?kind=flow&name=`) or an agent as a .zip; take one in (`?dryRun=1` first, `?overwrite=1` to replace) |
|
|
94
95
|
| `/restore` | POST | the workspace's source back to a point in its history — `dryRun` first, then `confirm` |
|
|
95
96
|
| `/notify/deliveries` · `/notify/deliveries/<id>/redeliver` | GET · POST | every notify: webhook sent and each attempt; send one again |
|
|
96
97
|
| `/api/secrets` · `/api/schedule` · `/api/account` · `/api/keys` | account-level | the vault, the clock, defaults, API keys |
|
|
@@ -193,6 +194,7 @@ provider: {} # BYOK: your own model credential
|
|
|
193
194
|
permissionMode: plan # plan first, act once approved
|
|
194
195
|
runtime: # only if scripts need packages
|
|
195
196
|
packages: [requests]
|
|
197
|
+
system: [ffmpeg] # Debian packages, apt-installed before the step
|
|
196
198
|
---
|
|
197
199
|
|
|
198
200
|
You research a topic and report what you found...
|
|
@@ -202,8 +204,12 @@ Only `name` and the prose are required. Every other field widens or narrows
|
|
|
202
204
|
what the agent can reach, and the default is narrow.
|
|
203
205
|
|
|
204
206
|
`runtime:` installs pip/npm packages in the sandbox each agent runs in. Pin
|
|
205
|
-
versions the normal way (`pandas>=2`, `lodash@^4`).
|
|
206
|
-
|
|
207
|
+
versions the normal way (`pandas>=2`, `lodash@^4`). `system:` adds Debian
|
|
208
|
+
packages — a program (`ffmpeg`), a library's headers for a pip package that
|
|
209
|
+
compiles (`libcairo2-dev`, `python3-dev`), `build-essential` — installed with
|
|
210
|
+
apt before the step starts and cached, so later runs only unpack them. An agent that
|
|
211
|
+
finds it needs something mid-run installs it itself with the `install` tool
|
|
212
|
+
(apt, pip or npm), which every agent has; declaring is only a speed-up.
|
|
207
213
|
|
|
208
214
|
Link documents to each other with `[[wikilinks]]` — the name, not the path.
|
|
209
215
|
|
package/docs/deploying.md
CHANGED
|
@@ -68,7 +68,11 @@ is its commit.
|
|
|
68
68
|
|
|
69
69
|
A rollback is a deploy of an earlier ref (`POST …/repo {action: deploy, ref}`
|
|
70
70
|
or the History page), and it lands as a **new commit on `main`** — history
|
|
71
|
-
moves forward, never rewrites.
|
|
71
|
+
moves forward, never rewrites. **Preview** beside "Deploy this commit" (and
|
|
72
|
+
"Preview deploy" beside a branch's "Merge & deploy" on the Repository page)
|
|
73
|
+
shows first what it would change, add and remove, with diffs, and any run in
|
|
74
|
+
flight that would hold it — the restore dry run
|
|
75
|
+
(`POST …/restore {to, dryRun: true}`), which is the same operation.
|
|
72
76
|
|
|
73
77
|
## Mirroring
|
|
74
78
|
|
package/docs/faq.md
CHANGED
|
@@ -13,8 +13,11 @@ with a team.
|
|
|
13
13
|
**Which models does it use?** Whatever you point it at. `model:` takes a
|
|
14
14
|
tier — `fast`, `default`, `max` — and the platform resolves the tier to a
|
|
15
15
|
model. Bring your own key for any provider that speaks the Anthropic or the
|
|
16
|
-
Chat Completions API
|
|
17
|
-
|
|
16
|
+
Chat Completions API — once for the whole account in Settings → Model
|
|
17
|
+
(`foldrun account model set`), or per workspace or agent with a `provider:`
|
|
18
|
+
block — and the platform bills you nothing for those tokens. On a hosted
|
|
19
|
+
platform every account brings its own API key; Claude login tokens are not
|
|
20
|
+
accepted. See [Providers](providers).
|
|
18
21
|
|
|
19
22
|
**Can agents reach the internet?** Yes — the public internet, as themselves,
|
|
20
23
|
with the secrets they were granted. Not private networks, not the cloud
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Feature flags
|
|
2
|
+
|
|
3
|
+
Some changes ship dark: the code is deployed, and whether an account gets
|
|
4
|
+
the new behaviour is a decision made afterwards, by the platform team, one
|
|
5
|
+
account or a share of accounts at a time. That decision is a feature flag.
|
|
6
|
+
|
|
7
|
+
An account cannot change its flags. It can see them: **Settings → Feature
|
|
8
|
+
flags** in the dashboard, `foldrun flags` in a terminal, or
|
|
9
|
+
`GET /api/account/flags`. Each flag says whether it is on for this account
|
|
10
|
+
and why.
|
|
11
|
+
|
|
12
|
+
## What decides a flag
|
|
13
|
+
|
|
14
|
+
The flags themselves are code — a name, what it changes, who owns it, a
|
|
15
|
+
default, and sometimes a rollout percentage. A name the code does not
|
|
16
|
+
register is not a flag; asking for one is a 404. What the platform's super
|
|
17
|
+
admin sets on top is checked in this order, and the first that has an
|
|
18
|
+
answer wins:
|
|
19
|
+
|
|
20
|
+
| | source | |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| 1 | `account` | this account's own override — on or off whatever else says |
|
|
23
|
+
| 2 | `global` | the install-wide setting — on or off for every account without an override |
|
|
24
|
+
| 3 | `rollout` | a percentage of accounts. Each account falls in a fixed bucket from 0 to 99, a hash of the account and the flag's names, so the same account gets the same answer on every server and every day, and raising the percentage only adds accounts — nobody who had it loses it |
|
|
25
|
+
| 4 | `default` | what the code says when nothing else does |
|
|
26
|
+
|
|
27
|
+
A change is seen at once on the server that made it, and on every other
|
|
28
|
+
within ten seconds.
|
|
29
|
+
|
|
30
|
+
## The example
|
|
31
|
+
|
|
32
|
+
`example-flag` is registered, off by default, and read by nothing: turning
|
|
33
|
+
it on changes no behaviour. It is there to try the console, the API and the
|
|
34
|
+
CLI against without risk.
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
foldrun flags # every flag, on or off for this account, and why
|
|
38
|
+
foldrun flags --json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{ "flags": [ { "name": "example-flag", "description": "The registered example. …",
|
|
43
|
+
"owner": "platform", "default": false, "on": false,
|
|
44
|
+
"source": "default", "rollout": null } ] }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Running the platform yourself
|
|
48
|
+
|
|
49
|
+
On a hosted install the super admin changes flags at **/admin → Flags**:
|
|
50
|
+
the install-wide on/off and rollout, and an override per account. The same
|
|
51
|
+
is `PUT`/`DELETE /api/admin/flags/<flag>` and
|
|
52
|
+
`/api/admin/flags/<flag>/accounts/<account>` (see the API reference's
|
|
53
|
+
super admin section). Every change is on the platform's audit as
|
|
54
|
+
`operator.flag.global`, `operator.flag.global_cleared`,
|
|
55
|
+
`operator.flag.account` or `operator.flag.account_cleared`, with who made
|
|
56
|
+
it and what it was before. The settings are rows in Postgres
|
|
57
|
+
(`feature_flags`), or `<data>/.feature-flags.json` without a database; an
|
|
58
|
+
account's overrides go when the account is deleted, and their audit stays.
|
|
59
|
+
|
|
60
|
+
A flag is added by a line in `foldrun-platform/src/flags.ts` and asked with
|
|
61
|
+
`flagOn(account, "<name>")`. Retire it by deleting the line and the code
|
|
62
|
+
that asks, once it is on for everyone; a row left behind for a name the
|
|
63
|
+
code no longer registers is ignored.
|
package/docs/flows.md
CHANGED
|
@@ -109,12 +109,31 @@ begins.
|
|
|
109
109
|
| `limits:` | this step's call limits, on one line — `limits: {web.search: 10, calls: 50}` — over the agent's, key by key. A call past one is refused, never run ([Tools](tools#limits--how-many-calls-a-step-may-make)) |
|
|
110
110
|
| `verify:` | a shell command, or an assertion (`contains:`, `matches:`, `file:`, `judge:`). The shell runs in the agent's folder and gets the step's final turn in the file `$FOLDRUN_REPLY_FILE`; `workspace/…` reaches the workspace root there, and in `file: workspace/storage/x.md`; `node workspace/tools/x/check.mjs` runs as its own main module, so a check it makes counts ([Agents](agents#where-files-live-from-the-agents-point-of-view)). The check runs on what is left of the step's `timeout:` (at least 30 s of it); with no `timeout:` it has no clock either. A stop ends it |
|
|
111
111
|
| `approve:` | park until a person releases it |
|
|
112
|
-
| `ask:` | the same gate carrying a question; the typed answer reaches the prompt |
|
|
112
|
+
| `ask:` | the same gate carrying a question; the typed answer reaches the prompt (a plain approval with no note reaches it as "approved with no note") |
|
|
113
113
|
| `preview:` | what the gate shows: paths under `storage/` (a `workspace/storage/` prefix is accepted), comma-separated, globs allowed — `draft/*.mdx, draft/images/*.webp`; at most 12 files |
|
|
114
|
-
| `wait:` | `3d` (`s`/`m`/`h`/`d`, capped at 30 days) or `event` |
|
|
115
|
-
| `on-fail:` | another agent takes the step over, with the failure as context |
|
|
114
|
+
| `wait:` | `3d` (`s`/`m`/`h`/`d`, capped at 30 days) or `event` — the run page shows the event URL and can release it with a payload, recorded as released by you (approve with `payload`) |
|
|
115
|
+
| `on-fail:` | another agent takes the step over, with the failure as context. The rescuer owes what the step owed: its `verify:` and its `output:` |
|
|
116
116
|
| `delegate:` | the step's agent picks who runs next, from this set |
|
|
117
117
|
|
|
118
|
+
### What is retried without a `retry:`
|
|
119
|
+
|
|
120
|
+
Some failures are the platform's or the network's, not the step's, and are
|
|
121
|
+
tried again whoever wrote the flow:
|
|
122
|
+
|
|
123
|
+
| what went wrong | what happens |
|
|
124
|
+
|---|---|
|
|
125
|
+
| the step's model connection was cut (the worker or the egress proxy restarted) | the step runs again, up to twice |
|
|
126
|
+
| the model wrote a tool call out as text instead of making it, so nothing ran | the step runs again once, told what went wrong |
|
|
127
|
+
| a connected account's token (`oauth2` secret) could not be refreshed for a network blip, a 5xx or a 429 | tried three times, about 1 s and 3 s apart; a refusal (revoked, wrong client) is not retried and mails the reconnect link |
|
|
128
|
+
| a `web` provider timed out or answered busy | tried three times, about 2 s and 6 s apart (see the web tool) |
|
|
129
|
+
|
|
130
|
+
None of the step-level ones runs again once a tool that can act outside the
|
|
131
|
+
workspace has run in that attempt: the post may already be up. When a step
|
|
132
|
+
still fails, the run says so — `notify:` sends `failed` (and `blocked`, for a
|
|
133
|
+
run that finished but refused), and the run page's **Re-run from here**,
|
|
134
|
+
`foldrun rerun <run-id> --from <n>` or `POST …/runs/<id>/rerun` start it
|
|
135
|
+
again once the cause is fixed.
|
|
136
|
+
|
|
118
137
|
### Routing
|
|
119
138
|
|
|
120
139
|
```markdown
|
|
@@ -272,10 +291,10 @@ decided by a number belongs in a command that can be tested:
|
|
|
272
291
|
`contains:`, `not-contains:` and `matches:` test the step's **conclusion** —
|
|
273
292
|
its final reply, the same text the run's headline is read from — so a step
|
|
274
293
|
that narrated between tool calls and then opened its answer with `BAD — …`
|
|
275
|
-
passes `matches: ^BAD\b`. `judge:` grades the
|
|
294
|
+
passes `matches: ^BAD\b`. `judge:` grades the same conclusion as "the reply", with everything the step wrote before it shown alongside as context, so a claim about the work can still be checked.
|
|
276
295
|
|
|
277
296
|
The command sees what the step's scripts saw — its secrets, `FOLDRUN_RUN_ID`,
|
|
278
|
-
`FOLDRUN_AGENT`, `FOLDRUN_WORKSPACE`, `FOLDRUN_DATE`, `TZ` — so a marker a step leaves on disk can
|
|
297
|
+
`FOLDRUN_AGENT`, `FOLDRUN_WORKSPACE`, `FOLDRUN_DATE`, `TZ`, and `FOLDRUN_API_URL` (this install's own API, when it has a public address) — so a marker a step leaves on disk can
|
|
279
298
|
be checked to name *this* run. That matters: a run's copy-back never
|
|
280
299
|
propagates a deletion, so a marker from an earlier run rides into every later
|
|
281
300
|
sandbox. `judge:` is a toolless fast-tier grading call; the other four cost
|
package/docs/hooks.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Hooks
|
|
2
|
+
|
|
3
|
+
A hook is your own code at the points every step passes through: before a
|
|
4
|
+
tool runs, after it ran, when a step starts and when it ends. It can refuse
|
|
5
|
+
a call, rewrite it, add context, or rewrite the reply, and it does the same
|
|
6
|
+
thing on every model: Claude, or any other provider through the translator.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
workspaces/<name>/hooks/no-force-push/
|
|
10
|
+
hook.md when it runs
|
|
11
|
+
run.mjs what it does
|
|
12
|
+
library/hooks/<name>/ the same, shared by every workspace
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Turning one on
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
# agents/deployer/agent.md
|
|
19
|
+
hooks: [no-force-push]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`hooks:` in an agent's frontmatter turns hooks on for that agent. The same
|
|
23
|
+
key in a workspace's `AGENTS.md` turns them on for every agent in it, and in
|
|
24
|
+
the account's `AGENTS.md` for every agent in every workspace. They run in
|
|
25
|
+
that order (account, workspace, agent), each name once. A name is looked up
|
|
26
|
+
in the workspace's `hooks/` first, then the library's. A name that resolves
|
|
27
|
+
to nothing is a `foldrun check` error, and a step that names one is refused
|
|
28
|
+
rather than run without its hooks.
|
|
29
|
+
|
|
30
|
+
## hook.md
|
|
31
|
+
|
|
32
|
+
```markdown
|
|
33
|
+
---
|
|
34
|
+
on: tool.call # tool.call | tool.result | step.start | step.end
|
|
35
|
+
tool: Bash # which tools; Write|Edit, mcp__*, a short name like web
|
|
36
|
+
timeout: 30 # seconds, 1-120 (30 for tool.call/step.start, 60 otherwise)
|
|
37
|
+
description: Refuses force pushes and skipped git hooks.
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
Why this hook exists, for the next person who reads it.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| `on:` | when | it can |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `tool.call` | before a tool runs | allow it, refuse it, or rewrite its input |
|
|
46
|
+
| `tool.result` | after a tool ran | see the input and the result; leave a note on the trail |
|
|
47
|
+
| `step.start` | before the model starts | add context to the task, or refuse the step |
|
|
48
|
+
| `step.end` | after the reply | see it, or replace it |
|
|
49
|
+
|
|
50
|
+
## run.mjs
|
|
51
|
+
|
|
52
|
+
One self-contained ES module, using Node's built-ins only (no relative
|
|
53
|
+
imports), at most 100 KB. The event arrives as one JSON object on stdin, and
|
|
54
|
+
the answer is the last line of JSON the hook prints:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
let raw = "";
|
|
58
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
59
|
+
const event = JSON.parse(raw);
|
|
60
|
+
// { event: "tool.call", hook, run_id, step, agent, workspace,
|
|
61
|
+
// tool: { name: "Bash", input: { command: "git push --force" } }, subagent? }
|
|
62
|
+
|
|
63
|
+
const cmd = String(event.tool?.input?.command ?? "");
|
|
64
|
+
if (/\bgit\s+push\b.*\s(--force|-f)(\s|$)/.test(cmd)) {
|
|
65
|
+
console.log(JSON.stringify({ decision: "deny", reason: "force push — use --force-with-lease" }));
|
|
66
|
+
} else {
|
|
67
|
+
console.log("{}");
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| event | answer |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `tool.call` | `{}` or `{"decision":"allow"}` lets it through; `{"decision":"deny","reason":"…"}` refuses it (the model reads the reason); `{"input":{…}}` replaces the tool's whole input |
|
|
74
|
+
| `tool.result` | `{}`, or `{"note":"…"}` for the trail |
|
|
75
|
+
| `step.start` | `{"context":"…"}` is added to the task; exit 2 refuses the step |
|
|
76
|
+
| `step.end` | `{"reply":"…"}` replaces the reply |
|
|
77
|
+
|
|
78
|
+
Exit code 2 refuses too, with what the hook printed to stderr as the reason,
|
|
79
|
+
which is the convention most agent tools share.
|
|
80
|
+
|
|
81
|
+
## When a hook goes wrong
|
|
82
|
+
|
|
83
|
+
A hook that guards (`tool.call`, `step.start`) and throws, times out, exits
|
|
84
|
+
with anything but 0 or 2, or answers with the wrong shape **refuses the
|
|
85
|
+
call**. A guard that crashes must not become an open door. A hook that
|
|
86
|
+
watches (`tool.result`, `step.end`) and fails is recorded, and the step goes
|
|
87
|
+
on, because what it was watching has already happened.
|
|
88
|
+
|
|
89
|
+
When several hooks match one call they run in order. The first refusal ends
|
|
90
|
+
it, and a rewrite is handed to the next hook, so the most restrictive answer
|
|
91
|
+
wins.
|
|
92
|
+
|
|
93
|
+
## What a hook can reach
|
|
94
|
+
|
|
95
|
+
No more than the agent. A hook runs inside the step's own sandbox, as a child
|
|
96
|
+
of the step, in the agent's directory, with an environment stripped to `PATH`
|
|
97
|
+
and the locale: no secrets, no model key, and a home folder of its own. Its
|
|
98
|
+
code is read when the step starts, so an agent that edits `run.mjs` mid-step
|
|
99
|
+
changes nothing that runs, and a sandbox never writes `hooks/` back.
|
|
100
|
+
|
|
101
|
+
A rewritten tool call goes through the same checks as the original (the
|
|
102
|
+
shell and path confinement, the agent's granted tools, its sub-agents), so a
|
|
103
|
+
hook can narrow what an agent does but never widen it.
|
|
104
|
+
|
|
105
|
+
## What a hook does not see
|
|
106
|
+
|
|
107
|
+
Tools the model provider runs on its own side never pass foldrun's dispatch.
|
|
108
|
+
With `web: {search: anthropic}` (or another provider's search, or
|
|
109
|
+
`fetch: anthropic`) the search runs at the provider, and a `tool.call` hook
|
|
110
|
+
cannot refuse or rewrite it. foldrun's own `web` tool, the default, goes
|
|
111
|
+
through hooks like every other tool. `foldrun check` warns when an agent with
|
|
112
|
+
a `tool.call` hook uses a provider's own search.
|
|
113
|
+
|
|
114
|
+
## On the run page
|
|
115
|
+
|
|
116
|
+
Every refusal, rewrite, added context and failure is a line on the step's
|
|
117
|
+
trail (`hook no-force-push denied Bash: force push — use --force-with-lease`)
|
|
118
|
+
and a badge on the run page. The workspace's Hooks page lists its hooks and
|
|
119
|
+
what each one refused or rewrote lately.
|
|
120
|
+
|
|
121
|
+
## Trying one
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
foldrun hook add no-force-push --on tool.call --tool Bash
|
|
125
|
+
foldrun hook test no-force-push --event '{"tool":{"name":"Bash","input":{"command":"git push -f"}}}'
|
|
126
|
+
foldrun check
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`foldrun hook test` runs the hook on your machine exactly as a step runs it.
|
|
130
|
+
`POST /api/workspaces/<ws>/hooks/<name>/test` does the same on the platform
|
|
131
|
+
(see [API](api)).
|
|
132
|
+
|
|
133
|
+
## Two in the library to copy
|
|
134
|
+
|
|
135
|
+
- **no-force-push** refuses `git push --force` or `-f` (allowing
|
|
136
|
+
`--force-with-lease`), and `--no-verify` or `SKIP_HOOKS=1`.
|
|
137
|
+
- **external-write-log**: when the shell calls `curl` to change something on
|
|
138
|
+
an outside service (POST, PUT, PATCH, DELETE), it writes a line on the
|
|
139
|
+
trail.
|
package/docs/notifications.md
CHANGED
|
@@ -140,6 +140,12 @@ The body, and the webhook's JSON:
|
|
|
140
140
|
that wants the data. A webhook URL is the one integration: those services,
|
|
141
141
|
and every phone-push relay, all accept a POST of that shape.
|
|
142
142
|
|
|
143
|
+
Every delivery says which shape its body has, in `x-foldrun-webhook-version`
|
|
144
|
+
(today `1`, also in the `foldrun-webhooks/1` user agent). Within a version the
|
|
145
|
+
body only gains fields, so ignore the ones you do not know. A field removed,
|
|
146
|
+
renamed or given another type is a new version, announced in What's new at
|
|
147
|
+
least six months before the old shape stops being sent.
|
|
148
|
+
|
|
143
149
|
## Webhook deliveries, retries and redelivery
|
|
144
150
|
|
|
145
151
|
Every webhook send is a **delivery**: recorded first, attempted at once,
|
|
@@ -290,7 +296,7 @@ sending, so it holds for every mail, from every part of the platform.
|
|
|
290
296
|
| `security` | password resets, email confirmations, a login moved, an account made for you | no |
|
|
291
297
|
| `invites` | an invitation to join an account | no |
|
|
292
298
|
| `billing` | a payment failed, and the grace before runs stop | no |
|
|
293
|
-
| `account` | a notification webhook switched off after days of failures; a close request | no |
|
|
299
|
+
| `account` | a notification webhook switched off after days of failures; a close request; an API route the account still calls that is being switched off | no |
|
|
294
300
|
| `run-alerts` | a workspace's `notify:` email: failed, completed, blocked, quarantined, `sla`, `budget` | yes — per workspace |
|
|
295
301
|
| `approvals` | a run waiting for you, with approve and reject links | yes — per workspace |
|
|
296
302
|
| `low-balance` | credits below the warning level | yes |
|