foldrun 1.2.0 → 2.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/bin/foldrun.mjs +2 -4
- package/docs/api.md +5 -11
- package/docs/backups-and-restore.md +21 -12
- package/docs/budgets-and-billing.md +3 -2
- package/docs/checks.md +42 -5
- package/docs/cli.md +8 -13
- package/docs/deploying.md +32 -43
- package/docs/flows.md +6 -7
- package/docs/glossary.md +2 -2
- package/docs/introduction.md +1 -1
- package/docs/runs.md +5 -3
- package/docs/self-hosting.md +2 -3
- package/docs/team-and-access.md +5 -6
- package/docs/tools.md +7 -0
- package/docs/workspaces.md +8 -14
- package/package.json +3 -3
- package/src/commands.mjs +54 -97
package/bin/foldrun.mjs
CHANGED
|
@@ -185,8 +185,7 @@ const HELP = `foldrun — agents are just folders
|
|
|
185
185
|
foldrun flags the feature flags for this account: each on or off, and why — set for this account, for every account, a staged rollout, or the default (--json). Read-only: the platform's super admin changes them
|
|
186
186
|
foldrun history [path] every change to a deployed workspace, newest first (--id <revision> for its diff, --limit, --to)
|
|
187
187
|
foldrun history restore <path> --id <revision> put one file back as that revision left it, as a new revision — diff, asks (--yes, --dry-run, --to)
|
|
188
|
-
foldrun
|
|
189
|
-
foldrun restore <ws> --to <commit|time|age> put a workspace's source back as it was then — shows the change, asks for the name typed back (--dry-run: the diff only, --yes)
|
|
188
|
+
foldrun restore <ws> --to <revision|time|age> put a workspace's source back as it was then — shows the change, asks for the name typed back (--dry-run: the diff only, --yes)
|
|
190
189
|
foldrun backups how the account is backed up (encrypted?), when it last was, the snapshots it is in — also request --what runs|state|storage|everything --at "<when>" [--to <ws>] [--note]
|
|
191
190
|
foldrun backups requests every restore asked of the platform team, and where each stands (--json)
|
|
192
191
|
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
|
|
@@ -294,7 +293,7 @@ Options
|
|
|
294
293
|
--step <n> approve, reject: decide only that step; default is every step that is waiting
|
|
295
294
|
--payload <json|text|@f> approve: release a step waiting on wait: event with this body, recorded as you
|
|
296
295
|
--note "<text>" approve, reject: guidance the agent reads — or the reason for a refusal; billing credit: the ledger line's note
|
|
297
|
-
--yes approve, stop, rerun (bulk), flow rotate-hook, flow draft, assistant, history restore, restore <ws>, import, billing plan,
|
|
296
|
+
--yes approve, stop, rerun (bulk), flow rotate-hook, flow draft, assistant, history restore, restore <ws>, import, billing plan, workspace set name, and every rm/revoke/unshare: skip the confirmation, deliberately (required with no terminal)
|
|
298
297
|
--json report: the raw run record instead of the report; observe, usage, api version, changelog, find, preferences, workspace vocabulary, flags, workspaces, workspaces show, agent ls, flow ls, agent import --list, backups requests, team, hook ls: the raw document; version: {cli, core, platform}
|
|
299
298
|
--events <a,b> account set notify: failed, awaiting-approval, completed
|
|
300
299
|
--failed webhooks deliveries: only the ones that gave up
|
|
@@ -324,7 +323,6 @@ Options
|
|
|
324
323
|
--port <n> connect: loopback port for the redirect (default 8642 — register http://localhost:8642/callback on the app)
|
|
325
324
|
--authorize-url / --token-url connect: a provider with no preset
|
|
326
325
|
--role <r> keys create: viewer, editor (default) or admin
|
|
327
|
-
--for <workspace> keys create: a deploy key for one workspace (--access read|write)
|
|
328
326
|
--expires <span> keys create: when it stops working — 30d, 90d, 365d or never (default never)
|
|
329
327
|
--grace <span> keys rotate: keep the old key working for 1h or 24h while you swap the new one in (default 0: revoked now)
|
|
330
328
|
|
package/docs/api.md
CHANGED
|
@@ -14,7 +14,7 @@ in common — versions (`Foldrun-Version`), rate limits (`X-RateLimit-*`, `429`)
|
|
|
14
14
|
One funnel for every caller. A route either takes an authenticated tenant or
|
|
15
15
|
refuses — there is no separate "browser API". The exceptions carry their own
|
|
16
16
|
proof instead of a key: a token or signature (`/s/<token>`, `/api/hooks`,
|
|
17
|
-
`/api/inbox`, `/api/events`, `/api/approve`, `/api/unsubscribe`, `/api/
|
|
17
|
+
`/api/inbox`, `/api/events`, `/api/approve`, `/api/unsubscribe`, `/api/billing/stripe`,
|
|
18
18
|
`/api/billing/confirm`), the sign-in and recovery steps (`/api/auth/signup`,
|
|
19
19
|
`login`, `forgot`, `reset`, `verify`, `mfa/verify`, `session`, `/api/cli/login`,
|
|
20
20
|
`/api/cli/login/<id>`, `/api/cli/authorize` GET, `/api/oauth/callback`), `/api/healthz`, `/api/status`, `/api/status/history`, `/api/version` and `/api/openapi.json`.
|
|
@@ -142,9 +142,7 @@ nothing outward, state untouched, receipts on the run). Returns
|
|
|
142
142
|
`{ ok, runId, steps, test }` (`from` is added when a start step was given); `409 { error, runId }` when the flow has `overlap: skip` and a run is live; with `?wait=true&timeout=<s>` it holds
|
|
143
143
|
on and returns the verdict instead, exactly as `GET .../runs/<id>?wait=true`
|
|
144
144
|
does below, which is where a caller goes on to wait when a piece runs out.
|
|
145
|
-
Left out,
|
|
146
|
-
a preview workspace's runs are tests unless the flow says `live: true`;
|
|
147
|
-
everywhere else a run is live.
|
|
145
|
+
Left out, a run is live.
|
|
148
146
|
|
|
149
147
|
## Assistant
|
|
150
148
|
|
|
@@ -208,12 +206,9 @@ paying for the steps around it.
|
|
|
208
206
|
| `/api/workspaces/<ws>/notify/deliveries` | GET | the outbound webhook log: every `notify:` url delivery from this workspace, newest first — `{ deliveries, total, limit, offset, endpoint }`. A delivery is `{ id, event, status, destination, attempts, attemptLog, lastStatusCode, lastError, nextAttemptAt, createdAt, updatedAt, body }`: `id` is the `X-Foldrun-Delivery` header; `status` is `pending` (a retry is due at `nextAttemptAt`), `delivered`, `failed` (gave up after six attempts) or `skipped` (the endpoint was switched off); each attempt is `{ n, at, statusCode, durationMs, response (first 500 characters), error, manual? }`. `body` is what was sent, except that a gate's `approveUrl`/`rejectUrl` (top level and in `links`) read `[redacted]` — they decide without a login, and this log is readable by every role; the receiver still gets them, on a redelivery too. `?status=`, `?event=`, `?limit=` (50, at most 200), `?offset=`. `endpoint` is `{ failingSince, lastSuccessAt, lastFailureAt, disabledAt, disabledReason }` or null. Kept 30 days |
|
|
209
207
|
| `/api/workspaces/<ws>/notify/deliveries/<id>/redeliver` | POST | send one delivery again now — the same id and body, signed afresh, to the URL `notify:` names today. `{ ok, attempt, delivery }`, always `200` when the delivery exists (whether the receiver took it is the answer); `404` when it does not. An accepted redelivery switches a disabled endpoint back on. Needs the permission that runs |
|
|
210
208
|
| `/api/workspaces/<ws>/notify/test` | POST | sends one real notification to whatever this workspace's `notify:` resolves to, and answers `{ ok, destination, detail }` — including the provider's own words when it failed, which are what name the problem. Always `200`: the request was handled, and the result is the answer. Needs the permission that runs, because it sends real mail |
|
|
211
|
-
| `/api/workspaces/<ws>/observe?days=` | GET | the observability report the Observe page renders — per-agent failures and retries, per-tool calls/errors/latency, per-flow duration, day series, recent failures. `days` 1–365, default 30 (`since=`, its older name, still works — deprecated) |
|
|
209
|
+
| `/api/workspaces/<ws>/observe?days=` | GET | the observability report the Observe page renders — per-agent failures and retries, per-tool calls/errors/latency, per-flow duration, day series, recent failures, and `startup` — one row per start-up phase in order, `{ phase, took, at }`, each `{ n, p50, p95, max }` in seconds (`took` is the phase's own share, `at` when it ended counted from the step's start; percentiles need five samples). `days` 1–365, default 30 (`since=`, its older name, still works — deprecated) |
|
|
212
210
|
| `/api/workspaces/<ws>/history?path=&id=&limit=` | GET | every change to the workspace, newest first — `path` narrows to one file, `id` returns one revision with its before/after and a line diff, `limit` 1–500, default 100. A deploy's revision id is its commit |
|
|
213
|
-
| `/
|
|
214
|
-
| `/git/<tenant>/_library.git` | git | the account library as a git remote — a push replaces the shelf |
|
|
215
|
-
| `/api/workspaces/<ws>/repo?branch=` | GET, POST | GET: branches, tags, mirror settings; `?branch=` a branch's net change against main with diffs. POST `{action}`: `deploy` a `ref` (a rollback is a new commit on main), `merge` a `branch` into main and deploy, `delete-branch`, `mirror` (set `url`, https, credentials via the MIRROR_TOKEN secret), `mirror-now` |
|
|
216
|
-
| `/api/workspaces/<ws>/restore` | POST | put the workspace's source back as it was at a point in its history. `to`: a commit or its prefix, a branch or tag, an ISO date/time (the last commit on main at or before it) or an age (`90m`, `12h`, `3d`, `2w`). `{ to, dryRun: true }` answers `{ to: { sha, at, by, message }, from, added, updated, removed, files: [{ path, change, diff }], blockedBy, noop, issues, warnings }` and writes nothing; `issues` are the deploy checks that would refuse that tree, `warnings` the ones it would deploy with. The restore needs `confirm` set to the workspace's name (400 without) and takes the dry run's `expectRemoved`; it deploys that commit's tree as a new commit on main (undo: restore to `from`), keeps runs, `state/`, storage, secrets and agent-written memory, and is refused 409 while runs are in flight or when it would remove something not in `expectRemoved`. `workspace:write`; audited as `workspace.restored` |
|
|
211
|
+
| `/api/workspaces/<ws>/restore` | POST | put the workspace's source back as it was at a point in its history. `to`: a revision id (`r-…`), the commit a deploy named (or its prefix), an ISO date/time (the last revision at or before it) or an age (`90m`, `12h`, `3d`, `2w`) — every revision since 17 Oct 2026 holds a snapshot of the whole source. `{ to, dryRun: true }` answers `{ to: { id, sha, at, by, message }, from, added, updated, removed, files: [{ path, change, diff }], blockedBy, noop, issues, warnings }` and writes nothing; `issues` are the deploy checks that would refuse that tree, `warnings` the ones it would deploy with. The restore needs `confirm` set to the workspace's name (400 without) and takes the dry run's `expectRemoved`; it deploys that revision's snapshot as a new revision (undo: restore to `from`; `to.sha` is the same id as `to.id`, kept from when revisions were git commits), keeps runs, `state/`, storage, secrets and agent-written memory, and is refused 409 while runs are in flight or when it would remove something not in `expectRemoved`. `workspace:write`; audited as `workspace.restored` |
|
|
217
212
|
|
|
218
213
|
`status` is one of `queued`, `running`, `awaiting-approval`, `completed`,
|
|
219
214
|
`failed`.
|
|
@@ -329,7 +324,7 @@ Reads return which secrets exist, never their values.
|
|
|
329
324
|
|
|
330
325
|
| Route | Methods | |
|
|
331
326
|
|---|---|---|
|
|
332
|
-
| `/api/keys` | GET, POST, DELETE | API keys — an admin's (`keys:manage`) to list, mint and revoke. POST `label`, `role` (viewer \| editor \| admin, never above your own, default editor), `workspaces` (names, or `null` for all — never wider than your own scope); `expires` (`30d`, `90d`, `365d`, any number of days up to 3650, or `never` — the default; anything else is a 400, never a key that lives forever); returns `{ ok, key, id, prefix, role, workspaces, expiresAt }`, and the key is shown once. GET lists each key with `createdAt`, `createdBy`, `expiresAt`, `lastUsedAt`, `lastIp` and `replacedBy` (the id of the key a rotation replaced it with; `null` until then) — the last-use pair written at most once a minute per key, the address cut to its /24 (IPv4) or /48 (IPv6). A key past its `expiresAt` is refused with 401 `API key expired on <date>`; the owner is emailed seven days before. Mint, revoke and rotate are written to the audit log. POST with `workspace`
|
|
327
|
+
| `/api/keys` | GET, POST, DELETE | API keys — an admin's (`keys:manage`) to list, mint and revoke. POST `label`, `role` (viewer \| editor \| admin, never above your own, default editor), `workspaces` (names, or `null` for all — never wider than your own scope); `expires` (`30d`, `90d`, `365d`, any number of days up to 3650, or `never` — the default; anything else is a 400, never a key that lives forever); returns `{ ok, key, id, prefix, role, workspaces, expiresAt }`, and the key is shown once. GET lists each key with `createdAt`, `createdBy`, `expiresAt`, `lastUsedAt`, `lastIp` and `replacedBy` (the id of the key a rotation replaced it with; `null` until then) — the last-use pair written at most once a minute per key, the address cut to its /24 (IPv4) or /48 (IPv6). A key past its `expiresAt` is refused with 401 `API key expired on <date>`; the owner is emailed seven days before. Mint, revoke and rotate are written to the audit log. POST with `workspace` or `access` is refused (400): those minted deploy keys for the platform's own git, which went on 17 Oct 2026 — a deploy key minted before is refused everywhere (403). A key for one workspace is `workspaces: ["<ws>"]`. DELETE `id`. `?tenant=` / body `tenant` is read only on an install with no users yet (first-run bootstrap) |
|
|
333
328
|
| `/api/keys/<id>/rotate` | POST | a new key with the old one's label, role, workspaces and deploy scope — and, when it had an end, the same lifetime from today — returned once like a mint (`{ ok, key, id, prefix, role, workspaces, expiresAt, replaced: { id, endsAt } }`). `grace`: `0` (default — the old key is revoked at once), or a duration up to `7d` such as `1h` or `24h`, during which the old key still works and after which it is refused as expired. A key is rotated once: rotating a key already replaced is 409 `this key was already rotated to <prefix>…; rotate that one` — and someone who lost the new key's secret rotates the NEW key, which mints another and ends the lost one (rotating the old one again would leave the lost key working). Two rotations of one key at once mint one key. `keys:manage`; a key outside the caller's scope is 404. Audited as `apikey.rotated` |
|
|
334
329
|
| `/api/team` | GET, POST | members with their roles and workspace scope; mint an invite — `role` (viewer \| editor \| admin, default editor), `workspaces` (names, or omit for all), `email` (bound into the invite: only that address can accept it; sent through the account's `RESEND_API_KEY` unless `send: false`). Returns `path` (the invite's relative URL), `expires`, `sent` and any `sendError` either way; the full link is in the mail |
|
|
335
330
|
| `/api/team/members/<id>` | PATCH, DELETE | change a member's role (`role`) and/or which workspaces they may open (`workspaces`: names, or null for all); remove them |
|
|
@@ -473,4 +468,3 @@ signed-in person, never by a key — mints an ordinary API key at their role.
|
|
|
473
468
|
| `/api/approve/<tenant>/<ws>/<id>?token=` | GET, POST | decide a run parked on a human, from the link in its notification. GET shows what is waiting and two buttons — it never decides, because inbox link-checkers follow GETs. POST `decision` (`approve` or `reject`) and an optional `note`, form-encoded or JSON; answers JSON when `Accept` prefers it. The token is derived from the run id under the install key, not stored, so a key rotation kills every link. Needs `FOLDRUN_PUBLIC_URL` to be minted at all |
|
|
474
469
|
| `/api/workspaces/<ws>/hooks/<flow>` | GET | read the flow's webhook URL without changing it: `{ ok, flow, path, url }` — `path` carries the token, `url` is it made absolute with `FOLDRUN_PUBLIC_URL` (null when that is unset). `workspace:manage`, as rotate and the settings page that shows it, since the URL is a credential. `404` for no such flow, `409` for a flow that is not `trigger: webhook` |
|
|
475
470
|
| `/api/workspaces/<ws>/hooks/<flow>/rotate` | POST | new token, old URLs stop working |
|
|
476
|
-
| `/api/git/<tenant>/<ws>?branch=&dir=` | POST | GitHub push webhook — HMAC-signed, fetches the tarball at that commit and deploys it |
|
|
@@ -29,7 +29,7 @@ It holds the database only, not the data volume.
|
|
|
29
29
|
### What it does not hold
|
|
30
30
|
|
|
31
31
|
- Workspace files, run records, `state/`, `memory/`, the vault and each
|
|
32
|
-
workspace's
|
|
32
|
+
workspace's history: the data volume is not backed up. A workspace's
|
|
33
33
|
source can still be restored from its own history (below).
|
|
34
34
|
- **Storage file bytes.** These live in object storage (R2), not on the data
|
|
35
35
|
volume. *Unverified:* whether a storage file deleted by mistake can still
|
|
@@ -63,18 +63,26 @@ public key).
|
|
|
63
63
|
|
|
64
64
|
## Restoring a workspace yourself
|
|
65
65
|
|
|
66
|
-
Every change to a workspace — a dashboard save, a deploy,
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
Every change to a workspace — a dashboard save, a deploy, an import — is a
|
|
67
|
+
revision, and every revision keeps a snapshot of the whole source as it
|
|
68
|
+
stood right after it. Restoring puts the workspace's source back as it was
|
|
69
|
+
at one of them — the platform's own deploy history, so it works the same
|
|
70
|
+
whether or not you keep the source in git:
|
|
69
71
|
|
|
70
72
|
```sh
|
|
71
73
|
foldrun restore sales --to 2026-09-28T14:00 --dry-run # what would change, with the diff
|
|
72
74
|
foldrun restore sales --to 2026-09-28T14:00 # asks you to type the name, then does it
|
|
73
75
|
```
|
|
74
76
|
|
|
75
|
-
`--to` takes a
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
`--to` takes a revision id (`r-…`, from the History page or `foldrun
|
|
78
|
+
history`), the commit a deploy named, a date or time (the last revision at
|
|
79
|
+
or before it — a time without a zone is read in the server's timezone, so
|
|
80
|
+
add `Z` or an offset to be exact), or an age: `90m`, `12h`, `3d`, `2w`.
|
|
81
|
+
A revision's diff is that one change; its snapshot is the whole source as
|
|
82
|
+
it stood on disk just after it, so two people saving at once each see the
|
|
83
|
+
other's change in their snapshot. History holds snapshots from 17 October 2026: each workspace's starts with
|
|
84
|
+
a "history starts here" revision, its source as it stood then, and nothing
|
|
85
|
+
before it can be restored this way.
|
|
78
86
|
|
|
79
87
|
- **Preview first.** The dry run lists every file that would change, come
|
|
80
88
|
back, or go, with a line diff. Nothing is written.
|
|
@@ -82,8 +90,8 @@ server's timezone, so add `Z` or an offset to be exact), or an age: `90m`, `12h`
|
|
|
82
90
|
back (`confirm` in the API). The CLI sends the removals you were shown, and
|
|
83
91
|
if anything else would now be removed — a file added in between — the
|
|
84
92
|
restore is refused and you are asked to look again.
|
|
85
|
-
- **It is undoable.** A restore is a new
|
|
86
|
-
ones; history says "restored to
|
|
93
|
+
- **It is undoable.** A restore is a new revision whose files are the old
|
|
94
|
+
ones; history says "restored to r-…". To undo it, restore to the revision
|
|
87
95
|
it answered as `from`.
|
|
88
96
|
- **It touches only the source.** Runs, `state/`, storage, secrets and the
|
|
89
97
|
memory agents wrote are what a deploy keeps, and a restore is a deploy:
|
|
@@ -97,11 +105,12 @@ server's timezone, so add `Z` or an offset to be exact), or an age: `90m`, `12h`
|
|
|
97
105
|
```http
|
|
98
106
|
POST /api/workspaces/<ws>/restore
|
|
99
107
|
{ "to": "3d", "dryRun": true }
|
|
100
|
-
{ "to": "<
|
|
108
|
+
{ "to": "<to.id from the dry run>", "confirm": "<ws>", "expectRemoved": [ … ] }
|
|
101
109
|
```
|
|
102
110
|
|
|
103
|
-
The workspace's History page lists every
|
|
104
|
-
|
|
111
|
+
The workspace's History page lists every revision; open one and **Roll back
|
|
112
|
+
to this** does the same restore, by revision instead of by time, with its
|
|
113
|
+
preview first.
|
|
105
114
|
|
|
106
115
|
## Asking for a restore from a snapshot
|
|
107
116
|
|
|
@@ -25,8 +25,9 @@ mistake the editor names rather than a cap it quietly keeps.
|
|
|
25
25
|
When an agent's cap and the flow's both apply to one step, the tighter one
|
|
26
26
|
wins, and the step's error says which line set it: `budget: in the flow
|
|
27
27
|
file` or `budget: on the writer agent`.
|
|
28
|
-
| **runs at once** | `concurrency: 3` in the account's `AGENTS.md`, or Settings → Defaults | the fourth run waits its turn in the queue — never refused, only later. Empty means the install's plan number (`FOLDRUN_PLAN_CONCURRENCY`, which applies only where billing is on), or no cap |
|
|
29
|
-
| **the queue lane** | `priority:
|
|
28
|
+
| **runs at once** | `concurrency: 3` in the account's `AGENTS.md`, or Settings → Defaults | the fourth run waits its turn in the queue — never refused, only later. Empty means the install's share (`FOLDRUN_ACCOUNT_CONCURRENCY`) or plan number (`FOLDRUN_PLAN_CONCURRENCY`, which applies only where billing is on), or no cap. Your own number can lower that, never raise it; a higher number comes with a plan |
|
|
29
|
+
| **the queue lane** | the plan's lane (`high` \| `normal` \| `low`); `priority: low` in the account's `AGENTS.md` lowers it, and `high` there counts only with a plan that gives it | when slots are short, accounts take turns, weighted by lane — high 3, normal 2, low 1: every account's first run goes before anyone's second, a fast lane takes three turns to a slow lane's one and never all of them, an account with runs going waits behind one with none, and a run that has waited ten minutes moves up a turn. A flow's own `priority:` orders runs within that. The cap above still holds |
|
|
30
|
+
| **room to start** | the install's `FOLDRUN_RUN_MEMORY_GIB` and `FOLDRUN_ACCOUNT_PODS` | a step whose sandbox would not fit, or whose account already has its most sandboxes running, waits to start — `room: waiting to start — …` on its trail — instead of failing. The wait is not billed; the run page's start-up chip shows it as `room`. A step still waiting after 30 minutes (`FOLDRUN_ROOM_WAIT_SEC`) fails, saying why |
|
|
30
31
|
|
|
31
32
|
A cap is a literal, never an expression, so it is readable off the file. The
|
|
32
33
|
run cap is hard: a step that reaches its share of the remainder is stopped
|
package/docs/checks.md
CHANGED
|
@@ -238,11 +238,48 @@ today.
|
|
|
238
238
|
|
|
239
239
|
### config/unknown-field
|
|
240
240
|
|
|
241
|
-
*warn.* A frontmatter key
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
241
|
+
*warn or info.* A frontmatter key that this kind of file does not read. There
|
|
242
|
+
are three cases, each with its own message:
|
|
243
|
+
- **A near-miss** of a real field (`tolls:`, `triger:`, `disallowed_tools:`) is
|
|
244
|
+
a warning that names the field it meant. YAML accepts a typo, so it is
|
|
245
|
+
silently ignored, and `tolls: [web]` leaves the agent with no tools.
|
|
246
|
+
- **A field read somewhere else** is a warning that says where it goes.
|
|
247
|
+
`timeout: 3600` on an agent is a step option, so it belongs under the step
|
|
248
|
+
in the flow. `language:` on a flow is read from the agent or `AGENTS.md`.
|
|
249
|
+
- **A field like nothing foldrun reads** (`favourite_colour:`) is a note,
|
|
250
|
+
since another tool may own it.
|
|
251
|
+
|
|
252
|
+
### config/wrong-type
|
|
253
|
+
|
|
254
|
+
*warn.* The right field holding the wrong kind of value: a map where a list
|
|
255
|
+
of names goes (`tools: {web: true}`), a list where one word goes. The runtime
|
|
256
|
+
does not read it. The message says what the field takes.
|
|
257
|
+
|
|
258
|
+
### config/invalid-value
|
|
259
|
+
|
|
260
|
+
*warn.* A value that does nothing, or quietly does something else:
|
|
261
|
+
- `effort: extreme` runs at the model's default;
|
|
262
|
+
- `size: giant` becomes `large`;
|
|
263
|
+
- `permissionMode:` other than `plan` is ignored;
|
|
264
|
+
- `trigger: sometimes` never fires;
|
|
265
|
+
- a `schedule:` the scheduler cannot read (`every day`) never fires. This is
|
|
266
|
+
checked with the scheduler's own cron parser;
|
|
267
|
+
- an unknown HTTP method in `methods:` is dropped;
|
|
268
|
+
- a duration that is not one (`debounce: soon`).
|
|
269
|
+
|
|
270
|
+
Aliases count as valid: `effort: quick` is `low`. Fields another rule already
|
|
271
|
+
checks in depth (`network:`, `limits:`, `timezone:`, `web:`) are left to it,
|
|
272
|
+
so nothing is reported twice.
|
|
273
|
+
|
|
274
|
+
### config/missing-field
|
|
275
|
+
|
|
276
|
+
*warn.* A required field is absent: a skill with no `description:` is never
|
|
277
|
+
loaded, because the model chooses skills by their descriptions.
|
|
278
|
+
|
|
279
|
+
Every field these four rules know is listed, with its type and values, in one
|
|
280
|
+
place in `@foldrun/core` (`frontmatter-fields.ts`). The editor's completions
|
|
281
|
+
come from the same list, so what the editor suggests and what `check`
|
|
282
|
+
accepts cannot drift apart.
|
|
246
283
|
|
|
247
284
|
### check/ignore-needs-reason
|
|
248
285
|
|
package/docs/cli.md
CHANGED
|
@@ -127,7 +127,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
127
127
|
| `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 |
|
|
128
128
|
| `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 |
|
|
129
129
|
| `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>` |
|
|
130
|
-
| `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 |
|
|
130
|
+
| `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), and start-up — each phase of a step's first seconds, its own share and when it was done, p50/p95. `--since <days>` (default 30), `--json` for the document |
|
|
131
131
|
| `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 |
|
|
132
132
|
| `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 |
|
|
133
133
|
| `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`) |
|
|
@@ -145,9 +145,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
145
145
|
| `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) |
|
|
146
146
|
| `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>` |
|
|
147
147
|
| `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`) |
|
|
148
|
-
| `foldrun
|
|
149
|
-
| `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`) |
|
|
150
|
-
| `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) |
|
|
148
|
+
| `foldrun restore <workspace> --to <revision\|time\|age>` | put a workspace's source back as it was then — `--to` is a revision id (`r-…`, from `foldrun history`), the commit a deploy named, a date/time (the last revision 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 revision, 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) |
|
|
151
149
|
| `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`) |
|
|
152
150
|
| `foldrun accounts` | every account signed in on this machine, and which one a bare command acts as. `foldrun profiles` is the same command |
|
|
153
151
|
| `foldrun use <name>` | act as one of them from here on. `foldrun switch <name>` is the same command |
|
|
@@ -214,7 +212,6 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
214
212
|
| `--client-id` / `--client-secret` | `connect`: the OAuth app; prompted when omitted, the secret never echoed |
|
|
215
213
|
| `--port <n>` | `connect`: the loopback port (default 8642) |
|
|
216
214
|
| `--role <r>` | `keys create`: `viewer`, `editor` (default) or `admin` |
|
|
217
|
-
| `--for <workspace>` | `keys create`: a deploy key for one workspace; `--access read` or `write` |
|
|
218
215
|
| `--expires <span>` | `keys create`: when it stops working — `30d`, `90d`, `365d` or `never` (default) |
|
|
219
216
|
| `--grace <span>` | `keys rotate`: keep the old key working for `1h` or `24h` (default `0`: revoked now) |
|
|
220
217
|
| `--force` | `deploy`: deploy while runs are in flight. `pull`, `storage get`: overwrite local files |
|
|
@@ -292,8 +289,7 @@ admin's: a key is never an owner.
|
|
|
292
289
|
installation; the last one signed in to is the default afterwards, and
|
|
293
290
|
`--url` on any command picks another. `foldrun login --token <key>` stores a
|
|
294
291
|
key made in the dashboard instead of opening a browser — for a machine with
|
|
295
|
-
none
|
|
296
|
-
works.
|
|
292
|
+
none — after checking that it works.
|
|
297
293
|
|
|
298
294
|
`foldrun whoami` says who the platform thinks you are and where the
|
|
299
295
|
credential came from. `foldrun logout` forgets the key and, when `login`
|
|
@@ -301,8 +297,8 @@ made it, revokes it.
|
|
|
301
297
|
|
|
302
298
|
**In CI, set `FOLDRUN_TOKEN`** rather than logging in: the environment beats
|
|
303
299
|
the file, so a job with a key in its variables never reads one from disk.
|
|
304
|
-
`foldrun keys create ci --
|
|
305
|
-
|
|
300
|
+
`foldrun keys create ci --workspaces leads` makes a key that opens that one
|
|
301
|
+
workspace and nothing else on the account.
|
|
306
302
|
|
|
307
303
|
## Signing in to a site, for an agent
|
|
308
304
|
|
|
@@ -478,7 +474,6 @@ foldrun logs --to publish-desk # what ran there lately
|
|
|
478
474
|
foldrun open runs # the same, in the browser
|
|
479
475
|
```
|
|
480
476
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
disappear when the branch does.
|
|
477
|
+
Previews of a branch come back with the GitHub and GitLab app; until then a
|
|
478
|
+
second workspace (`foldrun deploy --to leads-try`) is the place to try a
|
|
479
|
+
change.
|
package/docs/deploying.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Deploying
|
|
2
2
|
|
|
3
|
-
A workspace deploys by
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
A workspace deploys by sending its files: `foldrun deploy` from a folder,
|
|
4
|
+
from your laptop or from CI. Every deploy is checked before anything goes
|
|
5
|
+
live, refuses to swap files under a run in flight, and never touches what
|
|
6
|
+
the platform owns.
|
|
7
7
|
|
|
8
8
|
## From a folder
|
|
9
9
|
|
|
@@ -15,67 +15,56 @@ foldrun deploy --to leads --commit $(git rev-parse HEAD)
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
In CI there is no browser: set `FOLDRUN_URL` and `FOLDRUN_TOKEN` (a key from
|
|
18
|
-
Settings → API keys, or `foldrun keys create`) and the same commands run.
|
|
18
|
+
Settings → API keys, or `foldrun keys create`) and the same commands run. A
|
|
19
|
+
key for CI should open the one workspace it deploys:
|
|
20
|
+
`foldrun keys create ci --workspaces leads`.
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
`--commit` records which commit of your own repository the files came from;
|
|
23
|
+
the workspace page and its History show it.
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
## Your own GitHub or GitLab
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
workspace, read or write, with `foldrun keys create ci --for <workspace>
|
|
32
|
-
--access write` or on the Settings page — is what a CI job or a contractor
|
|
33
|
-
should hold; it is refused on every other workspace and on every other route.
|
|
34
|
-
|
|
35
|
-
Push a branch other than `main` and you get a **preview**: a workspace named
|
|
36
|
-
`<workspace>-preview-<branch>` deployed from that branch, with its evals run
|
|
37
|
-
there, its schedules never fired, and its source's secrets. Deleting the
|
|
38
|
-
branch deletes it.
|
|
27
|
+
Keep the folder in your own repository and deploy it from CI with the
|
|
28
|
+
commands above. A connected app — a push to your main branch deploys,
|
|
29
|
+
other branches become previews, a check on every commit — is coming, for
|
|
30
|
+
GitHub first, then GitLab. Until 17 October 2026 every workspace was also a
|
|
31
|
+
git remote on the platform, with deploy keys, branch previews, a mirror and
|
|
32
|
+
a GitHub push webhook; those went, and history moved to the platform's own
|
|
33
|
+
snapshots (below).
|
|
39
34
|
|
|
40
35
|
## What a deploy checks
|
|
41
36
|
|
|
42
37
|
The tree is validated the way `foldrun check` validates it — a step naming an
|
|
43
38
|
agent that does not exist, a tool that resolves to nothing, a knowledge
|
|
44
|
-
concept with no `type:` — and a
|
|
45
|
-
|
|
39
|
+
concept with no `type:` — and a deploy with issues is refused with the list.
|
|
40
|
+
The workspace on disk is unchanged.
|
|
46
41
|
|
|
47
42
|
## What a deploy never touches
|
|
48
43
|
|
|
49
44
|
`runs/`, `state/`, `secrets.json`, and any memory an agent wrote that the
|
|
50
|
-
|
|
45
|
+
deploy does not mention. A deploy that reverted what an agent learned would
|
|
51
46
|
make every run a little dumber, and a deploy that reset a cursor would make
|
|
52
47
|
the next run redo a fortnight.
|
|
53
48
|
|
|
54
49
|
## Runs in flight
|
|
55
50
|
|
|
56
|
-
A
|
|
57
|
-
— is
|
|
58
|
-
applies it the moment those runs finish. Swapping files under a running flow
|
|
51
|
+
A deploy while a run is in flight — including one parked at an approval gate
|
|
52
|
+
— is refused, saying which runs hold it. Swapping files under a running flow
|
|
59
53
|
would mean step 3 reads agents step 1 never saw. `foldrun deploy --force`
|
|
60
54
|
overrides that when you mean it.
|
|
61
55
|
|
|
62
56
|
## History and rollback
|
|
63
57
|
|
|
64
|
-
Every change to a workspace is a revision — a
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
Every change to a workspace is a revision — a deploy, a dashboard edit, an
|
|
59
|
+
import — with who, when and a diff, on the workspace's History page and at
|
|
60
|
+
`/api/workspaces/<ws>/history`. Every revision also keeps a snapshot of the
|
|
61
|
+
whole source as it stood just after it: the platform's own deploy history,
|
|
62
|
+
so rolling back never needs a repository.
|
|
68
63
|
|
|
69
|
-
A rollback is
|
|
70
|
-
|
|
71
|
-
moves forward, never rewrites. **Preview** beside
|
|
72
|
-
"Preview deploy" beside a branch's "Merge & deploy" on the Repository page)
|
|
64
|
+
A rollback is **Roll back to this** on a History row, or
|
|
65
|
+
`foldrun restore <ws> --to <revision|time|age>`, and it lands as a **new
|
|
66
|
+
revision** — history moves forward, never rewrites. **Preview** beside it
|
|
73
67
|
shows first what it would change, add and remove, with diffs, and any run in
|
|
74
68
|
flight that would hold it — the restore dry run
|
|
75
|
-
(`POST …/restore {to, dryRun: true}`), which is the same operation.
|
|
76
|
-
|
|
77
|
-
## Mirroring
|
|
78
|
-
|
|
79
|
-
A workspace can mirror to a repository you host — GitHub, GitLab — over
|
|
80
|
-
https with a token held as the `MIRROR_TOKEN` secret. The platform's remote
|
|
81
|
-
stays the mechanism; the mirror is the copy you read elsewhere.
|
|
69
|
+
(`POST …/restore {to, dryRun: true}`), which is the same operation. See
|
|
70
|
+
[Backups and restore](backups-and-restore).
|
package/docs/flows.md
CHANGED
|
@@ -338,7 +338,7 @@ normal outcome, recorded as one.
|
|
|
338
338
|
| `sla` | how long a run is expected to take; past it, one notification |
|
|
339
339
|
| `approvers` | who may decide this flow's gates — addresses, or `admins` |
|
|
340
340
|
| `approve_within` | how long a gate waits before the run is rejected |
|
|
341
|
-
| `live` | `true` to make this flow's runs real where the platform would otherwise make them [test runs](runs#test-runs):
|
|
341
|
+
| `live` | `true` to make this flow's runs real where the platform would otherwise make them [test runs](runs#test-runs): as an eval's subject. Only ever an opt-out of the safe default |
|
|
342
342
|
|
|
343
343
|
Every duration here is written the way `wait:` is — `45s`, `10m`, `2h`, `3d`,
|
|
344
344
|
or bare seconds — and capped at thirty days.
|
|
@@ -481,12 +481,11 @@ almost never means "run two". `queue` starts it but holds it until the live one
|
|
|
481
481
|
finishes. Unset, runs may overlap.
|
|
482
482
|
|
|
483
483
|
`priority:` decides where the flow's runs stand when the platform has more
|
|
484
|
-
runs than slots.
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
fleet.
|
|
484
|
+
runs than slots. Accounts take turns first, weighted by lane (see [Budgets
|
|
485
|
+
and billing](budgets-and-billing)), and among runs level on that, `high` goes before `normal` (unset) and `normal`
|
|
486
|
+
before `low`. It adds to the account's lane, so a high flow on a low account
|
|
487
|
+
is normal. The account's `concurrency:` still bounds how many of its runs are
|
|
488
|
+
live at once.
|
|
490
489
|
|
|
491
490
|
## Model resolution
|
|
492
491
|
|
package/docs/glossary.md
CHANGED
|
@@ -20,8 +20,8 @@ checks and headlines read.
|
|
|
20
20
|
a blog desk, a Reddit desk — its agents and flows, running on a schedule.
|
|
21
21
|
Nothing in the format is called a desk; it is a workspace.
|
|
22
22
|
|
|
23
|
-
**deploy** —
|
|
24
|
-
`foldrun deploy
|
|
23
|
+
**deploy** — sending a workspace's source tree to the platform with
|
|
24
|
+
`foldrun deploy`. Never touches runs, state, secrets or memory.
|
|
25
25
|
|
|
26
26
|
**eval** — a test for an agent or a flow: a task and what a good answer
|
|
27
27
|
must contain. Runs on every push.
|
package/docs/introduction.md
CHANGED
|
@@ -34,7 +34,7 @@ The design bets on three things:
|
|
|
34
34
|
| **Locally** | `npm install -g foldrun` and a model key | everything on your machine; no account, no server |
|
|
35
35
|
| **Hosted** | an account | ours: schedules, sandboxes, a vault, a team |
|
|
36
36
|
|
|
37
|
-
The same workspace moves between them with `foldrun deploy
|
|
37
|
+
The same workspace moves between them with `foldrun deploy`.
|
|
38
38
|
Start locally; that is where every workspace should be born. The CLI and
|
|
39
39
|
runtime are free from npm; the platform is not distributed — see
|
|
40
40
|
[Running it yourself](self-hosting).
|
package/docs/runs.md
CHANGED
|
@@ -17,6 +17,7 @@ Each run holds its steps in group order. A step carries:
|
|
|
17
17
|
| `data` | the parsed value, for a step with `output: json` |
|
|
18
18
|
| `events` | tool calls, script runs with their timing, verify verdicts, refusals |
|
|
19
19
|
| `costUsd`, `tokens`, `computeSecs`, `startupSecs` | what it cost and how long the sandbox lived — **summed across its attempts** |
|
|
20
|
+
| `startup` | where the step's first seconds went: when each start-up phase ended, in seconds since the step started — `services` (the account's search/browser pods up), `room` (room on the cluster to start — see [Budgets and billing](budgets-and-billing)), `applied` (sandbox created), `ready`, `files` (workspace copied in), `go` (step released), `shim`, `system` (system packages restored), `driver`, `imports`, `tools` (runtime and tools built) and `model` (the model's first text or tool call). Only the phases the step went through; `null` for a step that ran in-process, absent before 17 Oct 2026. The last attempt's |
|
|
20
21
|
| `image` | which runner image the step's sandbox ran (isolated runs on a cluster): `variant` `full` or `slim`, `why`, and `pod: true` when a browsing step ran slim and browsed through the account's browser pod ([slim steps and a lost pod](tools#slim-steps-and-a-lost-pod)) |
|
|
21
22
|
| `browserPod` | on a step that browsed through the pod from slim: `reconnects` (tries), `reconnected` (how many got through: the call went on after it) and `closedAgain` (how many reached the pod only for the call to drop again at once, when any did); when the pod was lost, `lost` (`cause` `lost` or `needs-full`, and `detail`) and `writes` (what the step had written by then, worded); then `fallback: "browser pod lost; re-ran on full"` when it had only read and ran again on the full image, or `failure` — `browser pod died after the step had written <what>` — when it had written and was not re-run |
|
|
22
23
|
| `tries` | one row per attempt, oldest first (and a `status: "lost"` row, `image: "slim"`, before the full re-run when the browser pod was lost under a slim step that had only read — see [tools](tools#slim-steps-and-a-lost-pod)): `n` (the try, numbered in the order they ran), `attempt` (the step's attempt it belongs to, only when that is not `n` — the full re-run after a lost slim try 1 is `n: 2, attempt: 1`), `status`, `costUsd`, `tokens`, `computeSecs`, `startedAt`, `finishedAt`, the attempt's `image` and its `browserPod` line when it had one, and the attempt's last `error` when it failed. A `retry: 2` step that failed twice and then passed has three rows, and its `costUsd` is all three — the run's spend, the flow's `budget:` and the bill read the step's figure, so the figure is the total. A retry is handed what is left of the budget after the failed attempt spent, and nothing left means no retry, said on the record |
|
|
@@ -47,6 +48,8 @@ one bar per step with its tool calls marked in milliseconds, so a slow run
|
|
|
47
48
|
says which step was slow. Every step shows its instruction, what it was
|
|
48
49
|
handed from earlier groups, what it returned, its cost, and how many times it
|
|
49
50
|
reached for a path it was not allowed and was refused (see [Security](security)).
|
|
51
|
+
A **start-up** chip in a step's header says how long it took the step to
|
|
52
|
+
reach the model's first word; hover it for each phase's share.
|
|
50
53
|
|
|
51
54
|
The page follows the run as it moves, gates included, without a reload: a
|
|
52
55
|
**live** dot beside the status says the stream is connected (amber while it
|
|
@@ -124,9 +127,8 @@ cannot reach it.
|
|
|
124
127
|
Start one with the **Test run** checkbox on the Run button, `foldrun run
|
|
125
128
|
<flow> --test` locally, `foldrun invoke <flow> --test` on a platform, or
|
|
126
129
|
`POST .../flows/<flow>/run` with `{ "test": true }`. An eval's runs are test
|
|
127
|
-
runs
|
|
128
|
-
|
|
129
|
-
default is opted out of, never into. A re-run of a test run, or a run started
|
|
130
|
+
runs unless the eval or the flow says `live: true` in its frontmatter — the
|
|
131
|
+
safe default is opted out of, never into. A re-run of a test run, or a run started
|
|
130
132
|
from one of its steps, stays a test.
|
|
131
133
|
|
|
132
134
|
What the platform does on a test run, in the order a step meets it:
|
package/docs/self-hosting.md
CHANGED
|
@@ -51,8 +51,7 @@ foldrun login
|
|
|
51
51
|
foldrun deploy # this folder → a workspace with the same name
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
the folder changes. What the platform adds is the operating around it —
|
|
54
|
+
Nothing about the folder changes. What the platform adds is the operating around it —
|
|
56
55
|
[Scheduling](scheduling), [Runs](runs) in gVisor sandboxes,
|
|
57
56
|
[Secrets](secrets) in a vault, [Notifications](notifications),
|
|
58
57
|
[Budgets and billing](budgets-and-billing), [Team and access](team-and-access).
|
|
@@ -61,6 +60,6 @@ the folder changes. What the platform adds is the operating around it —
|
|
|
61
60
|
|
|
62
61
|
On your machine: in the folder. On the platform: your workspace's files and
|
|
63
62
|
every run's record, on our data volume and in the file store, readable
|
|
64
|
-
through the dashboard and the API, exportable as a
|
|
63
|
+
through the dashboard and the API, exportable as a .zip (`foldrun export`). A deploy never
|
|
65
64
|
touches what an agent learned or a run recorded. See [Storage and
|
|
66
65
|
sharing](storage-and-sharing) and [Security](security).
|
package/docs/team-and-access.md
CHANGED
|
@@ -44,17 +44,16 @@ connection, or returned as a link to pass on yourself.
|
|
|
44
44
|
A key is minted with a role (`editor` unless you say otherwise) and acts as
|
|
45
45
|
the account with that role. It can never be wider than the person who made
|
|
46
46
|
it: not a higher role, and not a workspace they cannot open — a key minted
|
|
47
|
-
by someone scoped to two workspaces is scoped to those two.
|
|
48
|
-
honours the same scope: pushing needs write on that workspace, and pushing
|
|
49
|
-
the library needs admin. Two things a key cannot do: act against a named
|
|
47
|
+
by someone scoped to two workspaces is scoped to those two. Two things a key cannot do: act against a named
|
|
50
48
|
member — inviting, re-roling, removing, transferring need a signed-in person,
|
|
51
49
|
because a key names an account and never a who — and exceed its role.
|
|
52
50
|
A scoped admin sees and revokes only keys inside its own scope; a key wider
|
|
53
51
|
than that, the owner's included, is not in its list.
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
A CI job or a contractor should hold a key that opens one workspace —
|
|
54
|
+
`foldrun keys create ci --workspaces leads`, or Settings → API keys. (Deploy
|
|
55
|
+
keys, for the platform's own git, went with it on 17 October 2026; one
|
|
56
|
+
minted before opens nothing.)
|
|
58
57
|
|
|
59
58
|
A key **is** its account: the platform reads the account off the key, so one
|
|
60
59
|
account's key can never reach another's workspaces. Someone who looks after
|
package/docs/tools.md
CHANGED
|
@@ -1787,6 +1787,13 @@ Run `scripts/pace.py --spent <amount> --budget <amount>` from this skill's
|
|
|
1787
1787
|
folder, then report the verdict in one sentence.
|
|
1788
1788
|
```
|
|
1789
1789
|
|
|
1790
|
+
| field | |
|
|
1791
|
+
|---|---|
|
|
1792
|
+
| `name` | defaults to the folder's name |
|
|
1793
|
+
| `description` | **required**: what it does and when to use it. A skill without one is never loaded, because the model chooses skills by their descriptions |
|
|
1794
|
+
| `when` | run tags: the skill is loaded only on runs carrying one of them |
|
|
1795
|
+
| `license`, `compatibility`, `metadata`, `allowed-tools` | the Agent Skills format's optional fields. Accepted and carried, not read by foldrun |
|
|
1796
|
+
|
|
1790
1797
|
Only names and descriptions sit in context; the agent reads the full file when
|
|
1791
1798
|
a task matches. So an agent can carry many skills for a few tokens each — which
|
|
1792
1799
|
is the reason to move a procedure out of `agent.md` in the first place.
|
package/docs/workspaces.md
CHANGED
|
@@ -219,11 +219,10 @@ regenerated every run is `storage/`; a value accumulated across runs is
|
|
|
219
219
|
|
|
220
220
|
## What a deploy touches
|
|
221
221
|
|
|
222
|
-
A deploy — `foldrun deploy
|
|
223
|
-
replaces the source tree: agents, flows, tools, skills, evals, knowledge,
|
|
222
|
+
A deploy — `foldrun deploy` — replaces the source tree: agents, flows, tools, skills, evals, knowledge,
|
|
224
223
|
`AGENTS.md`. It **never** touches `runs/`, `state/`, `secrets.json`, or any
|
|
225
|
-
memory an agent wrote that your
|
|
226
|
-
platform, not to
|
|
224
|
+
memory an agent wrote that your deploy does not mention. Those belong to the
|
|
225
|
+
platform, not to your source, and a deploy that reverted what an agent learned would
|
|
227
226
|
make every run a little dumber.
|
|
228
227
|
|
|
229
228
|
The agent engine's own files are not workspace content either. Each step
|
|
@@ -234,14 +233,9 @@ They stay in the sandbox: never copied back, never copied in. (Your own
|
|
|
234
233
|
`.claude/agents/*.md` at the workspace root is different — a Claude Code
|
|
235
234
|
subagent you wrote, deployed as an agent.)
|
|
236
235
|
|
|
237
|
-
A
|
|
238
|
-
|
|
239
|
-
counts as in flight. See [Deploying](deploying).
|
|
236
|
+
A deploy while a run is in flight is refused, saying which runs hold it; a
|
|
237
|
+
run parked at an approval gate counts as in flight. See [Deploying](deploying).
|
|
240
238
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
workspace named `<workspace>-preview-<branch>` from that branch's tree, runs
|
|
245
|
-
its evals there, and never fires its schedules. It reads the source
|
|
246
|
-
workspace's secrets. Deleting the branch deletes the preview. It is how a
|
|
247
|
-
change to a live desk gets tried without touching the live desk.
|
|
239
|
+
Trying a change to a live desk without touching it is a second workspace —
|
|
240
|
+
`foldrun deploy --to <workspace>-try`. Branch previews come back with the
|
|
241
|
+
GitHub and GitLab app.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foldrun",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Agents are just folders. Write them, check them, run them — on your machine.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"node": ">=22"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@foldrun/core": "^
|
|
14
|
+
"@foldrun/core": "^2.0.0"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"bin",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"e2e": "FOLDRUN_E2E=1 FOLDRUN_DATA=$(mktemp -d /tmp/foldrun-e2e-XXXXXX) node --test --test-timeout=300000 tests/e2e.test.ts",
|
|
24
|
-
"test": "FOLDRUN_DATA=$(mktemp -d /tmp/foldrun-test-XXXXXX) node --test tests/cli-account.test.ts tests/model-credential.test.ts tests/cli-approvals.test.ts tests/cli-stop.test.ts tests/cli-account-settings.test.ts tests/cli-schedule.test.ts tests/cli-billing.test.ts tests/cli-storage.test.ts tests/cli-scaffold.test.ts tests/cli-secrets.test.ts tests/cli-site-login.test.ts tests/cli-check-library.test.ts tests/cli-check-timezone.test.ts tests/cli-check-frontmatter.test.ts tests/cli-agent-import.test.ts tests/cli-check-subagents.test.ts tests/cli-connect.test.ts tests/cli-accounts.test.ts tests/cli-remote.test.ts tests/cli-stream.test.ts tests/cli-doctor.test.ts tests/cli-tool-test.test.ts tests/cli-agent-run.test.ts tests/cli-check-remote.test.ts tests/cli-shares.test.ts tests/cli-triggers.test.ts tests/cli-providers.test.ts tests/cli-rerun-put.test.ts tests/cli-guide.test.ts tests/cli-runtimes.test.ts tests/cli-deploy-safety.test.ts tests/cli-operator.test.ts tests/cli-flow-patterns.test.ts tests/cli-flow-rm-step.test.ts tests/cli-flow-dup-step.test.ts tests/cli-flow-write-stale.test.ts tests/cli-agent-link-stale.test.ts tests/cli-parity.test.ts tests/cli-invoke-inputs.test.ts tests/cli-audit-keys.test.ts tests/cli-version.test.ts tests/cli-check-web-warnings.test.ts tests/cli-report-image.test.ts tests/cli-check-workspace-paths.test.ts tests/cli-parity-dashboard.test.ts tests/cli-api-standards.test.ts tests/cli-backups-onboarding.test.ts tests/cli-check-limits.test.ts tests/cli-gallery.test.ts tests/cli-status-platform.test.ts tests/cli-webhooks-notifications.test.ts tests/cli-flags.test.ts tests/cli-api-coverage.test.ts tests/cli-hooks.test.ts tests/cli-release.test.ts tests/cli-terminal.test.ts tests/cli-network.test.ts tests/cli-assistant.test.ts tests/cli-check-scan.test.ts",
|
|
24
|
+
"test": "FOLDRUN_DATA=$(mktemp -d /tmp/foldrun-test-XXXXXX) node --test tests/cli-account.test.ts tests/model-credential.test.ts tests/cli-approvals.test.ts tests/cli-stop.test.ts tests/cli-account-settings.test.ts tests/cli-schedule.test.ts tests/cli-billing.test.ts tests/cli-storage.test.ts tests/cli-scaffold.test.ts tests/cli-secrets.test.ts tests/cli-site-login.test.ts tests/cli-check-library.test.ts tests/cli-check-timezone.test.ts tests/cli-check-frontmatter.test.ts tests/cli-agent-import.test.ts tests/cli-check-subagents.test.ts tests/cli-connect.test.ts tests/cli-accounts.test.ts tests/cli-remote.test.ts tests/cli-stream.test.ts tests/cli-doctor.test.ts tests/cli-tool-test.test.ts tests/cli-agent-run.test.ts tests/cli-check-remote.test.ts tests/cli-shares.test.ts tests/cli-triggers.test.ts tests/cli-providers.test.ts tests/cli-rerun-put.test.ts tests/cli-guide.test.ts tests/cli-runtimes.test.ts tests/cli-deploy-safety.test.ts tests/cli-operator.test.ts tests/cli-flow-patterns.test.ts tests/cli-flow-rm-step.test.ts tests/cli-flow-dup-step.test.ts tests/cli-flow-write-stale.test.ts tests/cli-agent-link-stale.test.ts tests/cli-parity.test.ts tests/cli-invoke-inputs.test.ts tests/cli-audit-keys.test.ts tests/cli-version.test.ts tests/cli-check-web-warnings.test.ts tests/cli-report-image.test.ts tests/cli-check-workspace-paths.test.ts tests/cli-parity-dashboard.test.ts tests/cli-api-standards.test.ts tests/cli-backups-onboarding.test.ts tests/cli-check-limits.test.ts tests/cli-gallery.test.ts tests/cli-status-platform.test.ts tests/cli-webhooks-notifications.test.ts tests/cli-flags.test.ts tests/cli-api-coverage.test.ts tests/cli-hooks.test.ts tests/cli-release.test.ts tests/cli-terminal.test.ts tests/cli-network.test.ts tests/cli-assistant.test.ts tests/cli-check-scan.test.ts tests/cli-published.test.ts",
|
|
25
25
|
"release": "node scripts/release.mjs",
|
|
26
26
|
"typecheck": "tsc -p tsconfig.json",
|
|
27
27
|
"docs:sync": "node scripts/sync-docs.mjs",
|
package/src/commands.mjs
CHANGED
|
@@ -999,6 +999,10 @@ function narrowCheck(findings, root, opts, label, accountRoot = root) {
|
|
|
999
999
|
out = out.filter((f) => want.has(f.rule) || want.has(f.rule.split("/")[0]) || want.has(categoryOf(f.rule)));
|
|
1000
1000
|
}
|
|
1001
1001
|
if (opts.security) out = out.filter((f) => SECURITY_CATEGORIES.has(categoryOf(f.rule)));
|
|
1002
|
+
// check's own skill rule already says a skill has no description, as an
|
|
1003
|
+
// error; the schema's note would say it twice. Only when that error is
|
|
1004
|
+
// still in the report — `--rule config/missing-field` keeps the note.
|
|
1005
|
+
out = out.filter((f) => !(f.rule === "config/missing-field" && out.some((g) => g.file === f.file && g.rule === "skill/config" && /no description|needs name and description/.test(g.message))));
|
|
1002
1006
|
const file = path.join(root, ".foldrun", "check-baseline.json");
|
|
1003
1007
|
if (opts.writeBaseline) {
|
|
1004
1008
|
const created = new Date().toISOString();
|
|
@@ -3732,8 +3736,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
3732
3736
|
* asks "is this your terminal?"; the person says yes; the key that makes
|
|
3733
3737
|
* lands here and is kept in ~/.foldrun/credentials.json. No key to copy,
|
|
3734
3738
|
* nothing pasted into a shell history. `--token` skips the browser and
|
|
3735
|
-
* stores a key made in the dashboard — for a machine with no browser
|
|
3736
|
-
* deploy key for one workspace.
|
|
3739
|
+
* stores a key made in the dashboard — for a machine with no browser.
|
|
3737
3740
|
*/
|
|
3738
3741
|
async function login(flags) {
|
|
3739
3742
|
const url = normaliseUrl(flags.url ?? env("FOLDRUN_URL") ?? defaultPlatform() ?? DEFAULT_PLATFORM);
|
|
@@ -3968,8 +3971,7 @@ async function doctor(flags) {
|
|
|
3968
3971
|
*
|
|
3969
3972
|
* keys ls every key, live and revoked
|
|
3970
3973
|
* keys create <label> [--role r] an account key (editor unless said)
|
|
3971
|
-
* keys create <label> --
|
|
3972
|
-
* a deploy key: git clone/push for one workspace
|
|
3974
|
+
* keys create <label> --workspaces <ws> one that opens that workspace alone
|
|
3973
3975
|
* keys revoke <id>
|
|
3974
3976
|
* keys create <label> --expires 90d it stops working in 90 days (30d, 365d, never)
|
|
3975
3977
|
* keys rotate <id> [--grace 1h] a new key, same label/role/scope; the old
|
|
@@ -3993,7 +3995,8 @@ async function keysCmd(positional, flags) {
|
|
|
3993
3995
|
console.log("");
|
|
3994
3996
|
const day = (iso) => (iso ? iso.slice(0, 10) : "");
|
|
3995
3997
|
for (const k of keys) {
|
|
3996
|
-
|
|
3998
|
+
// A deploy key opened the platform's own git, gone since 17 Oct 2026.
|
|
3999
|
+
const what = k.scope ? "deploy key · opens nothing since the platform's git went — revoke it" : `${k.role ?? "admin"} · ${k.workspaces ? k.workspaces.join(",") || "no workspaces" : "all workspaces"}`;
|
|
3997
4000
|
const expired = !k.revokedAt && k.expiresAt && Date.parse(k.expiresAt) <= Date.now();
|
|
3998
4001
|
const state = k.revokedAt ? c.red("revoked") : expired ? c.red("expired") : c.green("live");
|
|
3999
4002
|
console.log(` ${state.padEnd(20)} ${k.id} ${k.prefix}… ${c.bold(k.label)} ${c.dim(what)}${k.createdBy ? c.dim(` by ${k.createdBy}`) : ""}`);
|
|
@@ -4008,8 +4011,6 @@ async function keysCmd(positional, flags) {
|
|
|
4008
4011
|
if (verb === "create" || verb === "new") {
|
|
4009
4012
|
if (!arg) throw new Error("what is the key for? foldrun keys create <label>");
|
|
4010
4013
|
const body = { label: arg };
|
|
4011
|
-
if (typeof flags.for === "string") body.workspace = flags.for;
|
|
4012
|
-
if (typeof flags.access === "string") body.access = flags.access;
|
|
4013
4014
|
if (typeof flags.role === "string") body.role = flags.role;
|
|
4014
4015
|
// --expires 30d|90d|365d|never; unsaid, the key never expires.
|
|
4015
4016
|
if (typeof flags.expires === "string") body.expires = flags.expires;
|
|
@@ -6207,10 +6208,34 @@ function filesWritten(run) {
|
|
|
6207
6208
|
return [...out].filter(Boolean);
|
|
6208
6209
|
}
|
|
6209
6210
|
|
|
6211
|
+
/** Trim what prose puts around a URL: trailing markdown/punctuation and a
|
|
6212
|
+
* trailing `)` that nothing in the URL opened — so `(url)` comes back bare,
|
|
6213
|
+
* but a wikipedia `..._(bar)` keeps its balanced `)`. `[` and `]` are not
|
|
6214
|
+
* URL chars, so a markdown `](url)` never carries a bracket in. */
|
|
6215
|
+
function trimUrl(u) {
|
|
6216
|
+
let prev;
|
|
6217
|
+
do {
|
|
6218
|
+
prev = u;
|
|
6219
|
+
u = u.replace(/[.,;:!?*_~`]+$/, "");
|
|
6220
|
+
if (u.at(-1) === ")" && u.split(")").length > u.split("(").length) u = u.slice(0, -1);
|
|
6221
|
+
} while (u !== prev);
|
|
6222
|
+
return u;
|
|
6223
|
+
}
|
|
6224
|
+
|
|
6210
6225
|
/** Every address a run said it published to, from its summary and conclusions. */
|
|
6211
|
-
function published(run) {
|
|
6226
|
+
export function published(run) {
|
|
6212
6227
|
const text = [run.summary ?? "", ...(run.steps ?? []).map((s) => s.conclusion ?? s.result ?? "")].join("\n");
|
|
6213
|
-
return [...new Set((text.match(/https?:\/\/[^\s
|
|
6228
|
+
return [...new Set((text.match(/https?:\/\/[^\s"'<>`\[\]]+/g) ?? []).map(trimUrl).filter(Boolean))];
|
|
6229
|
+
}
|
|
6230
|
+
|
|
6231
|
+
/** How long the step took to reach the model's first word — the record's
|
|
6232
|
+
* `startup` phases (seconds since the step started); null before 17 Oct
|
|
6233
|
+
* 2026 and for in-process steps. */
|
|
6234
|
+
export function startupLabel(step) {
|
|
6235
|
+
const ph = step.startup;
|
|
6236
|
+
if (!ph || typeof ph !== "object") return null;
|
|
6237
|
+
const end = typeof ph.model === "number" ? ph.model : Math.max(...Object.values(ph).filter((v) => typeof v === "number"));
|
|
6238
|
+
return Number.isFinite(end) ? `start-up ${end.toFixed(1)}s` : null;
|
|
6214
6239
|
}
|
|
6215
6240
|
|
|
6216
6241
|
/** Which runner image a step ran on, as the run page's chip says it:
|
|
@@ -6303,6 +6328,7 @@ async function reportCmd(runId, flags, layout) {
|
|
|
6303
6328
|
step.status,
|
|
6304
6329
|
step.attempts > 1 ? `${step.attempts} attempts` : null,
|
|
6305
6330
|
imageLabel(step),
|
|
6331
|
+
startupLabel(step),
|
|
6306
6332
|
step.costUsd ? `$${step.costUsd.toFixed(4)}` : null,
|
|
6307
6333
|
step.startedAt && step.finishedAt
|
|
6308
6334
|
? humanDuration(Date.parse(step.finishedAt) - Date.parse(step.startedAt))
|
|
@@ -8166,6 +8192,8 @@ function sinceDays(flags, fallback) {
|
|
|
8166
8192
|
}
|
|
8167
8193
|
|
|
8168
8194
|
const secs = (n) => (n == null ? "—" : humanDuration(n * 1000));
|
|
8195
|
+
/** Seconds that are often under one — start-up phases. */
|
|
8196
|
+
const fine = (n) => (n == null ? "—" : n < 1 ? `${Math.round(n * 1000)}ms` : `${n.toFixed(2)}s`);
|
|
8169
8197
|
const pct = (a, b) => (b ? `${Math.round((a / b) * 100)}%` : "—");
|
|
8170
8198
|
|
|
8171
8199
|
/** One workspace's observability report, as the Observe page reads it. */
|
|
@@ -8182,6 +8210,9 @@ function printObservation(o) {
|
|
|
8182
8210
|
table("flows", ["flow", "runs", "failed", "p50", "p95", "cost"], (o.flows ?? []).map((f) => [f.flow, f.runs, f.failed, secs(f.seconds?.p50), secs(f.seconds?.p95), `$${f.costUsd.toFixed(2)}`]));
|
|
8183
8211
|
table("agents", ["agent", "steps", "failed", "retried", "skipped", "p95", "cost"], (o.agents ?? []).map((a) => [a.agent, a.steps, a.failed, a.retried, a.skipped, secs(a.seconds?.p95), `$${a.costUsd.toFixed(2)}`]));
|
|
8184
8212
|
table("tools", ["tool", "calls", "errors", "p95", "used by"], (o.tools ?? []).map((t) => [t.name, t.calls, t.errors, t.ms?.p95 == null ? "—" : `${Math.round(t.ms.p95)}ms`, (t.agents ?? []).join(", ")]));
|
|
8213
|
+
// Where a step's first seconds go, phase by phase (FOL-29): each phase's
|
|
8214
|
+
// own share, and when it was done counted from the step's start.
|
|
8215
|
+
table("start-up", ["start-up", "n", "took p50", "p95", "done by p50", "p95"], (o.startup ?? []).map((p) => [p.phase, p.took?.n ?? 0, fine(p.took?.p50), fine(p.took?.p95), fine(p.at?.p50), fine(p.at?.p95)]));
|
|
8185
8216
|
const fails = (o.failures ?? []).slice(0, 8);
|
|
8186
8217
|
if (fails.length) {
|
|
8187
8218
|
console.log(`\n ${c.dim("recent failures")}`);
|
|
@@ -8704,85 +8735,6 @@ async function historyCmd(positional, flags, layout) {
|
|
|
8704
8735
|
return 0;
|
|
8705
8736
|
}
|
|
8706
8737
|
|
|
8707
|
-
/**
|
|
8708
|
-
* `foldrun repo ls | diff <branch> | deploy <ref> | merge <branch>` — the
|
|
8709
|
-
* Repository page: branches, what one changes, deploy any commit (a rollback
|
|
8710
|
-
* is a new commit on main), merge a branch into main and deploy it.
|
|
8711
|
-
*/
|
|
8712
|
-
async function repoCmd(positional, flags, layout) {
|
|
8713
|
-
const url = platformFor(flags, "repo");
|
|
8714
|
-
const ws = platformWorkspace(flags, layout, "repo");
|
|
8715
|
-
const [verb = "ls", arg] = positional;
|
|
8716
|
-
const base = `/api/workspaces/${enc(ws)}/repo`;
|
|
8717
|
-
if (verb === "ls" || verb === "list") {
|
|
8718
|
-
const r = await remoteCall(url, flags, base);
|
|
8719
|
-
console.log(`\n ${c.bold(ws)} ${c.dim(`main ${r.main ? String(r.main).slice(0, 7) : "— nothing committed yet"}`)}`);
|
|
8720
|
-
const refs = (title, list) => {
|
|
8721
|
-
if (!list?.length) return;
|
|
8722
|
-
console.log(`\n ${c.dim(title)}`);
|
|
8723
|
-
const w = Math.max(...list.map((b) => b.name.length));
|
|
8724
|
-
for (const b of list) console.log(` ${b.name === "main" ? c.green("●") : c.dim("○")} ${c.bold(pad(b.name, w))} ${c.dim(String(b.sha).slice(0, 7))} ${firstLine(b.subject, 60)} ${c.dim(b.at ? `${ago(b.at)} ago` : "")}`);
|
|
8725
|
-
};
|
|
8726
|
-
refs("branches", r.branches);
|
|
8727
|
-
refs("tags", r.tags);
|
|
8728
|
-
if (r.settings?.mirror) console.log(`\n ${c.dim(`mirror ${r.settings.mirror}${r.mirror?.at ? ` · last push ${ago(r.mirror.at)} ago${r.mirror.ok === false ? " (failed)" : ""}` : ""}`)}`);
|
|
8729
|
-
console.log(`\n ${c.dim(`foldrun repo diff <branch> · repo merge <branch> · repo deploy <ref> --to ${ws}`)}\n`);
|
|
8730
|
-
return 0;
|
|
8731
|
-
}
|
|
8732
|
-
if (verb === "diff") {
|
|
8733
|
-
if (!arg) throw new Error("`foldrun repo diff <branch> --to <workspace>` — what it changes against main");
|
|
8734
|
-
const r = await remoteCall(url, flags, `${base}?branch=${enc(arg)}`);
|
|
8735
|
-
if (!r.files?.length) console.log(`\n ${c.dim(`${arg} changes nothing against main`)}\n`);
|
|
8736
|
-
console.log();
|
|
8737
|
-
for (const f of r.files ?? []) {
|
|
8738
|
-
printOps(f.path, f.diff ?? []);
|
|
8739
|
-
console.log();
|
|
8740
|
-
}
|
|
8741
|
-
return 0;
|
|
8742
|
-
}
|
|
8743
|
-
if (verb === "rm-branch") {
|
|
8744
|
-
if (!arg || arg === "main") throw new Error("`foldrun repo rm-branch <branch> --to <workspace>` — any branch but main");
|
|
8745
|
-
if (!(await sureToDelete(flags, "repo rm-branch", `branch ${arg} in ${ws}`))) return 1;
|
|
8746
|
-
await remoteCall(url, flags, base, { method: "POST", body: JSON.stringify({ action: "delete-branch", branch: arg }) });
|
|
8747
|
-
console.log(`\n ${c.green("✓")} branch ${arg} deleted ${c.dim(`in ${ws}`)}\n`);
|
|
8748
|
-
return 0;
|
|
8749
|
-
}
|
|
8750
|
-
if (verb === "mirror") {
|
|
8751
|
-
if (!arg) throw new Error("`foldrun repo mirror <git-url> --to <workspace>` (every push to main goes there too, with the workspace's MIRROR_TOKEN) or `repo mirror off`");
|
|
8752
|
-
const r = await remoteCall(url, flags, base, { method: "POST", body: JSON.stringify({ action: "mirror", url: arg === "off" ? null : arg }) });
|
|
8753
|
-
console.log(`\n ${c.green("✓")} ${r.settings?.mirror ? `main is mirrored to ${r.settings.mirror}` : "no mirror"} ${c.dim(`· ${ws}${r.settings?.mirror ? " · `foldrun repo mirror-now` pushes it now" : ""}`)}\n`);
|
|
8754
|
-
return 0;
|
|
8755
|
-
}
|
|
8756
|
-
if (verb === "mirror-now") {
|
|
8757
|
-
const r = await remoteCall(url, flags, base, { method: "POST", body: JSON.stringify({ action: "mirror-now" }) });
|
|
8758
|
-
const st = r.status;
|
|
8759
|
-
if (!st) {
|
|
8760
|
-
console.log(`\n ${c.dim(`${ws} has no mirror — \`foldrun repo mirror <git-url> --to ${ws}\` sets one`)}\n`);
|
|
8761
|
-
return 1;
|
|
8762
|
-
}
|
|
8763
|
-
console.log(`\n ${st.ok ? c.green("✓") : c.red("✗")} mirror ${st.ok ? `has ${String(st.sha ?? "").slice(0, 7)}` : "push failed"}${st.detail ? ` — ${st.detail}` : ""} ${c.dim(ws)}\n`);
|
|
8764
|
-
return st.ok ? 0 : 1;
|
|
8765
|
-
}
|
|
8766
|
-
if (verb !== "deploy" && verb !== "merge") throw new Error(`unknown repo verb "${verb}" — ls, diff <branch>, deploy <ref>, merge <branch>, rm-branch <branch>, mirror <url|off>, mirror-now`);
|
|
8767
|
-
if (!arg) throw new Error(verb === "deploy" ? "`foldrun repo deploy <commit|branch|tag> --to <workspace>`" : "`foldrun repo merge <branch> --to <workspace>`");
|
|
8768
|
-
if (verb === "merge" && arg === "main") throw new Error("main is what a branch merges into — name the branch");
|
|
8769
|
-
console.log(
|
|
8770
|
-
`\n ${c.yellow("!")} ${verb === "deploy" ? `deploying ${c.bold(arg)} makes it what is live in ${ws} — a new commit on main, which can itself be undone` : `merging ${c.bold(arg)} into main deploys the result to ${ws}`}`,
|
|
8771
|
-
);
|
|
8772
|
-
if (!(await confirmed(flags, `repo ${verb}`, `${verb === "deploy" ? "Deploy" : "Merge"} it? [y/N] `))) {
|
|
8773
|
-
console.log(`\n ${c.dim(`nothing ${verb === "deploy" ? "deployed" : "merged"}`)}\n`);
|
|
8774
|
-
return 1;
|
|
8775
|
-
}
|
|
8776
|
-
const r = await remoteCall(url, flags, base, { method: "POST", body: JSON.stringify(verb === "deploy" ? { action: "deploy", ref: arg } : { action: "merge", branch: arg }) });
|
|
8777
|
-
if (r.applied) {
|
|
8778
|
-
console.log(`\n ${c.green("✓")} ${verb === "deploy" ? `${arg} is live` : `${arg} merged and live`} ${c.dim(`in ${ws} · main ${String(r.main ?? r.sha ?? "").slice(0, 7)}`)}\n`);
|
|
8779
|
-
return 0;
|
|
8780
|
-
}
|
|
8781
|
-
console.log(`\n ${c.red("✗")} not deployed${r.blockedBy ? ` — a run is in flight (${Array.isArray(r.blockedBy) ? r.blockedBy.join(", ") : r.blockedBy})` : ""}`);
|
|
8782
|
-
for (const i of r.issues ?? []) console.log(` ${c.red("error")} ${c.bold(i.where)} ${i.message}`);
|
|
8783
|
-
console.log();
|
|
8784
|
-
return 1;
|
|
8785
|
-
}
|
|
8786
8738
|
|
|
8787
8739
|
/**
|
|
8788
8740
|
* `foldrun onboarding` — the account's getting-started steps, each worked
|
|
@@ -8877,7 +8829,7 @@ async function backupsCmd(positional, flags) {
|
|
|
8877
8829
|
}
|
|
8878
8830
|
}
|
|
8879
8831
|
if (b.workspaces?.length) {
|
|
8880
|
-
console.log(`\n ${c.dim("restore a workspace's source yourself — foldrun restore <workspace> --to <
|
|
8832
|
+
console.log(`\n ${c.dim("restore a workspace's source yourself — foldrun restore <workspace> --to <revision|time|3d> --dry-run")}`);
|
|
8881
8833
|
const w = Math.max(...b.workspaces.map((x) => x.name.length));
|
|
8882
8834
|
for (const x of b.workspaces) {
|
|
8883
8835
|
console.log(` ${pad(x.name, w)} ${x.lastChange ? c.dim(`last change ${when(x.lastChange.at)} — ${firstLine(x.lastChange.message, 50)}`) : c.dim("no history yet")}`);
|
|
@@ -8891,8 +8843,12 @@ async function backupsCmd(positional, flags) {
|
|
|
8891
8843
|
return 0;
|
|
8892
8844
|
}
|
|
8893
8845
|
|
|
8846
|
+
/** A revision as the CLI prints it: a git sha shortened as git does, a
|
|
8847
|
+
* platform revision id (r-…) whole — it is what --to takes back. */
|
|
8848
|
+
const revShort = (id) => (/^[0-9a-f]{40}$/.test(String(id)) ? String(id).slice(0, 7) : String(id));
|
|
8849
|
+
|
|
8894
8850
|
/**
|
|
8895
|
-
* `foldrun restore <workspace> --to <
|
|
8851
|
+
* `foldrun restore <workspace> --to <revision|time|age> [--dry-run]`
|
|
8896
8852
|
* — the workspace's source as it was then. Always shows the change first;
|
|
8897
8853
|
* the restore asks for the workspace's name typed back (or --yes), and
|
|
8898
8854
|
* sends the removals it showed, so a file added in between refuses it.
|
|
@@ -8900,11 +8856,11 @@ async function backupsCmd(positional, flags) {
|
|
|
8900
8856
|
async function restoreCmd(positional, flags) {
|
|
8901
8857
|
const url = platformFor(flags, "restore");
|
|
8902
8858
|
const ws = positional[0];
|
|
8903
|
-
if (!ws) throw new Error("`foldrun restore <workspace> --to <
|
|
8904
|
-
if (typeof flags.to !== "string" || !flags.to.trim()) throw new Error("--to <
|
|
8859
|
+
if (!ws) throw new Error("`foldrun restore <workspace> --to <revision|time|3d> [--dry-run]`");
|
|
8860
|
+
if (typeof flags.to !== "string" || !flags.to.trim()) throw new Error("--to <revision, date or age> — the point to restore to, e.g. --to 2026-09-28T14:00 or --to 3d");
|
|
8905
8861
|
const base = `/api/workspaces/${enc(ws)}/restore`;
|
|
8906
8862
|
const plan = await remoteCall(url, flags, base, { method: "POST", body: JSON.stringify({ to: flags.to, dryRun: true }) });
|
|
8907
|
-
console.log(`\n ${c.bold(ws)} → ${c.bold(
|
|
8863
|
+
console.log(`\n ${c.bold(ws)} → ${c.bold(revShort(plan.to.id ?? plan.to.sha))} ${c.dim(`${when(plan.to.at)} · ${plan.to.by} · ${firstLine(plan.to.message, 60)}`)}`);
|
|
8908
8864
|
if (plan.noop) {
|
|
8909
8865
|
console.log(` ${c.dim(`${ws} already matches that point — nothing to restore`)}\n`);
|
|
8910
8866
|
return 0;
|
|
@@ -8930,13 +8886,13 @@ async function restoreCmd(positional, flags) {
|
|
|
8930
8886
|
}
|
|
8931
8887
|
const r = await remoteCall(url, flags, base, {
|
|
8932
8888
|
method: "POST",
|
|
8933
|
-
body: JSON.stringify({ to: plan.to.sha, confirm: ws, expectRemoved: plan.removed }),
|
|
8889
|
+
body: JSON.stringify({ to: plan.to.id ?? plan.to.sha, confirm: ws, expectRemoved: plan.removed }),
|
|
8934
8890
|
});
|
|
8935
8891
|
if (r.noop) {
|
|
8936
|
-
console.log(`\n ${c.dim(`${ws} already matches ${
|
|
8892
|
+
console.log(`\n ${c.dim(`${ws} already matches ${revShort(plan.to.id ?? plan.to.sha)}`)}\n`);
|
|
8937
8893
|
return 0;
|
|
8938
8894
|
}
|
|
8939
|
-
console.log(`\n ${c.green("✓")} ${ws} restored to ${
|
|
8895
|
+
console.log(`\n ${c.green("✓")} ${ws} restored to ${revShort(r.to.id ?? r.to.sha)} ${c.dim(`· main ${revShort(r.main ?? "")} · undo: foldrun restore ${ws} --to ${String(r.from ?? "")}`)}\n`);
|
|
8940
8896
|
return 0;
|
|
8941
8897
|
}
|
|
8942
8898
|
|
|
@@ -10413,7 +10369,8 @@ export async function run(command, positional, flags, workspace, layout) {
|
|
|
10413
10369
|
if (positional[0] === "restore") return historyRestoreCmd(positional, flags, layout);
|
|
10414
10370
|
return historyCmd(positional, flags, layout);
|
|
10415
10371
|
case "repo":
|
|
10416
|
-
|
|
10372
|
+
// The platform's own git went on 17 Oct 2026.
|
|
10373
|
+
throw new Error("`foldrun repo` is gone with the platform's own git — `foldrun history` lists a workspace's revisions and `foldrun restore <ws> --to <revision|time|age>` rolls back");
|
|
10417
10374
|
case "restore":
|
|
10418
10375
|
return restoreCmd(positional, flags);
|
|
10419
10376
|
case "backups":
|