foldrun 2.5.0 → 2.7.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/bin/foldrun.mjs CHANGED
@@ -15,6 +15,7 @@
15
15
  // foldrun runs what has run lately, across the account
16
16
  // foldrun report <run-id> one run, whole: every step, what it cost, what it wrote
17
17
  // foldrun approvals what is waiting for a person, anywhere in the account
18
+ // foldrun inbox what is waiting on YOU — assigned, started by you, the team's (read, dismiss)
18
19
  // foldrun approve <run-id> release a waiting gate (asks first) — reject refuses it
19
20
  // foldrun stop <run-id> kill a run in flight
20
21
  // foldrun account the account's own defaults — timezone, notify, budget, concurrency, deploy_block, network_enforce
@@ -36,6 +37,9 @@
36
37
  // foldrun agent new <name> one more agent in this workspace — also flow new, tool new (--platform: in the deployed one)
37
38
  // foldrun agent ls the agents deployed in a workspace — also flow ls
38
39
  // foldrun eval new <name> a new eval in a deployed workspace
40
+ // foldrun eval compare <eval> its cases under two to four models, side by side
41
+ // foldrun proposals proposed fixes for repeated failures — show, apply, dismiss
42
+ // foldrun eval grades <eval> what a grader found in the real runs it graded — eval grade <eval> --run <id> grades one
39
43
  // foldrun agent run <name> run one agent once on a platform, no flow
40
44
  // foldrun tool test <name> exercise one tool alone — no model, no run
41
45
  // foldrun hook <verb> ls / add / test / rm — your own code at every tool call and step
@@ -122,6 +126,7 @@ const HELP = `foldrun — agents are just folders
122
126
  foldrun flow rotate-hook <flow> a new webhook URL for a flow; the old one stops at once — asks first, --yes means it
123
127
  foldrun flow set <flow> --step <n|agent> key=value … the step editor: model, effort, retry, timeout, verify, when, case, else, loop, until, each, max, limits, instruction (key= clears) — diff, checked, --dry-run
124
128
  foldrun flow trigger <flow> manual|webhook|schedule the trigger picker (--schedule "<cron>", --timezone <zone>) — diff, checked, --dry-run
129
+ foldrun flow filter <flow> --payload <file|-|json> would this delivery start it? its filter: and the condition that held it back (--to <ws>: the deployed flow); exit 1 when not
125
130
  foldrun flow move-step <flow> --step <n|agent> drag a card: --group <g> runs it beside group g, --after <g> gives it its own group after g (0 = first) — diff, checked, --dry-run
126
131
  foldrun flow copy-step <flow> --step <n|agent>[,<n>…] the steps' markdown on stdout, as the canvas's Copy
127
132
  foldrun flow paste <flow> step markdown from stdin or --file in as new groups (--after <g>, default last) or beside one (--group <g>) — diff, checked, --dry-run
@@ -137,6 +142,11 @@ const HELP = `foldrun — agents are just folders
137
142
  foldrun run <target> run an agent or flow HERE (target: name, or flow:name) — on the platform: flow run / invoke a flow, agent run one agent
138
143
  foldrun eval [name] run one eval, or all of them (--to <workspace>: the deployed evals, run on the platform; --last reads their latest results, --run <id> one run's verdict, running nothing)
139
144
  foldrun eval new <name> a new eval in the deployed workspace, its agent the first one there — edit its cases, then eval <name> --to (--to)
145
+ foldrun eval compare <eval> --models fast,default --to <ws> the cases under 2–4 models side by side (m:effort, --agent, --repeat, --max-usd; --list, --run <id>)
146
+ foldrun eval improve <eval> --to <ws> ask the assistant for a proposed fix from the failures, wait for the draft (--no-wait)
147
+ foldrun proposals --to <ws> proposed fixes (--status ready, --eval) — show|apply|dismiss <id> (apply asks; --yes)
148
+ foldrun eval grades <eval> --to <ws> a grader's findings (trigger: runs): pass rate over --days (30), each graded run (--status fail, --limit); exit 1 on a failure
149
+ foldrun eval grade <eval> --run <id> --to <ws> grade one finished run now, sampled or not; exit 1 unless it passed
140
150
  foldrun promote <run-id> keep a finished run as a regression case in evals/ (--eval <name>, --expect "contains: …" repeatable, --case <name>); a run that went wrong takes --should "what it should have done"
141
151
  foldrun probe <model> live check: can this model hold a tool loop here?
142
152
  foldrun logs [run-id] recent runs in one workspace, or one run's full event trail (--local: this machine's)
@@ -153,6 +163,8 @@ const HELP = `foldrun — agents are just folders
153
163
  foldrun report <run-id> live the live browser view: which agents have a frame; --agent <name> saves the newest as a JPEG (--file <path>|-)
154
164
  foldrun approvals every gate waiting on a person, with its question (--to <workspace> for one)
155
165
  foldrun insights recurring problems found in the runs, with what they cost and what to do (--days, --to, --json)
166
+ foldrun inbox what waits on you: approvals, questions, issues, proposed fixes, your failed runs (--team, --all, --kind, --unread, --dismissed)
167
+ foldrun inbox read|unread|dismiss|restore <id>… mark items in your own view (dismiss sets aside; it still waits where it lives)
156
168
  foldrun issues what went wrong, grouped, open until resolved (--to <workspace>, --all, --status, --json)
157
169
  foldrun issues show|ack|snooze|resolve|reopen <id> one issue — snooze --for 1d or --times 5, --note "…"
158
170
  foldrun issues doctor <id> the run doctor on the issue's newest run, in its workspace: why, and the fix as diffs, saved on yes (--yes, --dry-run, --json)
@@ -203,7 +215,7 @@ const HELP = `foldrun — agents are just folders
203
215
  foldrun backups requests every restore asked of the platform team, and where each stands (--json)
204
216
  foldrun secrets set NAME store a secret (prompted, never echoed) — also ls, rm, status; --kind file|ssh|api|service-account|m2m for the other shapes; --site <domain> (repeatable) binds it as it is stored
205
217
  foldrun secrets sites NAME [site…] bind a secret to sites: the egress proxy fills it for those alone and it never enters a sandbox; no sites lists them, --clear unbinds
206
- foldrun secrets clients the OAuth clients saved on the platform — also add <name> (--provider or --authorize-url/--token-url, --client-id, --scopes), rm <name>
218
+ foldrun secrets clients the OAuth clients saved on the platform — also add <name> (--authorize-url/--token-url, --client-id, --scopes, --authorize-param name=value), rm <name>
207
219
  foldrun connect NAME OAuth sign-in from the terminal, stored as an auto-refreshing secret — --client <saved> runs it from a saved client through the platform
208
220
  foldrun deploy [dir] push the whole account, or deploy <workspace> for one of them
209
221
  foldrun pull [workspace] bring the platform's account down here (refuses to clobber; --force overrides)
@@ -278,6 +290,11 @@ Options
278
290
  --on-fail <agent> flow add: who takes the step over when it fails
279
291
  --off flow add: take the pattern off the step instead (fan-out, loop, approval, ask, wait, rescue)
280
292
  --schedule "<cron>" flow trigger: the cron line a schedule fires on (5 fields, or @daily); --timezone <zone> the zone it is read in
293
+ --team, --started, --mine, --all inbox: which bucket (default: assigned to you and started by you)
294
+ --dismissed, --unread inbox: what you set aside; only what you have not read
295
+ --models <m[:effort],…> eval compare: two to four tiers or model ids to compare, each with an optional :effort
296
+ --max-usd <n> eval compare: the spend limit — no run starts once the runs and judges have spent it (default 5)
297
+ --payload <file|-|json> flow filter: the delivery to test — a file, - for stdin, or JSON inline
281
298
  --kind <k> secrets set: file (--file <path>), ssh (--host, --user, --port, --key-file or a prompted password), api (--base-url, --header "Name: value" repeatable, or --basic-user <u> with the password prompted or --basic-password), service-account (--file <key.json>, --scopes, --subject <user to act as>), client-cert (--cert-file, --key-file, --passphrase, --export to put it in the sandbox as files too), identity (--audience: keyless cloud access, no credential stored), m2m (--token-url, --client-id, --scopes; secret prompted)
282
299
  --client <name> connect: run the consent from that OAuth client saved on the platform (secrets clients lists them)
283
300
  --subagent <name> agent link, agent unlink: a worker it delegates to (subagents:)
@@ -334,13 +351,14 @@ Options
334
351
  --print open: print the URL only
335
352
  --from <n> invoke, flow run, rerun: start at step n; earlier steps are recorded as skipped (a bulk rerun defaults to 1)
336
353
  --no-browser login, connect, billing top-up/card/plan: print the address instead of opening it
337
- --provider <name> connect: google, github, microsoft or linkedin (fills the URLs)
354
+ --provider <name> (deprecated — use --authorize-url, --token-url and --authorize-param) connect: google, github, microsoft or linkedin
338
355
  --no-pkce connect, secrets clients add: leave PKCE off (only for an app with a secret whose provider refuses it)
339
356
  --issuer <url> secrets clients add: the provider's issuer, checked against the iss it sends back (RFC 9207)
