foldrun 2.6.0 → 2.8.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/LICENSE +95 -29
- package/LICENSING.md +101 -0
- package/README.md +9 -4
- package/bin/foldrun.mjs +18 -1
- package/docs/agents.md +11 -5
- package/docs/api-usage.md +8 -1
- package/docs/api.md +12 -4
- package/docs/cli.md +17 -3
- package/docs/evals.md +151 -1
- package/docs/flows.md +63 -2
- package/docs/hooks.md +2 -2
- package/docs/introduction.md +6 -4
- package/docs/network.md +6 -2
- package/docs/runs.md +28 -0
- package/docs/self-hosting.md +48 -10
- package/docs/tools.md +1 -1
- package/docs/versions.md +2 -2
- package/package.json +8 -4
- package/src/commands.mjs +488 -19
- package/src/guide.mjs +1 -1
package/src/commands.mjs
CHANGED
|
@@ -1889,13 +1889,23 @@ async function probeCmd(modelArg) {
|
|
|
1889
1889
|
async function runEvals(name, flags = {}) {
|
|
1890
1890
|
assertCredentials();
|
|
1891
1891
|
await useGallery(flags);
|
|
1892
|
-
const { listEvals, runEval } = await core();
|
|
1892
|
+
const { listEvals, runEval, workspaceDir } = await core();
|
|
1893
1893
|
const T = "default";
|
|
1894
1894
|
const P = "workspace";
|
|
1895
|
+
// Where this workspace's evals are, for telling a grader by its file when
|
|
1896
|
+
// the installed core predates graders — the workspace chosen, not the cwd.
|
|
1897
|
+
const evalsDir = path.join(workspaceDir(T, P), "evals");
|
|
1895
1898
|
// An `inputs: true` file is a flow's saved inputs, not a test: it has
|
|
1896
1899
|
// nothing to assert, so it is never run as one (see evals.md).
|
|
1897
|
-
const
|
|
1900
|
+
const named = listEvals(T, P).filter((e) => (!name || e.name === name) && !e.inputs);
|
|
1901
|
+
// A grader (`trigger: runs`) grades real runs; it has no cases to run here.
|
|
1902
|
+
const all = named.filter((e) => !isGrader(e, evalsDir));
|
|
1903
|
+
for (const g of named.filter((e) => isGrader(e, evalsDir))) {
|
|
1904
|
+
console.log(`\n ${c.bold(g.name)} ${c.dim(`grades live runs — \`foldrun eval grades ${g.name} --to <workspace>\` shows what it found`)}`);
|
|
1905
|
+
}
|
|
1898
1906
|
if (all.length === 0) {
|
|
1907
|
+
// Only graders here (or the one named is a grader): listed above, nothing to run — not an error.
|
|
1908
|
+
if (named.length) return 0;
|
|
1899
1909
|
if (name && listEvals(T, P).some((e) => e.name === name)) {
|
|
1900
1910
|
throw new Error(`"${name}" is a file of saved inputs, not an eval — \`foldrun invoke <flow> --inputs <set>\` runs one`);
|
|
1901
1911
|
}
|
|
@@ -5581,16 +5591,12 @@ async function billingCmd(flags) {
|
|
|
5581
5591
|
const entries = body.entries ?? [];
|
|
5582
5592
|
|
|
5583
5593
|
console.log(`\n ${balance > 0 ? c.green("✓") : c.red("✗")} balance ${c.bold(`$${balance.toFixed(2)}`)} ${c.dim(body.enabled ? "billing on" : "billing off — nothing is charged on this install")}`);
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
// absorbed, and that is the state people mistake for "it is working".
|
|
5591
|
-
const waived = [...new Set(entries.flatMap((e) => e.waived ?? []))];
|
|
5592
|
-
if (waived.length) {
|
|
5593
|
-
console.log(` ${c.amber("⏸")} ${c.dim(`${waived.join(", ")} ${waived.length === 1 ? "is" : "are"} being waived on recent runs — those lines are charged $0`)}`);
|
|
5594
|
+
// What the account should be told — an empty wallet, an overdraft, a
|
|
5595
|
+
// waiver — as the platform words it: it knows the gate, the overdraft and
|
|
5596
|
+
// the waivers; the numbers here do not say enough to guess.
|
|
5597
|
+
for (const notice of Array.isArray(body.notices) ? body.notices : []) {
|
|
5598
|
+
const empty = /wallet is empty/.test(notice);
|
|
5599
|
+
console.log(` ${empty ? c.red("✗") : c.amber("⏸")} ${empty ? notice : c.dim(notice)}`);
|
|
5594
5600
|
}
|
|
5595
5601
|
|
|
5596
5602
|
const limit = Number(flags.limit) > 0 ? Math.floor(Number(flags.limit)) : 15;
|
|
@@ -6324,6 +6330,57 @@ function issueDoctor(issue) {
|
|
|
6324
6330
|
* on one — the dashboard's Issues page and its buttons, same route.
|
|
6325
6331
|
* `foldrun issues doctor <id>` is the card's Doctor: the run doctor on the
|
|
6326
6332
|
* issue's newest run, in that run's workspace. */
|
|
6333
|
+
/**
|
|
6334
|
+
* `foldrun inbox` — what is waiting on you across your workspaces (GET
|
|
6335
|
+
* /api/me/inbox): gates, questions agents ask, open issues, proposed fixes,
|
|
6336
|
+
* your runs that went wrong this week. Assigned to you and started by you by
|
|
6337
|
+
* default; `--team` for the rest of what waits in your workspaces, `--all`
|
|
6338
|
+
* for every bucket, `--dismissed` for what you set aside, `--kind
|
|
6339
|
+
* approval|question|issue|proposal|run`, `--unread`, `--json`. `read`,
|
|
6340
|
+
* `unread`, `dismiss` (set aside — it still waits where it lives) and
|
|
6341
|
+
* `restore` take item ids. A key reads the inbox of the person who minted it.
|
|
6342
|
+
* The dashboard's Inbox. Exit 1 when anything assigned to you is unread.
|
|
6343
|
+
*/
|
|
6344
|
+
async function inboxCmd(positional, flags) {
|
|
6345
|
+
const url = platformFor(flags, "inbox");
|
|
6346
|
+
const [verb, ...ids] = positional;
|
|
6347
|
+
const ACTIONS = { read: "read", unread: "unread", dismiss: "dismiss", restore: "restore" };
|
|
6348
|
+
if (verb) {
|
|
6349
|
+
const action = ACTIONS[verb];
|
|
6350
|
+
if (!action) throw new Error(`\`foldrun inbox\` lists what waits on you; its verbs are read, unread, dismiss and restore — not ${verb}`);
|
|
6351
|
+
if (!ids.length) throw new Error(`\`foldrun inbox ${verb} <id>…\` — the ids are the first column of \`foldrun inbox\``);
|
|
6352
|
+
await remoteCall(url, flags, "/api/me/inbox", { method: "POST", body: JSON.stringify({ ids, action }) });
|
|
6353
|
+
console.log(`\n ${c.green(action === "dismiss" ? "set aside" : action === "restore" ? "put back" : `marked ${action}`)} ${ids.length} item${ids.length === 1 ? "" : "s"}\n`);
|
|
6354
|
+
return 0;
|
|
6355
|
+
}
|
|
6356
|
+
const q = new URLSearchParams();
|
|
6357
|
+
if (flags.all === true) q.set("bucket", "all");
|
|
6358
|
+
else if (flags.team === true) q.set("bucket", "team");
|
|
6359
|
+
else if (flags.started === true) q.set("bucket", "started");
|
|
6360
|
+
else if (flags.mine === true) q.set("bucket", "mine");
|
|
6361
|
+
if (flags.dismissed === true) q.set("dismissed", "1");
|
|
6362
|
+
if (typeof flags.kind === "string") q.set("kind", flags.kind);
|
|
6363
|
+
if (typeof flags.workspace === "string") q.set("workspace", flags.workspace);
|
|
6364
|
+
const view = await remoteCall(url, flags, `/api/me/inbox${q.size ? `?${q}` : ""}`);
|
|
6365
|
+
const items = (view.items ?? []).filter((i) => (flags.unread === true ? !i.read : true));
|
|
6366
|
+
if (flags.json) {
|
|
6367
|
+
console.log(JSON.stringify({ ...view, items }, null, 2));
|
|
6368
|
+
return view.counts?.unread ? 1 : 0;
|
|
6369
|
+
}
|
|
6370
|
+
const k = view.counts ?? {};
|
|
6371
|
+
console.log(`\n ${c.bold(`inbox of ${view.person}`)} ${c.dim(`· for you ${k.mine ?? 0} · started by you ${k.started ?? 0} · team ${k.team ?? 0} · ${k.unread ?? 0} unread`)}`);
|
|
6372
|
+
if (!items.length) console.log(`\n ${c.dim(flags.dismissed ? "nothing set aside" : "nothing waiting")}`);
|
|
6373
|
+
const BUCKET = { mine: "for you", started: "yours", team: "team" };
|
|
6374
|
+
for (const i of items) {
|
|
6375
|
+
const dot = i.read ? " " : c.amber("•");
|
|
6376
|
+
console.log(`\n ${dot} ${c.bold(i.title)} ${c.dim(`· ${i.kind} · ${BUCKET[i.bucket] ?? i.bucket} · ${i.workspace} · ${String(i.at).slice(0, 16).replace("T", " ")}`)}`);
|
|
6377
|
+
if (i.detail) console.log(` ${i.detail}`);
|
|
6378
|
+
console.log(` ${c.dim(`${i.id} — ${i.why}`)}`);
|
|
6379
|
+
}
|
|
6380
|
+
console.log("");
|
|
6381
|
+
return k.unread ? 1 : 0;
|
|
6382
|
+
}
|
|
6383
|
+
|
|
6327
6384
|
async function issuesCmd(positional, flags, layout) {
|
|
6328
6385
|
const url = platformFor(flags, "issues");
|
|
6329
6386
|
const [verb, id] = positional;
|
|
@@ -8189,14 +8246,22 @@ function ownSandboxOf(agent) {
|
|
|
8189
8246
|
return /** @type {{ sandbox?: string } | undefined} */ (agent)?.sandbox === "own";
|
|
8190
8247
|
}
|
|
8191
8248
|
|
|
8192
|
-
/** An agent file's frontmatter as a run reads it
|
|
8193
|
-
*
|
|
8249
|
+
/** An agent file's frontmatter as a run reads it, from its text. A run folds
|
|
8250
|
+
* the four job blocks' keys up to the top (core's configMatter, kept private
|
|
8251
|
+
* — not in @foldrun/core's publicApi), so `guardrail: { sandbox: own }` reads
|
|
8252
|
+
* as `sandbox: own`. The CLI reads only two foldable keys off this — `sandbox`
|
|
8253
|
+
* and `network`, both in an agent's `guardrail:` block — so it lifts just
|
|
8254
|
+
* those, the same way a run does: a top-level value wins, else the block's is
|
|
8255
|
+
* lifted. {} when it does not parse. */
|
|
8194
8256
|
async function effectiveFront(raw) {
|
|
8195
8257
|
const { readFrontmatter } = await core();
|
|
8196
8258
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "foldrun-front-"));
|
|
8197
8259
|
try {
|
|
8198
8260
|
fs.writeFileSync(path.join(dir, "agent.md"), raw);
|
|
8199
|
-
|
|
8261
|
+
const front = /** @type {Record<string, any>} */ (readFrontmatter(path.join(dir, "agent.md")) ?? {});
|
|
8262
|
+
const guardrail = front.guardrail && typeof front.guardrail === "object" ? front.guardrail : {};
|
|
8263
|
+
for (const key of ["sandbox", "network"]) if (!(key in front) && key in guardrail) front[key] = guardrail[key];
|
|
8264
|
+
return front;
|
|
8200
8265
|
} catch {
|
|
8201
8266
|
return {};
|
|
8202
8267
|
} finally {
|
|
@@ -8443,10 +8508,12 @@ async function agentNetworkCmd(positional, flags, layout) {
|
|
|
8443
8508
|
}
|
|
8444
8509
|
const resolve = (files) =>
|
|
8445
8510
|
inTree(desk, files, async (dir) => {
|
|
8446
|
-
const { listAgents
|
|
8511
|
+
const { listAgents } = await core();
|
|
8512
|
+
// The folded view a run reads: `network` set inside a `guardrail:` block
|
|
8513
|
+
// counts the same as a top-level one (effectiveFront lifts it).
|
|
8447
8514
|
return {
|
|
8448
8515
|
info: listAgents("default", "workspace").find((a) => a.name === name),
|
|
8449
|
-
own: net.readNetwork(
|
|
8516
|
+
own: net.readNetwork((await effectiveFront(fs.readFileSync(path.join(dir, rel), "utf8"))).network).setting,
|
|
8450
8517
|
};
|
|
8451
8518
|
});
|
|
8452
8519
|
const show = (info) => {
|
|
@@ -8717,10 +8784,16 @@ async function remoteEvalsCmd(name, flags, layout) {
|
|
|
8717
8784
|
if (!name) throw new Error("--run reads one eval's run — name it: `foldrun eval <name> --to <ws> --run <id>`");
|
|
8718
8785
|
}
|
|
8719
8786
|
const { evals = [] } = await remoteCall(url, flags, `/api/workspaces/${enc(ws)}/evals`);
|
|
8720
|
-
const
|
|
8721
|
-
if (!
|
|
8787
|
+
const named = evals.filter((e) => !name || e.name === name);
|
|
8788
|
+
if (!named.length) {
|
|
8722
8789
|
throw new Error(name ? `no eval "${name}" in ${ws} — it has ${evals.map((e) => e.name).join(", ") || "none"}` : `no evals in ${ws} on ${url}`);
|
|
8723
8790
|
}
|
|
8791
|
+
// Graders grade real runs; `eval grades` shows what they found.
|
|
8792
|
+
const all = named.filter((e) => e.trigger !== "runs");
|
|
8793
|
+
for (const g of named.filter((e) => e.trigger === "runs")) {
|
|
8794
|
+
console.log(`\n ${c.bold(g.name)} ${c.dim(`grades live runs — \`foldrun eval grades ${g.name} --to ${ws}\``)}`);
|
|
8795
|
+
}
|
|
8796
|
+
if (!all.length) return 0;
|
|
8724
8797
|
let failed = 0;
|
|
8725
8798
|
for (const info of all) {
|
|
8726
8799
|
console.log(`\n ${c.bold(info.name)} ${c.dim(`${info.cases?.length ?? "?"} cases · ${ws}`)}`);
|
|
@@ -8761,6 +8834,341 @@ async function remoteEvalsCmd(name, flags, layout) {
|
|
|
8761
8834
|
return failed ? 1 : 0;
|
|
8762
8835
|
}
|
|
8763
8836
|
|
|
8837
|
+
/** Is this eval a grader (`trigger: runs`)? A core that predates graders
|
|
8838
|
+
* reads the trigger as `deploy`, so the file is asked too. */
|
|
8839
|
+
function isGrader(info, dir) {
|
|
8840
|
+
if (info.trigger === "runs") return true;
|
|
8841
|
+
try {
|
|
8842
|
+
const raw = fs.readFileSync(path.join(dir, info.file), "utf8");
|
|
8843
|
+
const front = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw)?.[1] ?? "";
|
|
8844
|
+
return /^trigger:\s*["']?runs["']?\s*$/m.test(front);
|
|
8845
|
+
} catch {
|
|
8846
|
+
return false;
|
|
8847
|
+
}
|
|
8848
|
+
}
|
|
8849
|
+
|
|
8850
|
+
/** One grade, the way `eval grades` and `eval grade` print it. */
|
|
8851
|
+
function printGrade(g) {
|
|
8852
|
+
const mark = g.status === "pass" ? c.green("✓ pass") : g.status === "fail" ? c.red("✗ fail") : c.amber("? not decided");
|
|
8853
|
+
const miss = (g.checks ?? []).find((x) => !x.passed);
|
|
8854
|
+
const why = miss ? ` — ${miss.name}: ${miss.error ?? (miss.assertions ?? []).find((a) => !a.passed)?.detail ?? "failed"}` : "";
|
|
8855
|
+
console.log(` ${mark} ${c.bold(g.runId)} ${c.dim(String(g.at ?? "").slice(0, 16).replace("T", " "))}${why}`);
|
|
8856
|
+
}
|
|
8857
|
+
|
|
8858
|
+
/**
|
|
8859
|
+
* `foldrun eval grades <eval> --to <workspace>` — what a grader (an eval with
|
|
8860
|
+
* `trigger: runs`) found in the real runs it graded: the pass rate over
|
|
8861
|
+
* `--days` (default 30), then each graded run, newest first (`--status
|
|
8862
|
+
* fail` for the failures alone, `--limit`). GET …/evals/<e>/grades — the
|
|
8863
|
+
* Evals page's grader card. Exit 1 when any graded run in the window failed.
|
|
8864
|
+
*/
|
|
8865
|
+
async function evalGradesCmd(name, flags, layout) {
|
|
8866
|
+
if (!name) throw new Error("which grader? `foldrun eval grades <eval> --to <workspace>`");
|
|
8867
|
+
const url = platformFor(flags, "eval grades");
|
|
8868
|
+
const ws = platformWorkspace(flags, layout, `eval grades ${name}`);
|
|
8869
|
+
const q = new URLSearchParams();
|
|
8870
|
+
if (flags.days !== undefined) q.set("days", String(flags.days));
|
|
8871
|
+
if (typeof flags.status === "string") q.set("status", flags.status);
|
|
8872
|
+
if (flags.limit !== undefined) q.set("limit", String(flags.limit));
|
|
8873
|
+
const r = await remoteCall(url, flags, `/api/workspaces/${enc(ws)}/evals/${enc(name)}/grades${q.size ? `?${q}` : ""}`);
|
|
8874
|
+
if (flags.json) {
|
|
8875
|
+
console.log(JSON.stringify(r, null, 2));
|
|
8876
|
+
return r.summary?.failed ? 1 : 0;
|
|
8877
|
+
}
|
|
8878
|
+
const s = r.summary ?? {};
|
|
8879
|
+
const rate = s.rate === null || s.rate === undefined ? "—" : `${Math.round(s.rate * 100)}%`;
|
|
8880
|
+
console.log(`\n ${c.bold(r.eval)} ${c.dim(`grades ${r.sample}% of ${r.flow ? `flow ${r.flow}` : `agent ${r.agent}`} runs · ${ws}`)}`);
|
|
8881
|
+
console.log(` ${rate} passing over ${r.days} days — ${s.graded ?? 0} graded, ${s.passed ?? 0} passed, ${s.failed ?? 0} failed${s.errors ? `, ${s.errors} not decided` : ""} · judges $${Number(s.costUsd ?? 0).toFixed(4)}\n`);
|
|
8882
|
+
for (const g of r.grades ?? []) printGrade(g);
|
|
8883
|
+
if (!(r.grades ?? []).length) console.log(` ${c.dim("no graded runs in the window")}`);
|
|
8884
|
+
console.log("");
|
|
8885
|
+
return s.failed ? 1 : 0;
|
|
8886
|
+
}
|
|
8887
|
+
|
|
8888
|
+
/**
|
|
8889
|
+
* `foldrun eval grade <eval> --run <id> --to <workspace>` — grade one finished
|
|
8890
|
+
* run now, sampled or not (POST …/evals/<e>/grade { run }). The grade is kept
|
|
8891
|
+
* like any other; a run graded again shows its newer grade. Exit 1 unless it
|
|
8892
|
+
* passed.
|
|
8893
|
+
*/
|
|
8894
|
+
async function evalGradeCmd(name, flags, layout) {
|
|
8895
|
+
if (!name || typeof flags.run !== "string" || !flags.run.trim()) {
|
|
8896
|
+
throw new Error("`foldrun eval grade <eval> --run <run-id> --to <workspace>`");
|
|
8897
|
+
}
|
|
8898
|
+
const url = platformFor(flags, "eval grade");
|
|
8899
|
+
const ws = platformWorkspace(flags, layout, `eval grade ${name}`);
|
|
8900
|
+
const g = await remoteCall(url, flags, `/api/workspaces/${enc(ws)}/evals/${enc(name)}/grade`, { method: "POST", body: JSON.stringify({ run: flags.run.trim() }) });
|
|
8901
|
+
if (flags.json) console.log(JSON.stringify(g, null, 2));
|
|
8902
|
+
else {
|
|
8903
|
+
console.log("");
|
|
8904
|
+
printGrade(g);
|
|
8905
|
+
for (const ch of g.checks ?? []) {
|
|
8906
|
+
console.log(` ${ch.passed ? c.green("✓") : ch.error ? c.amber("?") : c.red("✗")} ${ch.name}${ch.error ? c.dim(` — ${ch.error}`) : ""}`);
|
|
8907
|
+
for (const a of ch.assertions ?? []) if (!a.passed) console.log(` ${c.dim(`${a.type}: ${a.value}`)} — ${a.detail}`);
|
|
8908
|
+
}
|
|
8909
|
+
console.log(` ${c.dim(`judges $${Number(g.costUsd ?? 0).toFixed(4)}`)}\n`);
|
|
8910
|
+
}
|
|
8911
|
+
return g.status === "pass" ? 0 : 1;
|
|
8912
|
+
}
|
|
8913
|
+
|
|
8914
|
+
/** One comparison, as a table on a terminal. */
|
|
8915
|
+
function printComparison(r) {
|
|
8916
|
+
const pct = (x) => (x === null || x === undefined ? "—" : `${Math.round(x * 100)}%`);
|
|
8917
|
+
const w = Math.max(...(r.configs ?? []).map((x) => String(x.label).length), 7);
|
|
8918
|
+
console.log(`\n ${c.bold(r.eval)} ${c.dim(`judge ${r.judge?.model ?? "?"} · spent $${Number(r.spentUsd ?? 0).toFixed(2)} of $${Number(r.maxUsd ?? 0).toFixed(2)}${r.repeat > 1 ? ` · ${r.repeat}× each` : ""}`)}`);
|
|
8919
|
+
if (r.stopped) console.log(` ${c.amber(`stopped early: ${r.stopped}`)}`);
|
|
8920
|
+
console.log(`\n ${c.dim(`${"setting".padEnd(w)} passing cost per run time`)}`);
|
|
8921
|
+
for (const x of r.configs ?? []) {
|
|
8922
|
+
const decided = x.trials - (x.notRun ?? 0);
|
|
8923
|
+
console.log(` ${String(x.label).padEnd(w)} ${pct(x.rate).padStart(4)} ${`${x.passed}/${decided}`.padEnd(7)} ${`$${Number(x.costUsd).toFixed(4)}`.padStart(9)} ${x.avgCostUsd === null ? " —" : `$${Number(x.avgCostUsd).toFixed(4)}`.padStart(8)} ${x.avgSecs === null ? "—" : `${x.avgSecs}s`}${x.notRun ? c.dim(` (${x.notRun} not run)`) : ""}`);
|
|
8924
|
+
}
|
|
8925
|
+
const differ = (r.cases ?? []).filter((row) => new Set(row.passed.map((p, i) => p / (row.trials[i] || 1))).size > 1);
|
|
8926
|
+
if (differ.length) {
|
|
8927
|
+
console.log(`\n ${c.dim("where they differ:")}`);
|
|
8928
|
+
for (const row of differ) console.log(` ${row.case}: ${row.passed.map((p, i) => `${r.configs[i].label} ${p}/${row.trials[i]}`).join(" · ")}`);
|
|
8929
|
+
}
|
|
8930
|
+
console.log("");
|
|
8931
|
+
}
|
|
8932
|
+
|
|
8933
|
+
/**
|
|
8934
|
+
* `foldrun eval compare <eval> --models fast,default,max --to <workspace>` —
|
|
8935
|
+
* an eval's cases under two to four models, side by side (POST
|
|
8936
|
+
* …/evals/<e>/compare, then polled). `--models` takes model[:effort] items
|
|
8937
|
+
* (`fast:low,max:high`); `--effort` is the effort for the items that name
|
|
8938
|
+
* none; `--agent <name>` compares only that agent's steps of a flow eval;
|
|
8939
|
+
* `--repeat` (1–5) runs each case more than once; `--max-usd` is the spend
|
|
8940
|
+
* limit (default 5). Every run is a test run. Waits for the result
|
|
8941
|
+
* (`--timeout <s>`, default 3600; past it, the id to read it with).
|
|
8942
|
+
* `--run <id>` reads one comparison; `--list` the kept ones. The Evals
|
|
8943
|
+
* page's Compare models.
|
|
8944
|
+
*/
|
|
8945
|
+
async function evalCompareCmd(name, flags, layout) {
|
|
8946
|
+
if (!name) throw new Error("which eval? `foldrun eval compare <eval> --models fast,default --to <workspace>`");
|
|
8947
|
+
const url = platformFor(flags, "eval compare");
|
|
8948
|
+
const ws = platformWorkspace(flags, layout, `eval compare ${name}`);
|
|
8949
|
+
const base = `/api/workspaces/${enc(ws)}/evals/${enc(name)}/compare`;
|
|
8950
|
+
if (flags.list === true) {
|
|
8951
|
+
const r = await remoteCall(url, flags, base);
|
|
8952
|
+
if (flags.json) return console.log(JSON.stringify(r, null, 2)), 0;
|
|
8953
|
+
if (r.running) console.log(`\n ${c.dim(`running: ${r.runId}, since ${r.startedAt}${r.progress ? ` — ${r.progress.done}/${r.progress.total} runs` : ""}`)}`);
|
|
8954
|
+
for (const p of r.comparisons ?? []) {
|
|
8955
|
+
const line = p.status === "done" && p.result ? p.result.configs.map((x) => `${x.label} ${x.rate === null ? "—" : `${Math.round(x.rate * 100)}%`}`).join(" · ") : c.red(p.error ?? "failed");
|
|
8956
|
+
console.log(` ${c.bold(p.runId)} ${c.dim(String(p.startedAt).slice(0, 16).replace("T", " "))} ${line}`);
|
|
8957
|
+
}
|
|
8958
|
+
if (!(r.comparisons ?? []).length && !r.running) console.log(`\n ${c.dim(`no comparisons of ${name} yet`)}`);
|
|
8959
|
+
console.log("");
|
|
8960
|
+
return 0;
|
|
8961
|
+
}
|
|
8962
|
+
const finish = (view) => {
|
|
8963
|
+
if (view.state === "done" && view.result) {
|
|
8964
|
+
if (flags.json) console.log(JSON.stringify(view.result, null, 2));
|
|
8965
|
+
else printComparison(view.result);
|
|
8966
|
+
return 0;
|
|
8967
|
+
}
|
|
8968
|
+
throw new Error(view.error ?? `the comparison ended as ${view.state ?? "unknown"}, with no result`);
|
|
8969
|
+
};
|
|
8970
|
+
// --run reads one comparison and starts nothing: a bare --run (an empty
|
|
8971
|
+
// variable in a script) is refused, never read as "start a new one".
|
|
8972
|
+
if (flags.run !== undefined && (typeof flags.run !== "string" || !flags.run.trim())) {
|
|
8973
|
+
throw new Error("--run takes the comparison id it printed — `foldrun eval compare <eval> --to <ws> --run <id>`");
|
|
8974
|
+
}
|
|
8975
|
+
if (typeof flags.run === "string") {
|
|
8976
|
+
const view = await remoteCall(url, flags, `${base}?run=${encodeURIComponent(flags.run)}`);
|
|
8977
|
+
if (view.state === "running") {
|
|
8978
|
+
if (flags.json) console.log(JSON.stringify(view, null, 2));
|
|
8979
|
+
else console.log(`\n ${c.dim(`still running, since ${view.startedAt}${view.progress ? ` — ${view.progress.done}/${view.progress.total} runs` : ""}`)}\n`);
|
|
8980
|
+
return 1;
|
|
8981
|
+
}
|
|
8982
|
+
return finish(view);
|
|
8983
|
+
}
|
|
8984
|
+
if (typeof flags.models !== "string" || !flags.models.trim()) {
|
|
8985
|
+
throw new Error("--models takes two to four settings, e.g. --models fast,default or --models fast:low,max:high");
|
|
8986
|
+
}
|
|
8987
|
+
const effort = typeof flags.effort === "string" ? flags.effort : undefined;
|
|
8988
|
+
const agent = typeof flags.agent === "string" ? flags.agent : undefined;
|
|
8989
|
+
const configs = flags.models.split(",").map((item) => item.trim()).filter(Boolean).map((item) => {
|
|
8990
|
+
// Only a trailing known effort is an effort: llama3.2:3b stays one model id.
|
|
8991
|
+
const m = /^(.*):(low|medium|high|xhigh|max)$/.exec(item);
|
|
8992
|
+
const model = m ? m[1] : item;
|
|
8993
|
+
const e = m ? m[2] : undefined;
|
|
8994
|
+
return { model, ...((e ?? effort) ? { effort: e ?? effort } : {}), ...(agent ? { agent } : {}) };
|
|
8995
|
+
});
|
|
8996
|
+
const body = { configs, ...(flags.repeat !== undefined ? { repeat: Number(flags.repeat) } : {}), ...(flags["max-usd"] !== undefined ? { maxUsd: Number(flags["max-usd"]) } : {}) };
|
|
8997
|
+
const started = await remoteCall(url, flags, base, { method: "POST", body: JSON.stringify(body) });
|
|
8998
|
+
// --json is one JSON document on stdout; what happens on the way goes to stderr.
|
|
8999
|
+
const say = flags.json ? (s) => console.error(s) : (s) => console.log(s);
|
|
9000
|
+
say(`\n ${c.dim(`comparison ${started.runId}: ${started.trials} test runs, at most $${Number(started.maxUsd).toFixed(2)}`)}`);
|
|
9001
|
+
const wait = explicitTimeout(flags) ?? 3600;
|
|
9002
|
+
const deadline = Date.now() + wait * 1000;
|
|
9003
|
+
let said = -1;
|
|
9004
|
+
for (;;) {
|
|
9005
|
+
if (Date.now() > deadline) {
|
|
9006
|
+
throw new Error(`still running after ${wait}s — \`foldrun eval compare ${name} --to ${ws} --run ${started.runId}\` reads it when it is done`);
|
|
9007
|
+
}
|
|
9008
|
+
await new Promise((r) => setTimeout(r, Number(process.env.FOLDRUN_EVAL_POLL_MS) || 5000));
|
|
9009
|
+
const view = await remoteCall(url, flags, `${base}?run=${encodeURIComponent(started.runId)}`);
|
|
9010
|
+
if (view.state === "running") {
|
|
9011
|
+
if (view.progress && view.progress.done !== said) {
|
|
9012
|
+
said = view.progress.done;
|
|
9013
|
+
say(` ${c.dim(`${view.progress.done}/${view.progress.total} runs done`)}`);
|
|
9014
|
+
}
|
|
9015
|
+
continue;
|
|
9016
|
+
}
|
|
9017
|
+
return finish(view);
|
|
9018
|
+
}
|
|
9019
|
+
}
|
|
9020
|
+
|
|
9021
|
+
// ------------------------------------------------------------- proposals
|
|
9022
|
+
|
|
9023
|
+
/**
|
|
9024
|
+
* A file's change, as ordered -/+ lines with a little context — the same
|
|
9025
|
+
* reading the dashboard's diff gives. An LCS over the lines, so a reordered
|
|
9026
|
+
* step or a removed duplicate shows; a file too long for that (over 2,000
|
|
9027
|
+
* lines a side) is shown whole as removed and added rather than guessed at.
|
|
9028
|
+
*/
|
|
9029
|
+
function lineChanges(before, after) {
|
|
9030
|
+
const a = before === null || before === undefined ? [] : String(before).split("\n");
|
|
9031
|
+
const b = String(after ?? "").split("\n");
|
|
9032
|
+
if (a.length > 2000 || b.length > 2000) return [...a.map((l) => ["-", l]), ...b.map((l) => ["+", l])];
|
|
9033
|
+
const n = a.length;
|
|
9034
|
+
const m = b.length;
|
|
9035
|
+
const lcs = Array.from({ length: n + 1 }, () => new Uint16Array(m + 1));
|
|
9036
|
+
for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
9037
|
+
const out = [];
|
|
9038
|
+
let i = 0;
|
|
9039
|
+
let j = 0;
|
|
9040
|
+
while (i < n || j < m) {
|
|
9041
|
+
if (i < n && j < m && a[i] === b[j]) {
|
|
9042
|
+
out.push([" ", a[i]]);
|
|
9043
|
+
i++;
|
|
9044
|
+
j++;
|
|
9045
|
+
} else if (j < m && (i === n || lcs[i][j + 1] >= lcs[i + 1][j])) out.push(["+", b[j++]]);
|
|
9046
|
+
else out.push(["-", a[i++]]);
|
|
9047
|
+
}
|
|
9048
|
+
return out;
|
|
9049
|
+
}
|
|
9050
|
+
|
|
9051
|
+
function printFileChange(f) {
|
|
9052
|
+
console.log(`\n ${c.bold(f.path)}${f.before === null ? c.dim(" (new)") : ""}`);
|
|
9053
|
+
const ops = lineChanges(f.before, f.content);
|
|
9054
|
+
const near = (k) => ops.slice(Math.max(0, k - 2), k + 3).some(([op]) => op !== " ");
|
|
9055
|
+
let skipped = false;
|
|
9056
|
+
ops.forEach(([op, line], k) => {
|
|
9057
|
+
if (op === " " && !near(k)) {
|
|
9058
|
+
if (!skipped) console.log(` ${c.dim(" …")}`);
|
|
9059
|
+
skipped = true;
|
|
9060
|
+
return;
|
|
9061
|
+
}
|
|
9062
|
+
skipped = false;
|
|
9063
|
+
console.log(` ${op === "+" ? c.green(`+ ${line}`) : op === "-" ? c.red(`- ${line}`) : c.dim(` ${line}`)}`);
|
|
9064
|
+
});
|
|
9065
|
+
}
|
|
9066
|
+
|
|
9067
|
+
const PROPOSAL_LABEL = { drafting: "being drafted", ready: "ready to review", applying: "applying", applied: "applied", partial: "partly applied", failed: "draft failed", dismissed: "dismissed" };
|
|
9068
|
+
|
|
9069
|
+
/**
|
|
9070
|
+
* `foldrun proposals --to <workspace>` — the proposed fixes in a workspace
|
|
9071
|
+
* (GET …/proposals), newest first: status, the eval, how many failures each
|
|
9072
|
+
* covers. `--status ready` for the ones waiting on a person, `--eval <name>`,
|
|
9073
|
+
* `--json`. `show <id>` prints one (the assistant's reasoning, the failures,
|
|
9074
|
+
* the change per file); `apply <id>` writes its files after asking (`--yes`
|
|
9075
|
+
* without a terminal); `dismiss <id>` sets it aside. The Evals page's
|
|
9076
|
+
* Proposed fixes.
|
|
9077
|
+
*/
|
|
9078
|
+
async function proposalsCmd(positional, flags, layout) {
|
|
9079
|
+
const url = platformFor(flags, "proposals");
|
|
9080
|
+
const ws = platformWorkspace(flags, layout, "proposals");
|
|
9081
|
+
const verb = positional[0];
|
|
9082
|
+
const id = positional[1];
|
|
9083
|
+
if (verb === "show" || verb === "apply" || verb === "dismiss") {
|
|
9084
|
+
if (!id) throw new Error(`which proposal? \`foldrun proposals ${verb} <id> --to ${ws}\``);
|
|
9085
|
+
const p = await remoteCall(url, flags, `/api/workspaces/${enc(ws)}/proposals/${enc(id)}`);
|
|
9086
|
+
if (verb === "show") {
|
|
9087
|
+
if (flags.json) return console.log(JSON.stringify(p, null, 2)), 0;
|
|
9088
|
+
console.log(`\n ${c.bold(p.id)} ${PROPOSAL_LABEL[p.status] ?? p.status} · ${p.eval} · ${c.dim(`${String(p.createdAt).slice(0, 16).replace("T", " ")} · ${p.by === "auto" ? "opened after repeated failures" : `asked by ${p.by}`}`)}`);
|
|
9089
|
+
if (p.error) console.log(` ${c.red(p.error)}`);
|
|
9090
|
+
for (const n of p.notes ?? []) console.log(` ${c.amber(n)}`);
|
|
9091
|
+
console.log(`\n ${c.dim("covers:")}`);
|
|
9092
|
+
for (const f of p.failures ?? []) console.log(` · ${f.runId ?? f.case} — ${f.check}: ${f.detail}`);
|
|
9093
|
+
if (p.reply) console.log(`\n${p.reply.split("\n").map((l) => ` ${l}`).join("\n")}`);
|
|
9094
|
+
for (const f of p.files ?? []) printFileChange(f);
|
|
9095
|
+
for (const i of p.issues ?? []) console.log(` ${i.level === "error" ? c.red("✗") : c.amber("!")} ${i.where}: ${i.message}`);
|
|
9096
|
+
console.log("");
|
|
9097
|
+
return 0;
|
|
9098
|
+
}
|
|
9099
|
+
if (verb === "apply") {
|
|
9100
|
+
if (p.status !== "ready") throw new Error(`${id} is ${p.status} — only a ready proposal can be applied`);
|
|
9101
|
+
// With --json the change is still shown before the yes — on stderr, so stdout stays one document.
|
|
9102
|
+
if (flags.json) {
|
|
9103
|
+
const log = console.log;
|
|
9104
|
+
console.log = (...a) => console.error(...a);
|
|
9105
|
+
try {
|
|
9106
|
+
for (const f of p.files ?? []) printFileChange(f);
|
|
9107
|
+
} finally {
|
|
9108
|
+
console.log = log;
|
|
9109
|
+
}
|
|
9110
|
+
} else for (const f of p.files ?? []) printFileChange(f);
|
|
9111
|
+
const files = (p.files ?? []).map((f) => f.path).join(", ");
|
|
9112
|
+
if (!(await confirmed(flags, "proposals apply", `\n Write ${files} in ${ws}? [y/N] `))) {
|
|
9113
|
+
console.log(`\n ${c.dim("nothing written")}\n`);
|
|
9114
|
+
return 1;
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
9117
|
+
const r = await remoteCall(url, flags, `/api/workspaces/${enc(ws)}/proposals/${enc(id)}`, { method: "POST", body: JSON.stringify({ action: verb }) });
|
|
9118
|
+
if (flags.json) console.log(JSON.stringify(r, null, 2));
|
|
9119
|
+
else console.log(`\n ${c.green(PROPOSAL_LABEL[r.status] ?? r.status)} ${r.id}${r.applied?.length ? c.dim(` — wrote ${r.applied.join(", ")}`) : ""}\n`);
|
|
9120
|
+
return 0;
|
|
9121
|
+
}
|
|
9122
|
+
if (verb && verb !== "ls" && verb !== "list") throw new Error(`\`foldrun proposals\` lists them; show, apply and dismiss take an id — not "${verb}"`);
|
|
9123
|
+
const q = new URLSearchParams();
|
|
9124
|
+
if (typeof flags.status === "string") q.set("status", flags.status);
|
|
9125
|
+
if (typeof flags.eval === "string") q.set("eval", flags.eval);
|
|
9126
|
+
const { proposals = [] } = await remoteCall(url, flags, `/api/workspaces/${enc(ws)}/proposals${q.size ? `?${q}` : ""}`);
|
|
9127
|
+
if (flags.json) return console.log(JSON.stringify(proposals, null, 2)), 0;
|
|
9128
|
+
if (!proposals.length) {
|
|
9129
|
+
console.log(`\n ${c.dim(`no proposed fixes in ${ws}`)}\n`);
|
|
9130
|
+
return 0;
|
|
9131
|
+
}
|
|
9132
|
+
console.log("");
|
|
9133
|
+
for (const p of proposals) {
|
|
9134
|
+
const tone = p.status === "ready" ? c.amber : p.status === "applied" ? c.green : p.status === "failed" || p.status === "partial" ? c.red : c.dim;
|
|
9135
|
+
console.log(` ${c.bold(p.id)} ${tone((PROPOSAL_LABEL[p.status] ?? p.status).padEnd(15))} ${p.eval} ${c.dim(`· ${p.failures?.length ?? 0} failure(s) · ${(p.files ?? []).map((f) => f.path).join(", ") || "no files"} · ${String(p.createdAt).slice(0, 10)}`)}`);
|
|
9136
|
+
}
|
|
9137
|
+
console.log("");
|
|
9138
|
+
return 0;
|
|
9139
|
+
}
|
|
9140
|
+
|
|
9141
|
+
/**
|
|
9142
|
+
* `foldrun eval improve <eval> --to <workspace>` — ask for a proposed fix now
|
|
9143
|
+
* (POST …/evals/<e>/improve): from a grader's failed grades no proposal has
|
|
9144
|
+
* covered, or an eval's last failed cases. Waits for the draft (`--timeout`,
|
|
9145
|
+
* default 300) and prints it; `--no-wait` returns at once with the id.
|
|
9146
|
+
*/
|
|
9147
|
+
async function evalImproveCmd(name, flags, layout) {
|
|
9148
|
+
if (!name) throw new Error("which eval? `foldrun eval improve <eval> --to <workspace>`");
|
|
9149
|
+
const url = platformFor(flags, "eval improve");
|
|
9150
|
+
const ws = platformWorkspace(flags, layout, `eval improve ${name}`);
|
|
9151
|
+
const p = await remoteCall(url, flags, `/api/workspaces/${enc(ws)}/evals/${enc(name)}/improve`, { method: "POST", body: "{}" });
|
|
9152
|
+
const say = flags.json ? (s) => console.error(s) : (s) => console.log(s);
|
|
9153
|
+
say(`\n ${c.dim(`proposal ${p.id} — the assistant is drafting it from ${p.failures?.length ?? 0} failure(s)`)}`);
|
|
9154
|
+
if (flags.wait === false || flags["no-wait"] === true) {
|
|
9155
|
+
if (flags.json) console.log(JSON.stringify(p, null, 2));
|
|
9156
|
+
else say(` ${c.dim(`\`foldrun proposals show ${p.id} --to ${ws}\` when it is ready`)}\n`);
|
|
9157
|
+
return 0;
|
|
9158
|
+
}
|
|
9159
|
+
const deadline = Date.now() + (explicitTimeout(flags) ?? 300) * 1000;
|
|
9160
|
+
for (;;) {
|
|
9161
|
+
await new Promise((r) => setTimeout(r, Number(process.env.FOLDRUN_EVAL_POLL_MS) || 4000));
|
|
9162
|
+
const now = await remoteCall(url, flags, `/api/workspaces/${enc(ws)}/proposals/${enc(p.id)}`);
|
|
9163
|
+
if (now.status !== "drafting") return proposalsCmd(["show", p.id], flags, layout).then(() => (now.status === "ready" ? 0 : 1));
|
|
9164
|
+
if (Date.now() > deadline) {
|
|
9165
|
+
if (flags.json) console.log(JSON.stringify(now, null, 2));
|
|
9166
|
+
else say(` ${c.dim(`still drafting — \`foldrun proposals show ${p.id} --to ${ws}\` later`)}\n`);
|
|
9167
|
+
return 1;
|
|
9168
|
+
}
|
|
9169
|
+
}
|
|
9170
|
+
}
|
|
9171
|
+
|
|
8764
9172
|
/**
|
|
8765
9173
|
* `foldrun promote <run-id>` — a finished run, kept as a regression case:
|
|
8766
9174
|
* its task, and what the next run must still say.
|
|
@@ -9980,6 +10388,54 @@ async function flowTriggerCmd(positional, flags, layout) {
|
|
|
9980
10388
|
});
|
|
9981
10389
|
}
|
|
9982
10390
|
|
|
10391
|
+
/**
|
|
10392
|
+
* `foldrun flow filter <flow> --payload <file|-|json> [--to <workspace>]` —
|
|
10393
|
+
* would this delivery start the flow? Its `filter:` (core trigger-filter.ts)
|
|
10394
|
+
* against a payload: a webhook's JSON body, an email's { from, to, subject,
|
|
10395
|
+
* text }, a chained run's { run, flow, status, ending, verdict, summary }, a
|
|
10396
|
+
* storage event's { by, paths }. With --to, the deployed flow answers (POST
|
|
10397
|
+
* …/flows/<flow>/filter — the Triggers page's Test box); without, the local
|
|
10398
|
+
* file. Nothing starts. Exit 0 when it would start a run, 1 when not.
|
|
10399
|
+
*/
|
|
10400
|
+
async function flowFilterCmd(positional, flags, layout) {
|
|
10401
|
+
const flow = positional[1];
|
|
10402
|
+
if (!flow || flags.payload === undefined || flags.payload === true) {
|
|
10403
|
+
throw new Error("`foldrun flow filter <flow> --payload <file.json|->` (or inline JSON); --to <workspace> tests the deployed flow");
|
|
10404
|
+
}
|
|
10405
|
+
const spec = String(flags.payload);
|
|
10406
|
+
const text = spec === "-" ? fs.readFileSync(0, "utf8") : /^\s*[{[]/.test(spec) ? spec : fs.readFileSync(spec, "utf8");
|
|
10407
|
+
let payload;
|
|
10408
|
+
try {
|
|
10409
|
+
payload = JSON.parse(text);
|
|
10410
|
+
} catch {
|
|
10411
|
+
payload = text; // read as a webhook body: not JSON, so no filter matches
|
|
10412
|
+
}
|
|
10413
|
+
let answer;
|
|
10414
|
+
if (namesPlatform(flags)) {
|
|
10415
|
+
const url = platformFor(flags, "flow filter");
|
|
10416
|
+
const ws = platformWorkspace(flags, layout, "flow filter");
|
|
10417
|
+
answer = await remoteCall(url, flags, `/api/workspaces/${enc(ws)}/flows/${enc(flow)}/filter`, { method: "POST", body: JSON.stringify({ payload }) });
|
|
10418
|
+
} else {
|
|
10419
|
+
const desk = await openDesk(flags, layout, `flow filter ${flow}`, flow);
|
|
10420
|
+
const rel = await flowFileOf(desk, flow);
|
|
10421
|
+
const lib = /** @type {any} */ (await core());
|
|
10422
|
+
if (typeof lib.matchFilter !== "function") {
|
|
10423
|
+
throw new Error("this install's @foldrun/core predates filter: — update foldrun, or test the deployed flow with --to <workspace>");
|
|
10424
|
+
}
|
|
10425
|
+
const f = lib.parseFlow(path.basename(rel), desk.files.get(rel));
|
|
10426
|
+
const read = typeof payload === "string" ? lib.webhookFilterPayload(payload) : payload;
|
|
10427
|
+
answer = { flow: f.name, trigger: f.trigger, filter: f.filter, applies: lib.FILTERABLE_TRIGGERS.includes(f.trigger), ...(f.filter != null ? lib.matchFilter(f.filter, read) : { match: true, reason: "this flow has no filter: — every delivery starts it" }) };
|
|
10428
|
+
}
|
|
10429
|
+
if (flags.json) {
|
|
10430
|
+
console.log(JSON.stringify(answer, null, 2));
|
|
10431
|
+
} else {
|
|
10432
|
+
console.log(`\n ${answer.match ? c.green("would start a run") : c.amber("would not start a run")} — ${answer.reason}`);
|
|
10433
|
+
if (answer.applies === false) console.log(` ${c.dim(`trigger: ${answer.trigger} delivers no payload, so its filter never applies`)}`);
|
|
10434
|
+
console.log("");
|
|
10435
|
+
}
|
|
10436
|
+
return answer.match ? 0 : 1;
|
|
10437
|
+
}
|
|
10438
|
+
|
|
9983
10439
|
/**
|
|
9984
10440
|
* `foldrun flow move-step <flow> --step <n|agent> --group <g> | --after <g>`
|
|
9985
10441
|
* — dragging a card on the board: into group g, in parallel with what is
|
|
@@ -11290,6 +11746,13 @@ export async function run(command, positional, flags, workspace, layout) {
|
|
|
11290
11746
|
// `eval new <name>` makes one on the platform; an eval called "new"
|
|
11291
11747
|
// still runs as `foldrun eval new` with nothing after it.
|
|
11292
11748
|
if (positional[0] === "new" && positional[1]) return evalNewCmd(positional[1], flags, layout);
|
|
11749
|
+
// `eval grades <name>` / `eval grade <name> --run <id>`: a grader's
|
|
11750
|
+
// findings, or one run graded now (an eval called "grades" or "grade"
|
|
11751
|
+
// still runs as `foldrun eval grades` with nothing after it).
|
|
11752
|
+
if (positional[0] === "grades" && positional[1]) return evalGradesCmd(positional[1], flags, layout);
|
|
11753
|
+
if (positional[0] === "grade" && positional[1]) return evalGradeCmd(positional[1], flags, layout);
|
|
11754
|
+
if (positional[0] === "compare" && positional[1]) return evalCompareCmd(positional[1], flags, layout);
|
|
11755
|
+
if (positional[0] === "improve" && positional[1]) return evalImproveCmd(positional[1], flags, layout);
|
|
11293
11756
|
// --to names a deployed workspace: its evals run there.
|
|
11294
11757
|
if (typeof flags.to === "string") return remoteEvalsCmd(positional[0], flags, layout);
|
|
11295
11758
|
// --last reads the platform's results; a local eval has none to read,
|
|
@@ -11299,6 +11762,9 @@ export async function run(command, positional, flags, workspace, layout) {
|
|
|
11299
11762
|
return runEvals(positional[0], flags);
|
|
11300
11763
|
case "promote":
|
|
11301
11764
|
return promoteCmd(positional[0], flags, layout);
|
|
11765
|
+
case "proposals":
|
|
11766
|
+
case "proposal":
|
|
11767
|
+
return proposalsCmd(positional, flags, layout);
|
|
11302
11768
|
case "observe":
|
|
11303
11769
|
return observeCmd(flags, layout);
|
|
11304
11770
|
case "usage":
|
|
@@ -11360,6 +11826,8 @@ export async function run(command, positional, flags, workspace, layout) {
|
|
|
11360
11826
|
return approvalsCmd(flags, layout);
|
|
11361
11827
|
case "issues":
|
|
11362
11828
|
return issuesCmd(positional, flags, layout);
|
|
11829
|
+
case "inbox":
|
|
11830
|
+
return inboxCmd(positional, flags);
|
|
11363
11831
|
case "insights":
|
|
11364
11832
|
return insightsCmd(flags);
|
|
11365
11833
|
case "approve":
|
|
@@ -11437,6 +11905,7 @@ export async function run(command, positional, flags, workspace, layout) {
|
|
|
11437
11905
|
if (positional[0] === "hook") return flowHookCmd(positional, flags, layout);
|
|
11438
11906
|
if (positional[0] === "set") return flowSetCmd(positional, flags, layout);
|
|
11439
11907
|
if (positional[0] === "trigger") return flowTriggerCmd(positional, flags, layout);
|
|
11908
|
+
if (positional[0] === "filter") return flowFilterCmd(positional, flags, layout);
|
|
11440
11909
|
if (positional[0] === "move-step") return flowMoveStepCmd(positional, flags, layout);
|
|
11441
11910
|
if (positional[0] === "copy-step") return flowCopyStepCmd(positional, flags, layout);
|
|
11442
11911
|
if (positional[0] === "paste") return flowPasteCmd(positional, flags, layout);
|
package/src/guide.mjs
CHANGED
|
@@ -30,7 +30,7 @@ export function agentRulesBlock() {
|
|
|
30
30
|
|
|
31
31
|
# Read foldrun's docs before you edit this folder
|
|
32
32
|
|
|
33
|
-
This is a foldrun account: agents, flows, tools and knowledge as markdown, run by foldrun. Its formats — frontmatter fields, step options, \`verify:\` prefixes, which folder a file goes in — are foldrun's own and may differ from your training data. Before you write or change an agent, flow, tool or skill, read the relevant guide: \`foldrun docs\` lists them and \`foldrun docs <page>\` prints one (start with \`foldrun docs coding-agents\`). They ship with the CLI, so they match the version you are driving. Check every edit with \`foldrun check\` — offline, no model calls; it is also the security scanner (a key written into a file, hidden text, an agent that can leak), and \`foldrun check --security\` fails on what it finds. Never paste a credential into a file: \`foldrun secrets set NAME\` and write \`\${NAME}\`. For a flow's shape, \`foldrun flow show <flow>\` draws it, and \`foldrun flow add\`, \`foldrun flow rm-step\`, \`foldrun flow dup-step\` and \`foldrun agent link\` make the same edits as the dashboard canvas (patterns, sub-agents, consults, \`ask\`). An agent with \`tools: [peers]\` can message the steps running beside it in its parallel group (\`peers_message\`) and ask for what they have instead of guessing or failing (\`foldrun docs agents\`). A sub-agent that reads untrusted content (web pages, mail, uploads) gets \`sandbox: own\` in its own file: each job then runs in a sandbox of its own with its own tools, secrets and network, and only its reply
|
|
33
|
+
This is a foldrun account: agents, flows, tools and knowledge as markdown, run by foldrun. Its formats — frontmatter fields, step options, \`verify:\` prefixes, which folder a file goes in — are foldrun's own and may differ from your training data. Before you write or change an agent, flow, tool or skill, read the relevant guide: \`foldrun docs\` lists them and \`foldrun docs <page>\` prints one (start with \`foldrun docs coding-agents\`). They ship with the CLI, so they match the version you are driving. Check every edit with \`foldrun check\` — offline, no model calls; it is also the security scanner (a key written into a file, hidden text, an agent that can leak), and \`foldrun check --security\` fails on what it finds. Never paste a credential into a file: \`foldrun secrets set NAME\` and write \`\${NAME}\`. For a flow's shape, \`foldrun flow show <flow>\` draws it, and \`foldrun flow add\`, \`foldrun flow rm-step\`, \`foldrun flow dup-step\` and \`foldrun agent link\` make the same edits as the dashboard canvas (patterns, sub-agents, consults, \`ask\`). An agent with \`tools: [peers]\` can message the steps running beside it in its parallel group (\`peers_message\`) and ask for what they have instead of guessing or failing (\`foldrun docs agents\`). A sub-agent that reads untrusted content (web pages, mail, uploads) gets \`sandbox: own\` in its own file: each job then runs in a sandbox of its own with its own tools, secrets and network, and only its reply and the files it wrote reach the agent that delegated (\`foldrun agent link <agent> --subagent <worker> --isolated\`). A script tool's own \`secrets: [NAMES]\` are granted with the tool; list on an agent only the secrets it uses directly. A hook (\`hooks/<name>/hook.md\` + \`run.mjs\`, turned on with \`hooks: [name]\`) is code at every tool call, step start and step end — \`foldrun hook add\`/\`test\` and \`foldrun docs hooks\`. Paths to the workspace's own folders are written \`workspace/storage/…\` and \`workspace/state/…\` — in an agent's prose, a flow's options and a script's arguments — not \`../../\`, which still works but is the older spelling. Which sites a step's sandbox may reach is \`network:\` — open for an agent that may fetch or browse the web, the trusted list plus what it declares for every other; add a site with \`foldrun agent network <agent> --add <site>\` (\`foldrun docs network\`). Bind a secret to the sites it is for (\`foldrun secrets sites NAME <site>\`) and no sandbox ever holds its value. A webhook, email, chained or storage flow says which deliveries start it with \`filter:\` (\`foldrun flow filter <flow> --payload body.json\` tries one); an eval with \`trigger: runs\` grades a \`sample:\` of real finished runs instead of running cases, \`foldrun eval compare\` runs an eval's cases under several models side by side, and \`improve: true\` turns repeated failures into a proposed fix a person applies (\`foldrun docs evals\`).
|
|
34
34
|
|
|
35
35
|
This block is for coding tools. It is written and re-added by the foldrun CLI, and the runtime strips it, so the agents that run here never see it; the rest of this file is the account's own context, which they do read. Committing the block with your work keeps the tree clean.
|
|
36
36
|
|