340
357
  --scopes "<a b c>" connect: space-separated scopes (default: the provider's example; with --client, the saved client's default — the connection keeps the ones asked for)
341
358
  --client-id / --client-secret connect: the OAuth app (prompted if omitted; secret never echoed)
342
359
  --port <n> connect: loopback port for the redirect (default 8642 — register http://localhost:8642/callback on the app)
343
- --authorize-url / --token-url connect: a provider with no preset
360
+ --authorize-url / --token-url connect, secrets clients add: the provider's own OAuth addresses
361
+ --authorize-param name=value connect, secrets clients add: an extra authorize-URL parameter the provider documents (repeatable — e.g. access_type=offline)
344
362
  --role <r> keys create: viewer, editor (default) or admin
345
363
  --expires <span> keys create: when it stops working — 30d, 90d, 365d or never (default never)
346
364
  --grace <span> keys rotate: keep the old key working for 1h or 24h while you swap the new one in (default 0: revoked now)
@@ -384,9 +402,9 @@ if (!command || command === "--help" || command === "-h") {
384
402
  // `--value` as the account's argument and stored an empty secret; `--force
385
403
  // ./dir` swallowed the directory. A flag followed by another flag is also
386
404
  // boolean, so an unlisted switch at least does not eat its neighbour.
387
- const BOOLEAN_FLAGS = new Set(["account", "follow", "force", "oauth2", "wait", "watch", "print", "dry-run", "help", "no-browser", "local", "test", "flat", "yes", "platform", "json", "forever", "all", "check", "new-client", "quiet", "off", "can-ask", "preview", "csv", "failed", "starter", "history", "list", "library", "clear", "security", "changed", "write-baseline", "no-baseline", "rules", "calls", "tree", "tool-error", "stop", "last", "no-pkce", "export"]);
405
+ const BOOLEAN_FLAGS = new Set(["account", "follow", "force", "oauth2", "wait", "watch", "print", "dry-run", "help", "no-browser", "local", "test", "flat", "yes", "platform", "json", "forever", "all", "check", "new-client", "quiet", "off", "can-ask", "preview", "csv", "failed", "starter", "history", "list", "library", "clear", "security", "changed", "write-baseline", "no-baseline", "rules", "calls", "tree", "tool-error", "stop", "last", "no-pkce", "export", "isolated", "no-wait", "dismissed", "unread", "team", "started", "mine"]);
388
406
  // Flags said more than once collect into a list: `--tag a --tag b`.
389
- const REPEATABLE = new Set(["tag", "expect", "header", "site", "add", "remove", "rule"]);
407
+ const REPEATABLE = new Set(["tag", "expect", "header", "site", "add", "remove", "rule", "authorize-param"]);
390
408
  // `--wait` is a switch everywhere but `flow add <flow> wait`, where it takes
391
409
  // a span: `--wait 30m`, `--wait event`. Only a value that reads as one is
392
410
  // taken, so `invoke x --wait` and friends are as they were.
package/docs/agents.md CHANGED
@@ -91,6 +91,7 @@ the point of the week; a step that weighs two arguments and picks one is not.
91
91
  | `skills` | an allowlist. **Absent inherits every skill in scope**; `[]` withholds all of them |
92
92
  | `agents` | colleagues it may consult mid-run — an answer, no tools |
93
93
  | `subagents` | colleagues it may delegate a whole job to — own context, own tools, never more than its own ([below](#delegating-to-a-sub-agent)) |
94
+ | `sandbox` | `own` \| `parent` (default) — delegated to as a sub-agent, run each job in a sandbox of its own, with its own tools, secrets and network ([below](#isolated-sub-agents-sandbox-own)) |
94
95
  | `scripts` | programs in `scripts/`, each becoming a callable tool |
95
96
  | `hooks` | your own code at every tool call, step start and step end — refuse, rewrite, add context — the same on every model ([Hooks](hooks)). Also in a workspace's or the account's `AGENTS.md` for every agent under it |
96
97
  | `apis` | an HTTP API declared inline, as one tool |
@@ -267,6 +268,79 @@ platform alike. `foldrun check` errors on a name that is not an agent and on a
267
268
  sub-agent with no description, and warns when a sub-agent would hold none of
268
269
  its tools inside the delegating agent's.
269
270
 
271
+ ### Isolated sub-agents: `sandbox: own`
272
+
273
+ A sub-agent shares its parent's sandbox: whatever it reads, it reads where the
274
+ parent's secrets are. For a sub-agent that reads untrusted content — web
275
+ pages, mail, uploads — put it in a sandbox of its own instead. The sub-agent
276
+ says so in its own file:
277
+
278
+ ```yaml
279
+ ---
280
+ name: reader
281
+ description: Reads the pages it is given and summarises them.
282
+ tools: [web]
283
+ sandbox: own
284
+ ---
285
+ ```
286
+
287
+ The parent is unchanged (`subagents: [reader]`). Its model gets a
288
+ `delegate_reader(job)` tool in place of the SDK's `Agent` tool for this one.
289
+
290
+ | | `subagents:` | `subagents:` + `sandbox: own` |
291
+ |---|---|---|
292
+ | where it runs | inside the parent's sandbox | a sandbox of its own, started for the job |
293
+ | tools | its own, cut to the parent's | its own — the parent may hold less |
294
+ | secrets, network, size, runtime | the parent's | its own file's |
295
+ | gives back | its reply | its reply, and the files it changed, copied into the parent's sandbox |
296
+ | on the run | lines in the parent's trace | a step of its own after the parent's, marked `subagentOf` |
297
+ | cost | on the parent's step | on its own step, in the run's total |
298
+
299
+ What holds:
300
+
301
+ - **Its file decides what it holds.** Its `tools:`, `secrets:`, `network:`,
302
+ `limits:`, `hooks:`, `runtime:`, `size:` and `model:` are read exactly as
303
+ for a step of its own. So a reader with `tools: [web]` and no secrets can
304
+ serve a writer with secrets and no web: a page that tells the reader to
305
+ send something finds nothing to send, and the writer never read the page.
306
+ `foldrun check` names what it holds that the parent does not
307
+ ([`security/subagent-own-sandbox`](checks#securitysubagent-own-sandbox)).
308
+ - **Only the reply comes back.** The reply is still text the parent reads —
309
+ it can carry an instruction — but it reaches a model whose sandbox never
310
+ held the page.
311
+ - **Files.** What it writes reaches the workspace when its job ends, and is
312
+ copied into the parent's sandbox at the same paths before the reply comes
313
+ back — so the parent's model, its `verify: file:` and the next step all see
314
+ it. The reply lists the paths. Up to 4 MiB and 100 files a job are copied
315
+ back; what is larger is listed as not in the parent's sandbox and reaches
316
+ the workspace for the next step only. A file the parent had already
317
+ changed in its own sandbox is not overwritten: the job's version is put
318
+ beside it as `<name>.from-<agent><ext>`, and the reply says so. If both
319
+ write one path, the parent, finishing later, wins — as between two steps of
320
+ a parallel group.
321
+ - **One level deep.** Its own `subagents:` are not offered to it.
322
+ - **Bounded by its parent.** A job runs within the parent step's `timeout:`
323
+ and under two budgets. The sub-agent's own `budget:`: a job holds what is
324
+ left of it while it runs, so a second job for the same agent at the same
325
+ moment, or one after the budget is spent, is not started and comes back as
326
+ failed. The flow's `budget:`: a job gets only what is neither spent nor held
327
+ by the steps and jobs running now — and a running step holds its whole
328
+ share, so in a flow with a `budget:` a job usually finds nothing left and is
329
+ not started. Give the sub-agent its own `budget:` and leave the flow's off,
330
+ or expect the job to be refused. The parent's step ends only when its jobs have. A stopped run
331
+ stops them.
332
+ - **Needs a run.** On the platform each job starts a sandbox of its own.
333
+ With `foldrun run` on your machine no step has a sandbox, so a job runs in
334
+ the same process: its own context, tools and secrets, but the same files
335
+ and network as the step that asked — the step's trace says so. Where
336
+ nothing can start a job from inside a step (an install without the egress
337
+ proxy), or nothing can stop one job's sandbox on its own (the local docker
338
+ executor), the tool is not offered and the step says so: a job whose parent
339
+ stopped waiting must not run on.
340
+
341
+ `foldrun agent link <parent> --subagent <reader> --isolated` writes
342
+ `sandbox: own` on the reader along with the link.
343
+
270
344
  ## Where it runs
271
345
 
272
346
  | field | |
package/docs/api.md CHANGED
@@ -136,6 +136,7 @@ installer's `error`, its cause on the first line. `foldrun deploy` polls it afte
136
136
  | `/api/workspaces/<ws>/flows/<flow>` | POST, PATCH, DELETE | edit a step (`step`, `instruction`, `options`, `target`, `subflow`), rearrange (`groups`), set `trigger`, a canvas edit (`edit`, `edits`), undo (`restore` + `expect`); delete. Every PATCH and DELETE takes `expect` — the text the step numbers were read from, or its sha256 — and is refused with `409` and the current `text`, nothing written, when the file no longer reads it (the canvas, the list view's board and step options, and the CLI's `flow add`/`rm-step`/`dup-step --to` send it; the dashboard shows the 409 as "this flow changed since you opened it — reload") |
137
137
  | `/api/workspaces/<ws>/flows/draft` | POST, PATCH, DELETE | POST draft a flow with a model: `{ description, flow? }` (`flow` redrafts that one). Returns `{ ok, files: [{ path, content, before, sha256 }], issues: [{ where, message, level }], repaired, notes }` — checked with the deploy's checks and the flow lint, repaired once. **Writes nothing**; save with the source PUT. Needs `workspace:write`; a description over 4,000 characters is `413`; a Chat-Completions `provider:` is `422`. On the platform's models the balance is checked first (`402`) and the call is charged as one ledger line, flow `ai-draft`. PATCH and DELETE, and a POST without `description`, are the edits of a flow that happens to be named `draft`, exactly as at `flows/<flow>` |
138
138
  | `/api/workspaces/<ws>/flows/<flow>/run` | POST | start it. An `Idempotency-Key` header, or `idempotencyKey` in the body, makes a retried call safe: a dropped response, a proxy timeout or a CI step that ran twice is answered `started: false` with the `runId` it already started, rather than starting a second run and billing for it. Keys are remembered for 24 hours, per flow |
139
+ | `/api/workspaces/<ws>/flows/<flow>/filter` | POST | test the flow's `filter:` against a delivery without starting anything: `{ payload }` — a webhook's JSON body (or the body as a string, read as JSON), an email's `{ from, to, subject, text }`, a chained run's `{ run, flow, status, ending, verdict, summary }`, a storage event's `{ by, paths }`. Answers `{ flow, trigger, filter, applies, match, reason }` — `reason` names the first condition that failed; a flow with no filter matches everything. Reads the deployed flow; nothing is recorded or charged |
139
140
 
140
141
  `POST .../run` takes `task` (the input), `from` (start at step N — a rerun of
141
142
  the interesting half), `tags`, and `test` (`true` for a [test run](runs#test-runs):
@@ -173,13 +174,19 @@ history with each message.
173
174
  | Route | Methods | |
174
175
  |---|---|---|
175
176
  | `/api/workspaces/<ws>/agents` | GET, POST | list — each agent's frontmatter as read, `owner` and `ownerFrom` (its own `owner:`, else the workspace's, else the account's), `lastRunAt` (when its latest step that ran started, from the runs kept; null for none) and `stale` (past `alerts: stale_days`); create (`name`) |
176
- | `/api/workspaces/<ws>/agents/<agent>` | PATCH | edit one line of an agent's frontmatter in place, as the flow canvas does: `{ list: { key, name, action } }` — `key` one of `subagents`, `agents`, `tools`; `action` `add` or `remove` — or `{ set: { key: "description", value } }`, or `{ network }` — the sites its sandbox may reach: `"open"`, `"trusted"`, `"none"`, a list of sites, or `{ level, allow, defaults }`; `null` removes the line so it inherits ([Network](network)). A `network` nobody could read is a 400 that says why. Only that key's lines change — comments, key order and prose stay. Answers `{ ok, changed }`; 404 for an unknown agent. `workspace:write` |
177
+ | `/api/workspaces/<ws>/agents/<agent>` | PATCH | edit one line of an agent's frontmatter in place, as the flow canvas does: `{ list: { key, name, action } }` — `key` one of `subagents`, `agents`, `tools`; `action` `add` or `remove` — or `{ set: { key: "description" | "sandbox", value } }` (`sandbox` takes `own` or `parent`, else 400), or `{ network }` — the sites its sandbox may reach: `"open"`, `"trusted"`, `"none"`, a list of sites, or `{ level, allow, defaults }`; `null` removes the line so it inherits ([Network](network)). A `network` nobody could read is a 400 that says why. Only that key's lines change — comments, key order and prose stay. Answers `{ ok, changed }`; 404 for an unknown agent. `workspace:write` |
177
178
  | `/api/workspaces/<ws>/agents/<agent>/run` | POST | run one agent alone (`task`; `test: true` for a test run) |
178
179
  | `/api/workspaces/<ws>/agents/import` | GET, POST | copy an agent in from another workspace of the account. GET lists what could be imported (every other workspace you can read, with its agents). POST `{ from, agent, as? }` copies `agents/<agent>/` — its agent.md and own skills/, scripts/, knowledge/; never memory/, outputs/, state/ — and answers `{ name, files, warnings }`; `warnings` names tools, skills, scripts and secrets the copy uses that this workspace lacks. 409 on a name clash (pass `as`), 404 for an unknown workspace or agent, 403 when `from` is outside your key's scope. Idempotent |
180
+ | `/api/workspaces/<ws>/proposals` | GET | the workspace's proposed fixes, newest first: `{ proposals: [{ id, workspace, eval, by (auto, or who asked), createdAt, updatedAt, status, failures: [{ runId?, case?, check, detail, at }], jobId, reply (the assistant's reasoning), files: [{ path, content, before, sha256 }], issues: [{ where, message, level }], error?, notes?, decidedBy?, decidedAt?, applied? }] }`. `status` is `drafting`, `ready`, `failed`, `applying`, `applied`, `partial` (a write failed part-way; `applied` names what was written) or `dismissed`. `?status=`, `?eval=`. A drafting one is settled as it is read |
181
+ | `/api/workspaces/<ws>/proposals/<id>` | GET, POST | one proposal. `POST { action: "apply" }` writes its files, each a revision by the caller, only when every one still reads what it was drawn against (`409` naming the file that changed) and can be written (`422`; a check error the deploy would refuse is `422` too); `POST { action: "dismiss" }` sets it aside — its failures stay looked at. Only agents, flows and skills are ever in one. Needs `workspace:write` |
179
182
  | `/api/workspaces/<ws>/export` | GET | this workspace, one flow or one agent as a `.zip` — `?kind=flow&name=<flow>` (the flow, every flow it runs as a step, every agent those steps and their colleagues name, and the workspace tools, skills and scripts they grant), `?kind=agent&name=<agent>` (its folder and what it grants), or neither for the whole authored tree. Never memory/, state/, outputs/, storage, runs or a secret's value; `foldrun-package.json` inside says what it is. Any role that can read the workspace |
180
183
  | `/api/workspaces/<ws>/import` | POST | take a package in; the body is the `.zip` (`content-type: application/zip`, at most 20 MB). `?dryRun=1` answers `{ kind, name, from, creates, added, overwritten, unchanged, needs: { secrets, tools, skills, scripts, agents, flows } }` plus `skipped` (entries it will not write: memory, state, anything outside the authored folders) and writes nothing. Without it, applies as one revision; 409 when it would replace a file with different text unless `?overwrite=1`, or when a file cannot be written there (all or nothing). Never deletes. A `Content-Length` is required (411 without). A workspace package into a name that does not exist makes that workspace (`workspace:manage`, the plan's workspace count). 400 for anything not a package, or one holding memory/, state/ or outputs/. Idempotent |
181
184
  | `/api/workspaces/<ws>/evals` | GET, POST | list; create (`name`) |
182
185
  | `/api/workspaces/<ws>/evals/<eval>/run` | GET, POST | POST runs one eval and answers with its result; with `{ async: true }` it answers `202 { started, eval, runId, startedAt }` at once and the eval runs on — an eval of slow cases outlives a proxy's read timeout (Cloudflare's is 125 s by default). One run of an eval at a time: a second is `409` naming the run that holds it. `GET ?run=<runId>` is what to poll — that run alone, whatever else runs or writes the eval's latest result meanwhile: `{ runId, state, running }` where `state` is `running`, `done` (with its `result`), `failed` (with `error`), `lost` (it stopped reporting before saving a result, e.g. a restart) or `unavailable` (an id not on record — the last 20 runs are kept). Plain `GET` answers who holds the eval now (`running`, `runId`, `startedAt`) and the latest `result`. The dashboard and `foldrun eval --to` start async and poll by run id |
186
+ | `/api/workspaces/<ws>/evals/<eval>/compare` | GET, POST | compare models on one eval: its cases under two to four settings, side by side. `POST { configs: [{ model, effort?, agent? }], repeat?, maxUsd? }` — `model` a tier (`fast`, `default`, `max`) or a model id; `effort` low…max; `agent` (flow evals) forces only that agent's steps, else every step; `repeat` 1–5 (1); `maxUsd` the spend limit, above 0 and at most 100 (5). At most 60 runs (cases × settings × repeat, else `413`). Answers `202 { started, eval, runId, startedAt, trials, maxUsd }` and runs on: every run a test run, the eval's own judge for all. `GET ?run=<runId>` → `{ runId, state, running }` — `running` (with `progress: { done, total }`), `done` (with `result`), `failed` (with `error`), `lost` or `unavailable`. `result` is `{ eval, target, judge, startedAt, finishedAt, repeat, maxUsd, spentUsd, stopped, configs: [{ model, effort, agent, label, trials, passed, notRun, rate, costUsd (runs and judges), judgeCostUsd, avgCostUsd, avgSecs (worker pick-up to end), runIds }], cases: [{ case, passed: [per setting], trials: [per setting] }] }`. Plain `GET` → `{ running, runId?, startedAt?, progress?, comparisons }` (the last 20, newest first). One comparison of an eval at a time (`409`); a grader, saved inputs or an eval with a `run:` check `400`. Needs `run:start` to start |
187
+ | `/api/workspaces/<ws>/evals/<eval>/improve` | POST | ask for a proposed fix now: for a grader, from its failed grades no proposal has covered; for an eval that runs cases, from its last result's failed cases. Answers `202` with the proposal, `status: "drafting"` — the assistant drafts it (one reply, paid as Ask AI's are); poll `GET …/proposals/<id>`. `409` when there is nothing to fix, a proposal for this eval is open, or the account or workspace is paused. Needs `run:start` |
188
+ | `/api/workspaces/<ws>/evals/<eval>/grades` | GET | a grader's grades (an eval with `trigger: runs`): `{ eval, flow, agent, sample, improve, days, summary, grades }`. `summary` is the window's `{ graded, passed, failed, errors, rate, costUsd, byDay: [{ day, graded, passed, failed }] }` (`rate` is passed ÷ decided, `null` when nothing was decided); `grades` the newest first, one per run — `{ eval, runId, flow, at, status (pass, fail or error — not decided), checks: [{ name, passed, error?, assertions: [{ type, value, passed, detail }] }], costUsd (the judges'), model, evalRevision, runRevision, runFinishedAt, by }`. `?days=` 1–90 (30), `?status=` pass, fail or error, `?limit=` 1–500 (100). An eval that runs cases is `400` |
189
+ | `/api/workspaces/<ws>/evals/<eval>/grade` | POST | grade one finished run now, sampled or not: `{ run }` → the grade (the shape above), kept like any other — the newest grade of a run is the one shown. Needs `run:start`; the judges run on the account's model key. A run that is not this grader's (another flow, or no step by its agent) is `400`, a run still going `409`, one with no reply to grade `409`, an eval that runs cases `400`, a paused account or workspace `409` |
183
190
  | `/api/workspaces/<ws>/tools/<tool>/test` | POST | exercise one tool (`args`, `path`). An http tool with an `operations:` allowlist is probed only with a GET to one of its operations (no `path`: the first GET without parameters), and fails if the allowlist resolves nothing, as a run does |
184
191
  | `/api/workspaces/<ws>/hooks` | GET | the [hooks](hooks) this workspace can turn on — its own and the library's (shadowing marked), invalid ones with the reason, and who turns each on — plus `recent`: every deny, rewrite and failure hooks wrote in the last `?runs=` runs (1–500, default 50), newest first; `?all=1` includes notes and context |
185
192
  | `/api/workspaces/<ws>/hooks/<name>/test` | POST | run one [hook](hooks) on a sample event: body `{ event }`, answer the hook's own (`ok`, `decision`, `reason`, `input`, `context`, `reply`, `note`, `failure`, `ms`). Shares the `hooks/` path with flow webhooks below — `/test` is only ever a hook's. Answers `404` for no such hook, `422` for an invalid `hook.md`, `400` for an event that is not an object. On an isolated install hook code never runs in the platform's own process, so this answers with where to test it instead (`foldrun hook test` locally, or a test run) |
@@ -192,7 +199,7 @@ paying for the steps around it.
192
199
  | Route | Methods | |
193
200
  |---|---|---|
194
201
  | `/api/workspaces/<ws>/runs?limit=` | GET | `{ runs: [{ id, flow, status, verdict, outcome, startedAt, finishedAt, ... }] }`; search (see [Runs](runs#searching-runs)) with `tool` (a step called a tool whose name contains it), `tool_error=1`, `agent`, `model` (contains), `min_tokens` (`50k`), `min_secs` — `limit` counts the matches — `verdict` is the word the summary leads with (`GOOD`, `QUIET`, `BLOCKED`, `BAD` or null) and `outcome` the one word every page shows (`running`, `queued`, `waiting`, `good`, `done`, `quiet`, `blocked`, `bad`, `failed`, `stopped`); `failure` is `{ agent, reason }` for a run that failed (null otherwise), `chargedUsd` is what the account was charged for the run (one credit is one cent), null until the ledger settles it, while `costUsd` is the provider's token cost — off the run index (see [Runs](runs#the-runs-list)); a finished run's steps carry `agent`, `status`, `costUsd` and `tokens` (`{ input, output, cacheRead, cacheWrite }`, see [Runs](runs#the-record)), and the run its summed `tokens` — not their reply or trace |
195
- | `/api/workspaces/<ws>/runs/<id>/calls/<call>` | GET | one tool call's recorded content (see [Runs](runs#what-a-run-records)): `{ call: { id, tool, input, output, inputChars, outputChars, truncated, inputCut, outputCut } }` — what it was given and what it gave back, secrets masked, each at most 65,536 characters; `inputChars`/`outputChars` are the whole length, `inputCut`/`outputCut` say which part was cut (`truncated`: either). `<call>` is the tool event's `call`; an event with `captured: true` has content here. 404 when nothing was recorded: the workspace says `capture: metadata`, or the run is from before 19 Oct 2026. A run's steps also carry `calls`, each model call (`n`, `at`, `ms`, `model`, `stop`, `tokens`, `tools`, and `subagent` / `attempt` when a sub-agent or a later attempt made it), and the run its `revision` (the workspace's latest revision when a worker picked it up — a deploy's commit, or the id an edit through `/source` was saved as) and `drivenAt` (when that was) |
202
+ | `/api/workspaces/<ws>/runs/<id>/calls/<call>` | GET | one tool call's recorded content (see [Runs](runs#what-a-run-records)): `{ call: { id, tool, input, output, inputChars, outputChars, truncated, inputCut, outputCut } }` — what it was given and what it gave back, secrets masked, each at most 65,536 characters; `inputChars`/`outputChars` are the whole length, `inputCut`/`outputCut` say which part was cut (`truncated`: either). `<call>` is the tool event's `call`; an event with `captured: true` has content here. 404 when nothing was recorded: the workspace says `capture: metadata`, or the run is from before 19 Oct 2026. A run's steps also carry `calls`, each model call (`n`, `at`, `ms`, `model`, `stop`, `tokens`, `tools`, and `subagent` / `attempt` when a sub-agent or a later attempt made it); a step that handed jobs to `sandbox: own` sub-agents carries `delegations` (`id`, `agent`, `status`, `costUsd`, `ms`), and each job is a step of its own with `subagentOf` (the delegating agent), `delegation` (that `id`) and `wrote` (the workspace paths it changed — copied into the delegating step's sandbox too, up to 4 MiB a job); and the run its `revision` (the workspace's latest revision when a worker picked it up — a deploy's commit, or the id an edit through `/source` was saved as) and `drivenAt` (when that was) |
196
203
  | `/api/workspaces/<ws>/runs/<id>/trace` | GET | the run as one tree (see [Runs](runs#the-trace-tree)): `{ tree, failure }`. Each span is `{ id, kind, label, start, end, status, children, … }`, where `kind` is `run`, `step`, `model` (one model call: `model`, `stop`, `tokens`), `tool` (`call`, `captured` when its input and output were recorded) or `error` (`text`); `start`/`end` are epoch milliseconds and `status` is `ok`, `error`, `running`, `waiting`, `skipped` or `pending`. A tool call sits under the model call that asked for it. `failure` is the ids from the root to the first thing that went wrong, which the run page opens on; `[]` when nothing did |
197
204
  | `/api/workspaces/<ws>/runs/<id>/compare?with=` | GET | this run beside another of the same flow (see [Runs](runs#compare-with-the-last-good-run)): `{ comparison: { base, run, revisionChanged, changedFiles, steps }, chosen }`. Each step is `{ step, agent, base, run, changes }`: both sides' status, instruction, model, seconds, tokens, model calls, tool counts, tool errors, first errors and first line said, and `changes` in words (`status: completed → failed`, `new error: …`, `instruction changed`, `tools: +web ×3`, `took 12s → 48s`, …). `changedFiles` lists the files changed between the revisions the two ran (null when one is unknown). `with` names the other run; unasked it is the newest earlier run of the flow that finished well, else the run this one re-ran (`chosen` says which) |
198
205
  | `/api/workspaces/<ws>/runs/<id>` | GET, DELETE | the whole run: every step, its output, cost, timing, the runner `image` it ran on and — for a step that browsed through the account's pod from slim — its `browserPod` reconnects and fallback ([runs](runs#the-record)). `?wait=true&timeout=<s>` instead blocks until the run stops (or `timeout`, default 300, max 900) and answers the verdict the run routes give — `200` completed, `500` failed with the record, `202` still going (`timedOut`) or parked on a person (`awaitingApproval`). Ask in short pieces: whatever sits in front of the platform cuts a request that stays silent too long (Cloudflare at 100 s, an AWS ALB at 60 s), and a flow can run for many minutes — the CLI's `--wait` asks 25 s at a time and comes back here on every `202` |
@@ -353,7 +360,7 @@ Reads return which secrets exist, never their values.
353
360
  | `/api/openapi.json` | GET | this API as an OpenAPI 3.1 document — every route and method, bearer auth, the rate-limit, version and Idempotency-Key headers, and request/response schemas for runs, flow runs, deploy, source, secrets, keys and billing. No key needed. Generated from the route files by `scripts/gen-openapi.mjs`; the dashboard renders it at Docs → API reference, `foldrun api spec` saves it |
354
361
  | `/api/account` | GET, PATCH | the account's defaults from its AGENTS.md frontmatter — `timezone`, `budget` (a USD cap — a number for a month, `"60/day"`, `"60/week"`, or `{ usd, period }`; null is no limit), `notify` (`{ url?, email?, events }`), `concurrency`, `deploy_block` (`none`, `secrets` or `security` — answered as `deployBlock`), `network_enforce` (answered as `networkEnforce`), `owner`, `alerts` (as written), `judge` (`{ model?, effort? }`), `telemetry` (`{ otlp, headers }` — header values are `${SECRET}` names; answered as `{ endpoint, headers }`); PATCH sets the keys sent, null clears one, and edits the file in place |
355
362
  | `/api/audit?since&until&action&actor&workspace&limit&cursor&format` | GET | the account's audit log, newest first — the caller's account only, whatever a header, query or body names. The owner and unscoped admins (people, or `admin` keys); `audit:read`, and refused to a member or key scoped to some workspaces, because the log is the whole account's. `since`/`until`: `7d`, `24h`, `30m` or a date; `action`: exact (`apikey.created`) or a family (`apikey`); `actor`: exact (an email, `key:<label>`, `support:<email>`); `workspace`: entries about one; `limit` 1–1000 (100; 1000 for CSV); `cursor`: the `next` of the page before. JSON `{ entries: [{ id, at, actor, action, subject, detail }], next, retention }`; `format=csv` downloads the same rows (`x-next-cursor` header when there are more). Records sign-ins and refusals, sign-outs, session revokes, password and email changes, MFA on/off, key mint/rotate/revoke, invites, joins, role/scope changes, removals, ownership transfers, secrets set/deleted (by name — never a value), deploys, workspace deletes, plan and billing changes, export and close requests, and `support.viewed`/`support.ended`. The super admin's own levers (`operator.*`) are the platform's record and not listed. Kept for the life of the account |
356
- | `/api/avatars/<kind>/<id>` | GET, PUT, DELETE | a picture for a `user` (their own id), a `workspace` (its name) or the `account` (the tenant). PUT the bytes of a PNG, JPEG or WebP under 1 MB; a person sets their own, an admin a workspace's or the account's. GET is cached for a year — the URL's `?v=` changes with the picture |
363
+ | `/api/avatars/<kind>/<id>` | GET, PUT, DELETE | a picture for a `user` (their own id), a `workspace` (its name) or the `account` (the tenant). PUT the bytes of a PNG, JPEG or WebP under 1 MB; a person sets their own, an admin a workspace's or the account's. GET needs a signed-in tenant and is privately cached for a year — the URL's `?v=` changes with the picture |
357
364
  | `/api/search?q=` | GET | find anything in the account by name — workspaces, agents, flows, tools, skills, knowledge, memory, runs (by id, flow or headline); ranked, at most six per kind. The dashboard's Find palette reads it |
358
365
  | `/api/schedule` | GET, POST | GET: every scheduled flow with `valid` (the cron parses) and `upcoming` (the next three fires). POST: tick the scheduler now — install-wide, so `workspace:manage`; returns `{ ok, fired }` |
359
366
  | `/api/library/<kind>?path=` | GET, POST, PUT, DELETE | account-level shared documents (`name`, `content`, `path`, `template`) |
@@ -454,6 +461,7 @@ signed-in person, never by a key — mints an ordinary API key at their role.
454
461
  | `/api/cli/authorize` | GET, POST | `?code=` describes the request (machine, expiry). POST `{ code, decision }` from a session approves (mints the key, labelled `cli · <hostname>`) or denies — `decision` is `approve` or `deny`, and absent approves; any other word is a `400`. Every role may sign its own CLI in; an owner's key is an admin's |
455
462
  | `/api/me` | GET | who is calling: `account`, `owner`, `role`, `workspaces` (null: all), and the `actor` — a person's email, or a key's id, label, prefix and who minted it |
456
463
  | `/api/me/onboarding` | GET, PATCH | the account's getting-started steps, each derived from what is really there — `{ steps: [{ id, title, why, href, cli?, done }], done, total, complete, next, dismissed, dismissedAt }`. Steps: `model` (first; only for an account that must bring its own key — done once it has one, or a `provider:` block in the account's AGENTS.md), `workspace`, `agent` (an agent written or deployed after the workspace was made), `run`, `success` (a completed run), `secret`, `trigger` (a schedule or webhook), `teammate` (a second member), `billing` (hosted, not fully waived: a saved card or a plan), `cli` (any API key or CLI login). PATCH `{ dismissed: true or false }` hides or brings back the dashboard card for the signed-in person (kept in their prefs); a key reads `dismissed: false` and cannot PATCH |
464
+ | `/api/me/inbox` | GET, POST | your inbox — what waits on you across the workspaces you may see. GET `{ person, items: [{ id, kind (approval, question, issue, proposal, run), bucket (mine, started, team), why, workspace, title, detail, at, href, read, dismissed }], counts: { mine, started, team, unread } }`, newest first. `mine` is assigned to you (named in `approvers:`, owner of the flow or agent, or you asked for the proposal); `started` is runs you started — failed or BLOCKED/BAD this week, a gate on one (someone else decides it), a question it asked; `team` is the rest that waits in your workspaces. By default `mine` and `started`; `?bucket=mine\|started\|team\|all`, `?kind=`, `?workspace=`, `?dismissed=1` for what you set aside. `counts.unread` is unread `mine` + `started`. POST `{ ids \| id, action: read\|unread\|dismiss\|restore }` changes your view only — dismissing never decides a gate or resolves an issue; ids name an occurrence, so a thing that happens again is new. A key acts for the person who minted it, inside the key's own scope; one no person minted is a `400` |
457
465
  | `/api/me/notifications` | GET, PATCH | what mail you get. GET `{ email, categories: [{ category, label, description, required, why?, perWorkspace, enabled, workspaces: { <ws>: bool } }] }` for `security`, `invites`, `billing`, `account` (required — always sent) and `run-alerts`, `approvals` (per workspace), `low-balance`, `product-updates`. PATCH `{ category, enabled, workspace? }` or `{ preferences: [ … ] }`; answers the same as GET. Turning a required category off is a `409` with the reason; `workspace` on an account-wide category is a `400`; `enabled: true` account-wide clears that category's per-workspace offs. A key acts for the person who minted it |
458
466
  | `/api/me/preferences` | GET, PATCH | your own display preferences: `{ theme, runViews }` — `theme` is `system` (the default: follow the computer's light or dark setting), `light` or `dark`; `runViews` your saved run searches, `[{ name, query }]` (see [Runs](runs#searching-runs)). PATCH `{ theme }` and/or `{ runViews }` (the whole list, at most 20, each query of the runs filters) answers the same; anything else is a `400`. Kept in your prefs, so it follows you to every browser; the dashboard applies it on the server, before the first paint. A key acts for the person who minted it; a key minted by nobody is a `400` |
459
467
  | `/api/unsubscribe?token=` | POST | one-click unsubscribe (RFC 8058) — no login, the signed token is the credential. It is the `List-Unsubscribe` URL on every optional mail, beside `List-Unsubscribe-Post: List-Unsubscribe=One-Click`; a mail client POSTs `List-Unsubscribe=One-Click` to it. Turns off the one category (for the one workspace) the token was minted for, for its address. The token may also come as a form field (`token`, and `from=page` to be sent back 303 to the `/unsubscribe` confirmation page) or JSON. `200 { ok, category, workspace }`; `400` no token, `401` forged, `410` expired (180 days; `FOLDRUN_UNSUBSCRIBE_TTL_DAYS`). Never on a GET — `/unsubscribe?token=` is the page that asks first |
@@ -462,7 +470,7 @@ signed-in person, never by a key — mints an ordinary API key at their role.
462
470
 
463
471
  | Route | Methods | |
464
472
  |---|---|---|
465
- | `/api/oauth/clients` | GET, POST, DELETE | registered providers (`name`, `client`, `config`) |
473
+ | `/api/oauth/clients` | GET, POST, DELETE | saved OAuth apps (`name`, `client`, `config`). `config`: `authorize_url`, `token_url`, `client_id`, `client_secret` (empty for a public app), `scopes`, optional `authorize_extra` (`{ name: value }` added to the authorize URL — the flow's own parameters are refused), `pkce` (false to turn it off), `issuer` |
466
474
  | `/api/oauth/start` | POST | begin a connection (`client`, `secret`, `workspace`, optional `scopes`). From a signed-in browser: `{ url }` is the provider's consent, bound to that browser by a cookie the callback checks, with PKCE. With an API key: `{ url, handoff: true }` is a dashboard page where a signed-in person accepts first (`foldrun connect --client` opens it). `scopes` (space-separated, as the provider spells them) is what this connection asks for instead of the client's default, and the connection keeps it for its reconnects — one saved app serves any number of connections at different scopes |
467
475
  | `/api/oauth/callback` | GET | where the provider returns — refuses a consent that comes back to another browser, and an `iss` that is not the app's issuer |
468
476
  | `/api/identity?workspace=` | GET | keyless cloud identity: `{ enabled, issuer, subject, tokenSeconds }` — the issuer a cloud must trust and the subject pattern each agent's tokens carry (`account:<a>:workspace:<w>:agent:<agent>`). Open (no key), outside `/api`: `/.well-known/openid-configuration` and `/.well-known/jwks.json`, 404 when the install does not issue tokens |
package/docs/checks.md CHANGED
@@ -261,6 +261,16 @@ is the other answer, though this rule looks at the agent, not the flow.
261
261
  *info · ASI03.* A sub-agent declares tools its parent does not have. A
262
262
  sub-agent never gets more than its parent, so they are dropped inside the
263
263
  delegation. Grant them to the parent, or remove them from the sub-agent.
264
+ A sub-agent with `sandbox: own` is not this rule's: its tools are its own
265
+ ([below](#securitysubagent-own-sandbox)).
266
+
267
+ ### security/subagent-own-sandbox
268
+
269
+ *info · ASI03.* A sub-agent with `sandbox: own` holds tools its parent does
270
+ not. That is what `sandbox: own` is for: each job it is handed runs in a
271
+ sandbox of its own, with its own tools, secrets and network, and only its
272
+ reply reaches the parent. The finding says what it holds, so the choice
273
+ stays visible ([agents](agents#isolated-sub-agents-sandbox-own)).
264
274
 
265
275
  ### flow/outward-ungated
266
276
 
package/docs/cli.md CHANGED
@@ -44,7 +44,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
44
44
  | `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 |
45
45
  | `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. `--online` adds the network checks the platform runs: known vulnerabilities for exact runtime pins and remote MCP servers' tool text, and what could not be checked |
46
46
  | `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) |
47
- | `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 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. `--isolated` also writes `sandbox: own` on the worker: each job it is handed runs in its own sandbox, with its own tools, secrets and network, not cut to the delegating agent's; its reply comes back with the files it wrote (`foldrun docs agents`). 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 |
48
48
  | `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) |
49
49
  | `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` |
50
50
  | `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 |
@@ -58,6 +58,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
58
58
  | `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 |
59
59
  | `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` |
60
60
  | `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 }` |
61
+ | `foldrun flow filter <flow> --payload <file\|-\|json>` | would this delivery start the flow? Its `filter:` against a payload — a webhook's JSON body, an email's `{ from, to, subject, text }`, a chained run's `{ run, flow, status, ending, verdict, summary }`, a storage event's `{ by, paths }` — and the condition that held it back. Reads the local flow; `--to <workspace>` asks the deployed one (`POST …/flows/<flow>/filter`, the Triggers page's Test box). Starts nothing; exits `0` when it would start a run, `1` when not; `--json` for the whole answer |
61
62
  | `foldrun flow move-step <flow> --step <n\|agent>` | dragging a card on the board: `--group <g>` runs it beside group g, `--after <g>` gives it a group of its own after g (`0` = first). Only step numbers change (core's `reorderFlowSteps`). Diff, checked, `--dry-run`; `--to` sends `PATCH { groups }` |
62
63
  | `foldrun flow copy-step <flow> --step <n\|agent>[,<n>…]` | the canvas's Copy: the steps' markdown — line, marker and every option (core's `stepSource`) — on stdout. Reads only |
63
64
  | `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" } }` |
@@ -70,7 +71,13 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
70
71
  | `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 }`) |
71
72
  | `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 |
72
73
  | `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 |
73
- | `foldrun eval [name]` | one eval, or all of them. `--to <workspace>` runs the DEPLOYED workspace's evals on the platform (`POST …/evals/<name>/run` with `async: true`, then polling `GET …/run?run=<id>` — an eval longer than a proxy's timeout still gets its own verdict), waiting for each (`--timeout <s>`, default 900; past it, the run id to read it with). `--run <id>` prints that run's verdict and runs nothing — a run that was lost or is unknown exits 1; `--last` prints each eval's latest result and runs nothing, and prints the same lines; exits 1 when a case fails |
74
+ | `foldrun eval [name]` | one eval, or all of them — a grader (`trigger: runs`) is listed and skipped, since it has no cases. `--to <workspace>` runs the DEPLOYED workspace's evals on the platform (`POST …/evals/<name>/run` with `async: true`, then polling `GET …/run?run=<id>` — an eval longer than a proxy's timeout still gets its own verdict), waiting for each (`--timeout <s>`, default 900; past it, the run id to read it with). `--run <id>` prints that run's verdict and runs nothing — a run that was lost or is unknown exits 1; `--last` prints each eval's latest result and runs nothing, and prints the same lines; exits 1 when a case fails |
75
+ | `foldrun eval compare <eval> --models <m[:effort],…> --to <workspace>` | an eval's cases under two to four models, side by side (`POST …/evals/<eval>/compare`, then polled) — the Evals page's **Compare models**. `--models fast,default,max` or `fast:low,max:high`; `--effort` for the items that name none; `--agent <name>` compares only that agent's steps of a flow eval; `--repeat` 1–5; `--max-usd` the spend limit (default 5). Every run is a test run. Waits (`--timeout <s>`, default 3600; past it, the id to read it with) and prints each setting's passing, cost, cost and time per run, then the cases where they differ. `--list` the kept comparisons; `--run <id>` one of them; `--json` |
76
+ | `foldrun eval improve <eval> --to <workspace>` | ask for a proposed fix now (`POST …/evals/<eval>/improve`) — from a grader's failed grades no proposal covers, or an eval's last failed cases — and wait for the draft (`--timeout`, default 300), then print it like `proposals show`. `--no-wait` returns with the id. Exits 1 unless the draft is ready |
77
+ | `foldrun proposals --to <workspace>` | the proposed fixes in a workspace, newest first: status, the eval, the failures it covers, the files it changes. `--status ready` for the ones waiting on you, `--eval <name>`, `--json`. The Evals page's Proposed fixes |
78
+ | `foldrun proposals show\|apply\|dismiss <id> --to <workspace>` | `show`: the assistant's reasoning, the failures, the change per file as `-`/`+` lines. `apply`: prints the change and asks before writing it (`--yes` without a terminal); each file is a revision by you; refused when a file changed since. `dismiss`: set it aside |
79
+ | `foldrun eval grades <eval> --to <workspace>` | what a grader (an eval with `trigger: runs`) found in the real runs it graded: the pass rate over `--days` (default 30), then each graded run, newest first, with the check that failed — `--status fail` for the failures alone, `--limit`, `--json`. `GET …/evals/<eval>/grades`, the Evals page's grader card. Exits 1 when a graded run in the window failed |
80
+ | `foldrun eval grade <eval> --run <id> --to <workspace>` | grade one finished run now, sampled or not (`POST …/evals/<eval>/grade`) — **Grade a run** on the card. Prints each check; exits 1 unless it passed |
74
81
  | `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>` |
75
82
  | `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. A run that went wrong (failed, BLOCKED, BAD) takes `--should "what it should have done"` — the judge's claim — or `--expect` |
76
83
  | `foldrun probe <model>` | can this model hold a tool loop here? A live check |
@@ -85,6 +92,8 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
85
92
  | `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 |
86
93
  | `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`) |
87
94
  | `foldrun insights` | recurring [problems](runs#insights) found in the last week of runs — a prompt mostly uncached, a big tool result sent again, a retry loop, steps near their timeout — each with why, what to do, about what it cost and the runs it was seen in. `--days <1-30>`, `--to <workspace>`, `--json` |
95
+ | `foldrun inbox` | what waits on you across your workspaces ([your inbox](runs#your-inbox)): gates, questions agents ask, open issues, proposed fixes, your runs that went wrong this week — assigned to you and started by you by default; `--team`, `--started`, `--mine`, `--all`, `--dismissed`, `--kind approval\|question\|issue\|proposal\|run`, `--unread`, `--json`. Each item shows its id; exits 1 when something assigned to you is unread |
96
+ | `foldrun inbox read\|unread\|dismiss\|restore <id>…` | mark items in your own view: `dismiss` sets one aside (it still waits where it lives), `restore` puts it back |
88
97
  | `foldrun issues` | the account's [issues](runs#issues): what went wrong, grouped, open until someone resolves it — kind, desk and flow (or the platform cause and every desk it hit), how many times since when, the newest reason and what to do. `--to <workspace>` narrows it, `--all` adds resolved ones (`--status open\|acknowledged\|snoozed\|resolved\|all`), `--json` |
89
98
  | `foldrun issues show\|ack\|snooze\|resolve\|reopen <id>` | one issue: `show` adds its runs and history; `ack` says you have seen it, `snooze --for 30m\|4h\|1d\|1w` (or `--until <ISO>`, or `--times <n>`: until it happens n more times) hides it for a while, `resolve` closes it until it happens again, `reopen` undoes that. `--note "…"` is kept in its history |
90
99
  | `foldrun issues doctor <id>` | the run doctor on an issue's newest run, in that run's workspace — the issue card's **Doctor** button: why (platform, check, work or outside), and the fix as diffs, saved on yes (`--yes`, `--dry-run`, `--json`). A missed schedule has no run and is refused |
@@ -97,8 +106,8 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
97
106
  | `foldrun secrets set NAME` | store a secret — also `ls`, `rm`, and `status` (OAuth grant health). `--site <domain>` (repeatable, or `a,b`) binds it as it is stored |
98
107
  | `foldrun secrets sites NAME [site…]` | bind a secret to the sites it is for: the egress proxy fills it in requests to those sites alone, and it never enters a sandbox ([bound to sites](secrets#bound-to-sites)). No sites prints what it is bound to; `--clear` unbinds. On the platform, or this machine's vault with `--local` |
99
108
  | `foldrun secrets set NAME --kind <k>` | the other shapes the Connections form stores: `file` (`--file <path>`), `ssh` (`--host`, `--user`, `--port`, `--key-file <path>` or a prompted password), `api` (`--base-url`, `--header "Name: value"` repeatable, or `--basic-user <u>` with the password prompted or `--basic-password`), `service-account` (`--file <key.json>`, `--scopes`, `--subject <user>` to act as a user where the provider allows it), `client-cert` (`--cert-file`, `--key-file`, `--passphrase`, `--export` to put it in the sandbox as files too; bind it with `secrets sites`), `identity` (`--audience`: keyless cloud access — nothing secret is stored), `m2m` (a client-credentials app: `--token-url`, `--client-id`, `--scopes`, the secret prompted). On the platform, or this machine's vault with `--local` |
100
- | `foldrun secrets clients` | the OAuth clients saved on the platform — what a consent runs from without typing the client again; the client secret is never shown. `clients add <name> --provider google\|github\|microsoft\|linkedin` (or `--authorize-url` / `--token-url`) `--client-id … --scopes …` saves one (secret prompted), `clients rm <name>` removes it |
101
- | `foldrun connect NAME` | OAuth sign-in from the terminal, stored as an auto-refreshing secret — `--provider google\|github\|microsoft\|linkedin`; PKCE is on (`--no-pkce` only for an app with a secret whose provider refuses it), and an app with no client secret is a public app; reconnecting a secret whose client is saved asks for nothing (`--new-client` to enter one) |
109
+ | `foldrun secrets clients` | the OAuth clients saved on the platform — what a consent runs from without typing the client again; the client secret is never shown. `clients add <name> --authorize-url … --token-url … --client-id … --scopes …` saves one (secret prompted), with `--authorize-param name=value` for anything else the provider's authorize URL takes, `clients rm <name>` removes it |
110
+ | `foldrun connect NAME` | OAuth sign-in from the terminal, stored as an auto-refreshing secret — `--authorize-url` and `--token-url` from the provider's OAuth docs, `--authorize-param name=value` for its extras; PKCE is on (`--no-pkce` only for an app with a secret whose provider refuses it), and an app with no client secret is a public app; reconnecting a secret whose client is saved asks for nothing (`--new-client` to enter one) |
102
111
  | `foldrun connect NAME --client <saved>` | the Connections page's **Connect**: a consent from a client saved on the platform (`--scopes "<a b>"` asks for this connection's own scopes instead of the client's default; the connection keeps them). It opens a page in the dashboard where you, signed in, accept the connection before the provider is asked, returning to the platform's own callback, which stores the grant as NAME (`--to <workspace>` for a workspace's). Opens the browser (`--no-browser` prints the address) and waits for it to land |
103
112
  | `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 |
104
113
  | `foldrun help <command>` | that command's usage and options. `foldrun <command> --help` and `-h` print the same and never run the command |
@@ -186,9 +195,14 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
186
195
  | `--flow <name>` | a bulk `stop` or `rerun`: only that flow's runs. `flow add … subflow`: the other flow to run as a step |
187
196
  | `--agent`, `--instruction`, `--after`/`--before`, `--group`, `--step`, `--cases`/`--else`, `--each`/`--max`, `--loop`/`--until`/`--judge`, `--question`, `--wait`, `--on-fail`, `--off` | `flow add`: see the patterns above. `--agent` on `rerun` is the first step that agent runs; `--question` on `answer` picks one of several questions; `--wait` alone stays the switch `invoke` and `agent run` read |
188
197
  | `--schedule "<cron>"`, `--timezone <zone>` | `flow trigger schedule`: the cron line (5 fields, or `@daily`) and the zone it is read in |
198
+ | `--team`, `--started`, `--mine`, `--all` | `inbox`: which bucket — the rest of what waits in your workspaces, runs you started, what is assigned to you, or every one (default: assigned and started) |
199
+ | `--dismissed`, `--unread` | `inbox`: what you set aside; only what you have not read |
200
+ | `--models <m[:effort],…>` | `eval compare`: the settings to compare — two to four tiers or model ids, each with an optional `:effort` |
201
+ | `--max-usd <n>` | `eval compare`: stop starting runs once the runs and judges have spent this (default 5) |
202
+ | `--payload <file\|-\|json>` | `flow filter`: the delivery to test — a file, `-` for stdin, or JSON inline |
189
203
  | `--kind <k>` | `secrets set`: `file`, `ssh`, `api`, `service-account` or `m2m` — each with its own flags, in the `secrets set NAME --kind` row above |
190
204
  | `--client <name>` | `connect`: run the consent from that OAuth client saved on the platform (`secrets clients` lists them) |
191
- | `--subagent`, `--consult`, `--can-ask`, `--description` | `agent link`, `agent unlink`: which list to edit, and a worker's description when it has none |
205
+ | `--subagent`, `--consult`, `--can-ask`, `--description`, `--isolated` | `agent link`, `agent unlink`: which list to edit, a worker's description when it has none, and (`link --subagent` only) `sandbox: own` on the worker |
192
206
  | `--eval <name>`, `--expect "<line>"`, `--case <name>` | `promote`: the eval file, an assertion (repeatable), the case's name |
193
207
  | `--tag <t>` | `invoke`, `flow run`: a label on the run — repeatable |
194
208
  | `--inputs <name>` | `invoke`, `flow run`: the task is that saved input set; not with `--task` |
@@ -217,7 +231,9 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
217
231
  | `--wait` | `invoke`, `agent run`: hold on and print the result. Asked in 25-second pieces, so a flow that runs for many minutes is never cut by whatever sits in front of the platform; a run parked on a person prints its page and exits 2 |
218
232
  | `--from <n>` | `invoke`, `flow run`, `rerun`: start at step n; earlier steps are recorded as skipped. A bulk `rerun` defaults to 1. (One flag, two meanings: a template on `init`, a step everywhere else) |
219
233
  | `--no-browser` | `login`, `connect`: print the address instead of opening it |
220
- | `--provider <name>` | `connect`: fills the authorize and token URLs and an example scope string; `--authorize-url` / `--token-url` for a provider with no preset |
234
+ | `--authorize-url` / `--token-url` | `connect`, `secrets clients add`: the provider's own OAuth addresses |
235
+ | `--authorize-param name=value` | `connect`, `secrets clients add`: an extra authorize-URL parameter the provider documents, repeatable — Google wants `access_type=offline` and `prompt=consent` before it issues a refresh token. The flow's own parameters (`client_id`, `redirect_uri`, `state`, `scope`, PKCE) cannot be set this way |
236
+ | `--provider <name>` | (deprecated — use `--authorize-url`, `--token-url` and `--authorize-param`) `connect`, `secrets clients add`: filled the URLs for google, github, microsoft or linkedin |
221
237
  | `--scopes "<a b c>"` | `connect`: space-separated, as OAuth defines it |
222
238
  | `--client-id` / `--client-secret` | `connect`: the OAuth app; prompted when omitted, the secret never echoed |
223
239
  | `--port <n>` | `connect`: the loopback port (default 8642) |
package/docs/evals.md CHANGED
@@ -35,7 +35,9 @@ satisfy. The heading is the case's name, so make it the thing being checked.
35
35
  | `agent` **or** `flow` | what runs. A flow eval runs the whole flow per case |
36
36
  | `model` | the **judge's** tier, not the subject's — cases run the agent on its own model. Unset, the workspace's `judge:` in `AGENTS.md` (see [The four jobs](four-jobs#judge--the-evaluation-default)), else the fast tier |
37
37
  | `effort` | the judge's effort. PASS/FAIL against one sentence is not deep work |
38
- | `trigger` | `deploy` (default) or `manual` |
38
+ | `trigger` | `deploy` (default), `manual`, or `runs` — grade real finished runs instead of running cases (see [Grading live runs](#grading-live-runs)) |
39
+ | `sample` | `trigger: runs` — the percent of finished runs graded, 0 to 100 (default 25) |
40
+ | `improve` | `trigger: runs` — `true` (5) or a count: once that many graded runs failed, a fix is proposed for a person to review (see [Proposed fixes](#proposed-fixes)) |
39
41
  | `inputs` | `true` makes the file a flow's saved inputs, not a test: its cases are named tasks for [Run with…](flows#run-with-saved-inputs) and `foldrun invoke --inputs`, and nothing runs them as an eval |
40
42
  | `live` | an eval's runs are [test runs](runs#test-runs) — nothing outward, `state/` untouched — unless this says `true`. A flow eval that must really send to prove itself opts in; nothing opts in by accident |
41
43
 
@@ -69,6 +71,154 @@ so it counts against the account's `concurrency:` and waits its turn.
69
71
  eval runs the entire flow once per case, so a three-case eval on an hour-long
70
72
  flow is three hours and the full cost, on every deploy.
71
73
 
74
+ ## Grading live runs
75
+
76
+ An eval tests the cases someone wrote. A grader tests the runs that actually
77
+ happened: the 3am desk run nobody watched, held to the same checks. It is an
78
+ eval with `trigger: runs` and no tasks — each `##` heading is a check, with
79
+ only an `expect:` list:
80
+
81
+ ```markdown
82
+ ---
83
+ name: digest-quality
84
+ flow: weekly-digest # or agent: writer — that agent's steps, in any flow
85
+ trigger: runs
86
+ sample: 25 # percent of finished runs graded
87
+ model: fast # the judge
88
+ ---
89
+
90
+ ## cites its sources
91
+ expect:
92
+ - judge: every claim names the source it came from
93
+
94
+ ## no placeholders
95
+ expect:
96
+ - not-contains: lorem ipsum
97
+ ```
98
+
99
+ - **What it grades.** For `flow:`, every step's reply joined; for `agent:`,
100
+ that agent's replies, in whichever flow it ran. The judge is also given
101
+ the run's task, so it grades the reply against what was asked.
102
+ - **Which runs.** Completed ones — a crash is already an
103
+ [issue](runs#issues). Not test runs, not eval runs, and only those that
104
+ finished after the grader was first deployed: a new grader starts from
105
+ now, it does not grade a week of runs at once. The sample is chosen by a
106
+ hash of the run id, so the same run is always in or out.
107
+ - **When.** The platform grades a few runs every five minutes, at most four
108
+ per account a pass, so a backlog drains steadily rather than spending at
109
+ once. A paused account or workspace is not graded.
110
+ - **The checks.** `contains:`, `not-contains:` and `judge:`, cheap ones
111
+ first; the judge only runs when they pass. `file:` and `run:` are refused
112
+ (`foldrun check` and the deploy say so): a finished run's sandbox is gone,
113
+ and they would act on the platform's host instead. `matches:` is refused
114
+ too — a regular expression over whatever a run said, on the platform's
115
+ scheduler, could stall it. A `task:` in a grader does nothing and is
116
+ refused as well.
117
+ - **A judge that does not answer** (a spent usage cap, a provider outage)
118
+ leaves the run *not decided*, which is not a failure. It is tried again
119
+ after an hour, three times at most.
120
+ - **What it costs.** One judge call per `judge:` line per graded run — at
121
+ most five lines in a grader — on the account's own model key like any
122
+ eval judge. A paused account or workspace is not graded, by the pass or
123
+ by **Grade a run**. The card shows what the
124
+ judges cost; the runs' own cost is on the runs.
125
+
126
+ The grades are on the Evals page: each grader's card has its pass rate over
127
+ 7 and 30 days, a bar per day, the recent failures with the check that failed
128
+ and the judge's reason, and **Grade a run** to grade any finished run now,
129
+ sampled or not (a run graded again shows its newer grade). A run's own page
130
+ lists every grade it got. From the CLI, `foldrun eval grades <eval> --to
131
+ <workspace>` (`--status fail`, `--days 7`) and `foldrun eval grade <eval>
132
+ --run <id> --to <workspace>`; the API is `GET …/evals/<eval>/grades` and
133
+ `POST …/evals/<eval>/grade`. Grades live in `evals/.results/grades/`, kept
134
+ 90 days; a deploy never touches them.
135
+
136
+ A grader never runs as cases: not on deploy, not from Run, not from
137
+ `foldrun eval`.
138
+
139
+ ## Proposed fixes
140
+
141
+ A failure that keeps happening should turn into a change someone can look
142
+ at, not sit on a card. A grader with `improve: true` (or `improve: 3` — how
143
+ many failures) opens a **proposal** on its own once that many graded runs
144
+ have failed and no proposal has covered them, or once the oldest such failure
145
+ is a week old:
146
+
147
+ ```yaml
148
+ trigger: runs
149
+ improve: true # 5 failed grades nobody has looked at → a proposal
150
+ ```
151
+
152
+ - **The assistant drafts it.** It is given a short digest of the failures —
153
+ at most ten, each the run, the check and the judge's reason — and reads
154
+ the agent, the flow, their skills and the runs themselves. It is asked for
155
+ the smallest change that fixes the cause, never to loosen a check, and to
156
+ propose nothing when the fault is not in the files (a missing secret, an
157
+ outage).
158
+ - **Only agents, flows and skills.** A proposal may change
159
+ `agents/<name>/agent.md`, `flows/<name>.md` and `skills/<name>/…`; anything
160
+ else the assistant proposes is left out, and the proposal says so.
161
+ - **Nothing is written until you apply it.** Apply checks that every file
162
+ still reads what the proposal was drawn against and can be written, then
163
+ writes them all, each a revision by you (so History can take it back). A
164
+ file someone changed since is refused — dismiss it and propose again. A
165
+ check error the deploy would refuse blocks Apply.
166
+ - **At most one open proposal per eval**, and the automatic ones at most one
167
+ a day per grader. A failure is *looked at* once a proposal covers it —
168
+ drafting, ready, applied or dismissed; a draft that failed releases its
169
+ failures so they count again. A draft that has not finished in half an
170
+ hour has failed.
171
+ - **What it costs**: one assistant reply, paid the way Ask AI is.
172
+
173
+ They are on the eval's card under **Proposed fixes**: what failed, the
174
+ assistant's reasoning, a diff per file, and Apply or Dismiss. **Propose a
175
+ fix** asks for one now — for a grader, from the failures no proposal has
176
+ covered; for an eval that runs cases, from its last result's failed cases.
177
+ Ready proposals are in your [inbox](runs#your-inbox). From the CLI: `foldrun
178
+ eval improve <eval> --to <workspace>` asks for one and waits for the draft;
179
+ `foldrun proposals --to <workspace>` (`--status ready`), `foldrun proposals
180
+ show|apply|dismiss <id> --to <workspace>`. The API: `POST
181
+ …/evals/<eval>/improve`, `GET …/proposals`, `GET`/`POST …/proposals/<id>`.
182
+
183
+ ## Comparing models
184
+
185
+ "Could this step run on the fast model?" is answered from the cases you
186
+ already have. **Compare models** on an eval's card runs every case under two
187
+ to four settings — a model (`fast`, `default`, `max`, or a model id), an
188
+ effort, and for a flow eval either every step or one agent's — and puts them
189
+ side by side: how many passed, what the runs and their judges cost, the cost
190
+ and time per run, and each case under each setting, so you see where they
191
+ differ.
192
+
193
+ - **The model is forced on the steps**, so it wins over the agent's own
194
+ `model:` and the flow's defaults. "every step" forces all of a flow's
195
+ steps; naming an agent forces only that agent's.
196
+ - **Every run is a test run**, whatever the eval or the flow says about
197
+ `live:` — a comparison is never the reason something was sent.
198
+ - **The judge is the eval's own** and the same for every setting: what
199
+ changes is the subject, never the grader.
200
+ - **`repeat`** (1–5) runs each case more than once; one run each tells you
201
+ little where the model is not deterministic. Repeats interleave — each
202
+ setting once, then again — so a provider that slows down mid-way slows
203
+ every setting alike.
204
+ - **Bounded twice**: at most 60 runs (cases × settings × repeat), and a spend
205
+ limit (default $5, at most $100) — once the runs and judges have spent it,
206
+ the rest are recorded as not run and the result says it stopped early.
207
+ - **Time per run** is from a worker picking the run up to its end; time in
208
+ the queue is left out, so a busy account does not read as a slow model.
209
+ - **What it writes**: the comparison's own result, kept with the last 20 —
210
+ never the eval's latest result or its history. One comparison of an eval
211
+ at a time.
212
+
213
+ It runs on the platform and can take as long as its runs do; leave the page
214
+ and come back. From the CLI: `foldrun eval compare <eval> --models
215
+ fast,default,max --to <workspace>` (`--models fast:low,max:high` for
216
+ efforts, `--agent <name>`, `--repeat 3`, `--max-usd 10`; `--list` and `--run
217
+ <id>` read the kept ones). The API is `POST …/evals/<eval>/compare`, then
218
+ `GET …/compare?run=<id>`. A grader (`trigger: runs`) has no cases, so it
219
+ cannot be compared, and neither can an eval with a `run:` check — a
220
+ comparison repeats every check many times, and `run:` executes a command.
221
+
72
222
  ## Promoting a run
73
223
 
74
224
  A run is a case you already have — the task is on record. A run that worked