foldrun 2.2.0 → 2.3.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 +23 -9
- package/docs/agents.md +54 -2
- package/docs/api.md +21 -12
- package/docs/assistant.md +32 -2
- package/docs/budgets-and-billing.md +46 -5
- package/docs/checks.md +101 -2
- package/docs/cli.md +18 -10
- package/docs/evals.md +9 -4
- package/docs/flows.md +6 -1
- package/docs/network.md +22 -7
- package/docs/notifications.md +54 -0
- package/docs/runs.md +196 -4
- package/docs/security.md +3 -2
- package/docs/tools.md +3 -1
- package/docs/workspaces.md +42 -0
- package/package.json +3 -3
- package/src/commands.mjs +551 -23
- package/src/guide.mjs +1 -1
package/src/commands.mjs
CHANGED
|
@@ -440,20 +440,34 @@ async function remoteListCmd(kind, flags, layout) {
|
|
|
440
440
|
const url = platformFor(flags, `${one} ls`);
|
|
441
441
|
const ws = platformWorkspace(flags, layout, `${one} ls`);
|
|
442
442
|
const r = await remoteCall(url, flags, `/api/workspaces/${enc(ws)}/${kind}`);
|
|
443
|
-
|
|
443
|
+
let list = r[kind] ?? [];
|
|
444
|
+
// --stale [days]: the agents nobody has run lately — with a number, not
|
|
445
|
+
// run in that many days (never run included); bare, the ones past the
|
|
446
|
+
// workspace's alerts: stale_days, as the platform counts them.
|
|
447
|
+
if (kind === "agents" && flags.stale !== undefined) {
|
|
448
|
+
const days = flags.stale === true ? null : Number(flags.stale);
|
|
449
|
+
if (days !== null && !(Number.isInteger(days) && days > 0)) throw new Error(`--stale takes a whole number of days, e.g. --stale 30 — not "${flags.stale}"`);
|
|
450
|
+
list = list.filter((x) => (days === null ? x.stale === true : !x.lastRunAt || Date.now() - Date.parse(x.lastRunAt) >= days * 86_400_000));
|
|
451
|
+
}
|
|
444
452
|
if (flags.json === true) {
|
|
445
453
|
console.log(JSON.stringify(list, null, 2));
|
|
446
454
|
return 0;
|
|
447
455
|
}
|
|
448
456
|
if (!list.length) {
|
|
449
|
-
console.log(`\n ${c.dim(`no ${kind} in ${ws} on ${url} — \`foldrun ${one} new <name> --platform --to ${ws}\` makes one`)}\n`);
|
|
457
|
+
console.log(`\n ${c.dim(flags.stale !== undefined ? `no stale agents in ${ws}` : `no ${kind} in ${ws} on ${url} — \`foldrun ${one} new <name> --platform --to ${ws}\` makes one`)}\n`);
|
|
450
458
|
return 0;
|
|
451
459
|
}
|
|
452
460
|
const w = Math.max(...list.map((x) => x.name.length));
|
|
461
|
+
// Who answers for each one (owner:, or the workspace's, or the account's);
|
|
462
|
+
// the column only when someone is named, so a list with none is unchanged.
|
|
463
|
+
const ow = kind === "agents" ? Math.max(0, ...list.map((x) => (x.owner ?? "").length)) : 0;
|
|
453
464
|
console.log();
|
|
454
465
|
for (const x of list) {
|
|
455
466
|
if (kind === "agents") {
|
|
456
|
-
|
|
467
|
+
const owner = ow ? `${pad(x.owner ?? "—", Math.max(ow, 1))} ` : "";
|
|
468
|
+
// When it last ran, from a platform that says (lastRunAt is absent before it did).
|
|
469
|
+
const ran = "lastRunAt" in x ? `${pad(x.lastRunAt ? `${Math.floor((Date.now() - Date.parse(x.lastRunAt)) / 86_400_000)}d ago` : "never", 7)} ` : "";
|
|
470
|
+
console.log(` ${c.bold(pad(x.name, w))} ${pad(x.model ?? "", 8)} ${ran}${owner}${x.stale ? c.amber("stale ") : ""}${c.dim(firstLine(x.description ?? "", 80))}`);
|
|
457
471
|
} else {
|
|
458
472
|
const when = x.trigger === "schedule" && x.schedule ? `schedule ${x.schedule}${x.timezone ? ` ${x.timezone}` : ""}` : (x.trigger ?? "manual");
|
|
459
473
|
console.log(` ${c.bold(pad(x.name, w))} ${c.dim(when)}`);
|
|
@@ -973,7 +987,7 @@ async function checkWorkspace(workspace, flags, opts) {
|
|
|
973
987
|
let accountNetwork;
|
|
974
988
|
try {
|
|
975
989
|
const md = path.join(accountRoot, "AGENTS.md");
|
|
976
|
-
if (md !== path.join(workspace, "AGENTS.md") && fs.existsSync(md)) accountNetwork = (await import("gray-matter")).default(fs.readFileSync(md, "utf8")).data?.network;
|
|
990
|
+
if (md !== path.join(workspace, "AGENTS.md") && fs.existsSync(md)) accountNetwork = (await import("gray-matter")).default(fs.readFileSync(md, "utf8"), {}).data?.network;
|
|
977
991
|
} catch {
|
|
978
992
|
// an unreadable account AGENTS.md is reported by its own check
|
|
979
993
|
}
|
|
@@ -1228,12 +1242,17 @@ async function checkProblems(workspace, flags = {}) {
|
|
|
1228
1242
|
if (webWarnings) {
|
|
1229
1243
|
try {
|
|
1230
1244
|
const matter = (await import("gray-matter")).default;
|
|
1231
|
-
|
|
1245
|
+
// `{}`: gray-matter caches a failed parse as an empty success without it.
|
|
1246
|
+
const front = matter(fs.readFileSync(path.join(workspace, "agents", a.name, "agent.md"), "utf8"), {}).data ?? {};
|
|
1232
1247
|
for (const w of webWarnings(front, runnerBrowsers)) note("warn", `agents/${a.name}`, w);
|
|
1233
1248
|
} catch {
|
|
1234
1249
|
// an unreadable agent.md is already reported by the loader
|
|
1235
1250
|
}
|
|
1236
1251
|
}
|
|
1252
|
+
// agent.md whose YAML does not parse: core lists it with defaults and the
|
|
1253
|
+
// reason (core 2.5) rather than throwing; the file is unreadable to a run.
|
|
1254
|
+
const frontmatterError = /** @type {{ frontmatterError?: string }} */ (a).frontmatterError;
|
|
1255
|
+
if (frontmatterError) note("error", `agents/${a.name}`, frontmatterError);
|
|
1237
1256
|
// `schedule:` on an agent fires nothing — only a flow has a clock.
|
|
1238
1257
|
if (a.scheduleProblem) note("error", `agents/${a.name}`, a.scheduleProblem);
|
|
1239
1258
|
// `language:` that is not a tag — "Persian" where `fa` was meant.
|
|
@@ -5250,10 +5269,11 @@ async function rerunCmd(runId, flags, layout) {
|
|
|
5250
5269
|
const ws = await workspaceOfRun(url, flags, layout, runId);
|
|
5251
5270
|
const body = await remoteCall(url, flags, `/api/workspaces/${ws}/runs/${runId}/rerun`, {
|
|
5252
5271
|
method: "POST",
|
|
5253
|
-
|
|
5272
|
+
// --instruction: what that step is told this once (FOL-72 step 6).
|
|
5273
|
+
body: JSON.stringify({ ...(agent ? { agent } : { step: from }), ...(typeof flags.instruction === "string" ? { instruction: flags.instruction } : {}) }),
|
|
5254
5274
|
});
|
|
5255
5275
|
const id = body.runId ?? body.id;
|
|
5256
|
-
console.log(`\n ${c.green("✓")} ${c.bold(id)} ${c.dim(`${ws} · again from ${agent ? `agent ${agent}` : `step ${from}`} of ${runId} · ${body.steps ?? "?"} steps`)}`);
|
|
5276
|
+
console.log(`\n ${c.green("✓")} ${c.bold(id)} ${c.dim(`${ws} · again from ${agent ? `agent ${agent}` : `step ${from}`} of ${runId} · ${body.steps ?? "?"} steps${typeof flags.instruction === "string" ? " · with your instruction for that step" : ""}`)}`);
|
|
5257
5277
|
console.log(` ${c.dim(`foldrun logs ${id} --to ${ws}${flags.wait === true ? "" : " · --wait follows it"}`)}\n`);
|
|
5258
5278
|
if (flags.wait !== true) return 0;
|
|
5259
5279
|
return logsCmd([id], { ...flags, follow: true, to: ws }, layout);
|
|
@@ -5573,6 +5593,119 @@ function printDefaults(d) {
|
|
|
5573
5593
|
console.log(` notify ${notify}`);
|
|
5574
5594
|
console.log(` budget ${d.budget ? `$${d.budget.usd} per ${d.budget.period}` : c.dim("no cap")}`);
|
|
5575
5595
|
console.log(` concurrency ${d.concurrency ?? c.dim("the plan's number")}`);
|
|
5596
|
+
console.log(` deploy_block ${d.deployBlock && d.deployBlock !== "none" ? `${d.deployBlock} ${c.dim(d.deployBlock === "secrets" ? "— a written-in key refuses a deploy" : "— any security finding refuses a deploy")}` : c.dim("none — security findings only warn")}`);
|
|
5597
|
+
console.log(` network ${networkEnforceText(d.networkEnforce, "the install")}`);
|
|
5598
|
+
}
|
|
5599
|
+
|
|
5600
|
+
/** network_enforce: as a person reads it. Off is never "allowed" outright:
|
|
5601
|
+
* the level above (the account, or the install's own switch) may refuse
|
|
5602
|
+
* anyway, and this file cannot see either, so off says what it leaves to. */
|
|
5603
|
+
function networkEnforceText(on, above) {
|
|
5604
|
+
return on ? `refused ${c.dim("— a site off an agent's network: list gets a 403")}` : c.dim(`${above}'s — this file does not ask to refuse`);
|
|
5605
|
+
}
|
|
5606
|
+
|
|
5607
|
+
/** `on`/`off` (or true/false, yes/no) for network_enforce, or an error. */
|
|
5608
|
+
function onOff(key, value, how) {
|
|
5609
|
+
const v = String(value ?? "").toLowerCase();
|
|
5610
|
+
if (["on", "true", "yes"].includes(v)) return true;
|
|
5611
|
+
if (["off", "false", "no"].includes(v)) return false;
|
|
5612
|
+
throw new Error(`${key} is on or off — e.g. \`${how}\` — not "${value ?? ""}"`);
|
|
5613
|
+
}
|
|
5614
|
+
|
|
5615
|
+
/**
|
|
5616
|
+
* `foldrun account telemetry` — where each finished run goes as an
|
|
5617
|
+
* OpenTelemetry trace (the account's `telemetry:`). `set <url> --header
|
|
5618
|
+
* authorization=\${OTEL_TOKEN}`, `clear`, `test` (POST …/telemetry/test:
|
|
5619
|
+
* one made-up run, and what the collector answered).
|
|
5620
|
+
*/
|
|
5621
|
+
async function telemetryCmd(url, args, flags) {
|
|
5622
|
+
const [sub, endpoint] = args;
|
|
5623
|
+
if (!sub) {
|
|
5624
|
+
const { defaults } = await remoteCall(url, flags, "/api/account");
|
|
5625
|
+
const t = defaults?.telemetry;
|
|
5626
|
+
if (!t) {
|
|
5627
|
+
console.log(`\n ${c.dim("runs are not sent anywhere — foldrun account telemetry set https://<collector>/v1/traces --header authorization=\${OTEL_TOKEN}")}\n`);
|
|
5628
|
+
return 0;
|
|
5629
|
+
}
|
|
5630
|
+
console.log(`\n ${c.bold("telemetry")} ${t.endpoint}`);
|
|
5631
|
+
for (const [k, v] of Object.entries(t.headers ?? {})) console.log(` ${c.dim(k)} ${v}`);
|
|
5632
|
+
console.log();
|
|
5633
|
+
return 0;
|
|
5634
|
+
}
|
|
5635
|
+
if (sub === "test") {
|
|
5636
|
+
const r = await remoteCall(url, flags, "/api/account/telemetry/test", { method: "POST", body: "{}" });
|
|
5637
|
+
if (r.ok) console.log(`\n ${c.green("✓")} ${r.endpoint} took it — ${r.spans} spans, HTTP ${r.status}. Look for the trace "run foldrun-telemetry-test".\n`);
|
|
5638
|
+
else console.log(`\n ${c.red("✗")} not delivered to ${r.endpoint} — ${r.error}\n`);
|
|
5639
|
+
return r.ok ? 0 : 1;
|
|
5640
|
+
}
|
|
5641
|
+
if (sub === "clear") {
|
|
5642
|
+
await remoteCall(url, flags, "/api/account", { method: "PATCH", body: JSON.stringify({ telemetry: null }) });
|
|
5643
|
+
console.log(`\n ${c.green("✓")} runs are no longer sent\n`);
|
|
5644
|
+
return 0;
|
|
5645
|
+
}
|
|
5646
|
+
if (sub !== "set") throw new Error(`unknown telemetry verb "${sub}" — set <url> [--header name=\${SECRET}], clear, test (bare: show it)`);
|
|
5647
|
+
if (!endpoint) throw new Error("`foldrun account telemetry set https://<collector>/v1/traces --header authorization=${OTEL_TOKEN}`");
|
|
5648
|
+
const headers = {};
|
|
5649
|
+
for (const h of [flags.header].flat().filter((x) => typeof x === "string")) {
|
|
5650
|
+
const i = h.indexOf("=");
|
|
5651
|
+
if (i < 1) throw new Error(`--header is name=value, e.g. --header authorization='\${OTEL_TOKEN}' — not "${h}"`);
|
|
5652
|
+
headers[h.slice(0, i).trim().toLowerCase()] = h.slice(i + 1).trim();
|
|
5653
|
+
}
|
|
5654
|
+
const r = await remoteCall(url, flags, "/api/account", { method: "PATCH", body: JSON.stringify({ telemetry: { otlp: endpoint, ...(Object.keys(headers).length ? { headers } : {}) } }) });
|
|
5655
|
+
console.log(`\n ${c.green("✓")} every run that finishes is sent to ${r.defaults?.telemetry?.endpoint ?? endpoint} — \`foldrun account telemetry test\` proves it arrives\n`);
|
|
5656
|
+
return 0;
|
|
5657
|
+
}
|
|
5658
|
+
|
|
5659
|
+
/** A GET …/pause, or null from a platform older than the kill switch —
|
|
5660
|
+
* the settings still print, without the line. */
|
|
5661
|
+
async function pauseState(request) {
|
|
5662
|
+
try {
|
|
5663
|
+
return await request;
|
|
5664
|
+
} catch (err) {
|
|
5665
|
+
if (err instanceof HttpError && err.status === 404) return null;
|
|
5666
|
+
throw err;
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5669
|
+
|
|
5670
|
+
/** One pause mark as a line: since when, by whom, why. */
|
|
5671
|
+
function pauseLine(m) {
|
|
5672
|
+
return `${m.at.slice(0, 16).replace("T", " ")} UTC by ${m.by}${m.reason ? ` — ${m.reason}` : ""}`;
|
|
5673
|
+
}
|
|
5674
|
+
|
|
5675
|
+
/**
|
|
5676
|
+
* `foldrun account pause|resume`, `foldrun workspace pause|resume` — the
|
|
5677
|
+
* kill switch (POST / DELETE …/pause). Pausing is undone by a resume, so it
|
|
5678
|
+
* asks nothing; --stop also stops the runs going now, which nothing undoes,
|
|
5679
|
+
* so that asks.
|
|
5680
|
+
*/
|
|
5681
|
+
async function pauseCmd(flags, verb, what, { post, del }) {
|
|
5682
|
+
if (verb === "resume") {
|
|
5683
|
+
const r = await del();
|
|
5684
|
+
const still = r.paused;
|
|
5685
|
+
// An account resume leaves a workspace paused on its own paused.
|
|
5686
|
+
const own = Object.keys(r.workspaces ?? {});
|
|
5687
|
+
const held = still
|
|
5688
|
+
? c.amber(` — but still held: ${still.scope === "account" ? "the account" : "this workspace"} is paused (${pauseLine(still)})`)
|
|
5689
|
+
: own.length
|
|
5690
|
+
? c.amber(` — but ${own.join(", ")} ${own.length === 1 ? "is" : "are"} paused on ${own.length === 1 ? "its" : "their"} own (\`foldrun workspace resume --to <name>\`)`)
|
|
5691
|
+
: " — runs start again";
|
|
5692
|
+
console.log(`\n ${c.green("✓")} resumed ${what}${held}\n`);
|
|
5693
|
+
return 0;
|
|
5694
|
+
}
|
|
5695
|
+
const stop = flags.stop === true;
|
|
5696
|
+
if (stop && !(await confirmed(flags, "pause --stop", `Pause ${what} and stop every run going now? [y/N] `))) {
|
|
5697
|
+
console.log(`\n ${c.dim("nothing paused, nothing stopped")}\n`);
|
|
5698
|
+
return 1;
|
|
5699
|
+
}
|
|
5700
|
+
const body = { ...(typeof flags.reason === "string" && flags.reason.trim() ? { reason: flags.reason.trim() } : {}), ...(stop ? { stop: true } : {}) };
|
|
5701
|
+
const r = await post(JSON.stringify(body));
|
|
5702
|
+
console.log(`\n ${c.green("✓")} paused ${what} — no new run starts until \`resume\``);
|
|
5703
|
+
if (stop) console.log(` ${c.dim(`${r.stopped?.length ?? 0} run(s) stopped`)}`);
|
|
5704
|
+
// A stop that failed is a run that may still be going: named, and the
|
|
5705
|
+
// exit says so, rather than assumed to have finished on its own.
|
|
5706
|
+
for (const f of r.failed ?? []) console.log(` ${c.red("✗")} ${f.workspace}/${f.runId} not stopped — ${f.error}`);
|
|
5707
|
+
console.log();
|
|
5708
|
+
return r.failed?.length ? 1 : 0;
|
|
5576
5709
|
}
|
|
5577
5710
|
|
|
5578
5711
|
/**
|
|
@@ -5595,18 +5728,28 @@ async function accountCmd(positional, flags) {
|
|
|
5595
5728
|
if (!verb) {
|
|
5596
5729
|
const { defaults } = await remoteCall(url, flags, "/api/account");
|
|
5597
5730
|
printDefaults(defaults ?? {});
|
|
5731
|
+
const halt = await pauseState(remoteCall(url, flags, "/api/account/pause"));
|
|
5732
|
+
const own = Object.keys(halt?.workspaces ?? {});
|
|
5733
|
+
if (halt) console.log(` paused ${halt.account ? c.red(pauseLine(halt.account)) : c.dim("no")}${own.length ? c.dim(` · on their own: ${own.join(", ")}`) : ""}`);
|
|
5598
5734
|
console.log(`\n ${c.dim("foldrun account set timezone Australia/Sydney · set notify email you@example.com · set budget 60/month · clear budget")}\n`);
|
|
5599
5735
|
return 0;
|
|
5600
5736
|
}
|
|
5601
5737
|
if (verb === "providers") return providersCmd(url, flags);
|
|
5738
|
+
if (verb === "telemetry") return telemetryCmd(url, positional.slice(1), flags);
|
|
5739
|
+
if (verb === "pause" || verb === "resume") {
|
|
5740
|
+
return pauseCmd(flags, verb, "every workspace in the account", {
|
|
5741
|
+
post: (body) => remoteCall(url, flags, "/api/account/pause", { method: "POST", body }),
|
|
5742
|
+
del: () => remoteCall(url, flags, "/api/account/pause", { method: "DELETE" }),
|
|
5743
|
+
});
|
|
5744
|
+
}
|
|
5602
5745
|
if (verb === "model") return accountModelCmd(url, positional.slice(1), flags);
|
|
5603
5746
|
if (verb === "export") return accountExportCmd(url, flags);
|
|
5604
5747
|
if (verb !== "set" && verb !== "clear") {
|
|
5605
|
-
throw new Error(`unknown account verb "${verb}" — \`foldrun account\` shows them, \`set\` and \`clear\` change one, \`model\` is the account's own model key, \`providers\` checks the model keys, \`export\` downloads everything`);
|
|
5748
|
+
throw new Error(`unknown account verb "${verb}" — \`foldrun account\` shows them, \`set\` and \`clear\` change one, \`model\` is the account's own model key, \`providers\` checks the model keys, \`export\` downloads everything, \`pause\`/\`resume\` the kill switch`);
|
|
5606
5749
|
}
|
|
5607
5750
|
|
|
5608
5751
|
const key = positional[1];
|
|
5609
|
-
const KEYS = ["timezone", "notify", "budget", "concurrency"];
|
|
5752
|
+
const KEYS = ["timezone", "notify", "budget", "concurrency", "deploy_block", "network_enforce"];
|
|
5610
5753
|
if (!KEYS.includes(key)) {
|
|
5611
5754
|
throw new Error(`which setting? \`foldrun account ${verb} <${KEYS.join("|")}> …\``);
|
|
5612
5755
|
}
|
|
@@ -5647,7 +5790,13 @@ async function accountCmd(positional, flags) {
|
|
|
5647
5790
|
if (key === "concurrency" && !/^\d+$/.test(value)) {
|
|
5648
5791
|
throw new Error(`concurrency is a whole number of runs at once, e.g. \`foldrun account set concurrency 4\` — not "${value}"`);
|
|
5649
5792
|
}
|
|
5650
|
-
|
|
5793
|
+
// deploy_block (FOL-58): none | secrets | security.
|
|
5794
|
+
if (key === "deploy_block" && !["none", "secrets", "security"].includes(value)) {
|
|
5795
|
+
throw new Error(`deploy_block is none, secrets or security — e.g. \`foldrun account set deploy_block secrets\` — not "${value}"`);
|
|
5796
|
+
}
|
|
5797
|
+
patch = {
|
|
5798
|
+
[key]: key === "concurrency" ? Number(value) : key === "network_enforce" ? onOff(key, value, "foldrun account set network_enforce on") : value,
|
|
5799
|
+
};
|
|
5651
5800
|
}
|
|
5652
5801
|
|
|
5653
5802
|
const answer = await remoteCall(url, flags, "/api/account", { method: "PATCH", body: JSON.stringify(patch) });
|
|
@@ -5929,6 +6078,127 @@ const eventSteps = (run) =>
|
|
|
5929
6078
|
* steps in one group is two decisions, and a list that folded them into one
|
|
5930
6079
|
* line named one agent and asked neither question.
|
|
5931
6080
|
*/
|
|
6081
|
+
/** `foldrun runs views` — your saved run searches (FOL-72 step 9). */
|
|
6082
|
+
async function runViewsCmd(flags) {
|
|
6083
|
+
const url = platformFor(flags, "runs");
|
|
6084
|
+
const { runViews = [] } = await remoteCall(url, flags, "/api/me/preferences");
|
|
6085
|
+
if (flags.json === true) return console.log(JSON.stringify(runViews, null, 2)), 0;
|
|
6086
|
+
if (!runViews.length) return console.log(`\n ${c.dim("no saved views — save one from the runs page's search")}\n`), 0;
|
|
6087
|
+
console.log();
|
|
6088
|
+
for (const v of runViews) console.log(` ${c.bold(v.name)} ${c.dim(v.query)}`);
|
|
6089
|
+
console.log(`\n ${c.dim("foldrun runs --view <name> opens one")}\n`);
|
|
6090
|
+
return 0;
|
|
6091
|
+
}
|
|
6092
|
+
|
|
6093
|
+
// ---------------------------------------------------------------- insights
|
|
6094
|
+
|
|
6095
|
+
/** `foldrun insights` (FOL-72 step 8): recurring problems found in the runs,
|
|
6096
|
+
* with evidence, what they cost and what to do — GET /api/insights. */
|
|
6097
|
+
async function insightsCmd(flags) {
|
|
6098
|
+
const url = platformFor(flags, "insights");
|
|
6099
|
+
const qs = new URLSearchParams({ ...(flags.days !== undefined ? { days: String(flags.days) } : {}), ...(typeof flags.to === "string" ? { workspace: flags.to } : {}) });
|
|
6100
|
+
const { insights = [], days = 7 } = await remoteCall(url, flags, `/api/insights${qs.size ? `?${qs}` : ""}`);
|
|
6101
|
+
if (flags.json === true) return console.log(JSON.stringify(insights, null, 2)), 0;
|
|
6102
|
+
if (!insights.length) {
|
|
6103
|
+
console.log(`\n ${c.dim(`nothing recurring in the last ${days} days`)}\n`);
|
|
6104
|
+
return 0;
|
|
6105
|
+
}
|
|
6106
|
+
console.log();
|
|
6107
|
+
for (const i of insights) {
|
|
6108
|
+
const cost = i.credits ? ` · about ${i.credits.toLocaleString()} credits` : "";
|
|
6109
|
+
console.log(` ${c.amber("◆")} ${c.bold(i.title)} ${c.dim(`${i.workspace} · ${i.steps} step${i.steps === 1 ? "" : "s"}${cost}`)}`);
|
|
6110
|
+
for (const line of wrap(i.why, 84)) console.log(` ${c.dim(line)}`);
|
|
6111
|
+
for (const [k, line] of wrap(i.fix, 80).entries()) console.log(` ${k === 0 ? "→ " : " "}${line}`);
|
|
6112
|
+
console.log(` ${c.dim(i.evidence.map((e) => `${e.runId}#${e.step}`).join(" "))}`);
|
|
6113
|
+
console.log();
|
|
6114
|
+
}
|
|
6115
|
+
return 0;
|
|
6116
|
+
}
|
|
6117
|
+
|
|
6118
|
+
// ---------------------------------------------------------------- issues
|
|
6119
|
+
|
|
6120
|
+
const ISSUE_VERBS = { ack: "acknowledge", acknowledge: "acknowledge", snooze: "snooze", resolve: "resolve", reopen: "reopen" };
|
|
6121
|
+
|
|
6122
|
+
/** What the run doctor reads for an issue: its newest run, that run's
|
|
6123
|
+
* workspace, and the question the run page's Doctor asks — core's
|
|
6124
|
+
* issueDoctor (issues.ts), repeated here as the cache note is until the
|
|
6125
|
+
* next core release. Null for a missed schedule, which has no run. */
|
|
6126
|
+
function issueDoctor(issue) {
|
|
6127
|
+
const run = (issue.runs ?? []).find((r) => !r.missed);
|
|
6128
|
+
if (!run || issue.kind === "missed") return null;
|
|
6129
|
+
const ended = issue.kind === "blocked" ? "end BLOCKED" : issue.kind === "bad" ? "end BAD" : "fail";
|
|
6130
|
+
return { workspace: run.workspace, runId: run.id, prompt: `Doctor: why did run ${run.id} ${ended}, and how do I fix it?` };
|
|
6131
|
+
}
|
|
6132
|
+
|
|
6133
|
+
/** `foldrun issues` (FOL-54 step 3): what went wrong, grouped, open until a
|
|
6134
|
+
* person resolves it; `foldrun issues ack|snooze|resolve|reopen <id>` acts
|
|
6135
|
+
* on one — the dashboard's Issues page and its buttons, same route.
|
|
6136
|
+
* `foldrun issues doctor <id>` is the card's Doctor: the run doctor on the
|
|
6137
|
+
* issue's newest run, in that run's workspace. */
|
|
6138
|
+
async function issuesCmd(positional, flags, layout) {
|
|
6139
|
+
const url = platformFor(flags, "issues");
|
|
6140
|
+
const [verb, id] = positional;
|
|
6141
|
+
if (verb === "doctor") {
|
|
6142
|
+
if (!id || !/^iss_[a-z0-9]{2,32}$/.test(id)) throw new Error("`foldrun issues doctor <issue-id>` — the id is the iss_… on each line of `foldrun issues`");
|
|
6143
|
+
const { issue } = await remoteCall(url, flags, `/api/issues/${id}`);
|
|
6144
|
+
const doc = issueDoctor(issue);
|
|
6145
|
+
if (!doc) throw new Error(`${id} is a missed schedule: no run started, so there is nothing for the doctor to read — open the flow's schedule instead`);
|
|
6146
|
+
return assistantCmd([doc.prompt], { ...flags, to: doc.workspace }, layout);
|
|
6147
|
+
}
|
|
6148
|
+
if (verb && verb !== "show") {
|
|
6149
|
+
const action = ISSUE_VERBS[verb];
|
|
6150
|
+
if (!action) throw new Error(`\`foldrun issues\` lists them; its verbs are show, doctor, ack, snooze, resolve, reopen — not ${verb}`);
|
|
6151
|
+
if (!id || !/^iss_[a-z0-9]{2,32}$/.test(id)) throw new Error(`\`foldrun issues ${verb} <issue-id>\` — the id is the iss_… on each line of \`foldrun issues\``);
|
|
6152
|
+
if (action === "snooze" && typeof flags.for !== "string" && typeof flags.until !== "string" && flags.times === undefined) throw new Error("a snooze takes --for 30m|4h|1d|1w, --until <ISO time>, or --times <n> (until it happens n more times)");
|
|
6153
|
+
const body = {
|
|
6154
|
+
action,
|
|
6155
|
+
...(typeof flags.for === "string" ? { for: flags.for } : {}),
|
|
6156
|
+
...(typeof flags.until === "string" ? { until: flags.until } : {}),
|
|
6157
|
+
...(flags.times !== undefined ? { times: Number(flags.times) } : {}),
|
|
6158
|
+
...(typeof flags.note === "string" ? { note: flags.note } : {}),
|
|
6159
|
+
};
|
|
6160
|
+
const { issue } = await remoteCall(url, flags, `/api/issues/${id}`, { method: "POST", body: JSON.stringify(body) });
|
|
6161
|
+
if (flags.json === true) return console.log(JSON.stringify(issue, null, 2)), 0;
|
|
6162
|
+
console.log(`\n ${c.green("✓")} ${issue.id} is ${issue.status}${issue.snoozedUntil ? ` until ${when(issue.snoozedUntil)}` : ""}${issue.snoozedFor ? ` for ${issue.snoozedFor} more` : ""} — ${issue.what}\n`);
|
|
6163
|
+
return 0;
|
|
6164
|
+
}
|
|
6165
|
+
if (verb === "show") {
|
|
6166
|
+
if (!id) throw new Error("`foldrun issues show <issue-id>`");
|
|
6167
|
+
const { issue } = await remoteCall(url, flags, `/api/issues/${id}`);
|
|
6168
|
+
if (flags.json === true) return console.log(JSON.stringify(issue, null, 2)), 0;
|
|
6169
|
+
printIssue(issue, true);
|
|
6170
|
+
return 0;
|
|
6171
|
+
}
|
|
6172
|
+
const status = typeof flags.status === "string" ? flags.status : flags.all === true ? "all" : "active";
|
|
6173
|
+
const qs = new URLSearchParams({ status, ...(typeof flags.to === "string" ? { workspace: flags.to } : {}) });
|
|
6174
|
+
const { issues = [] } = await remoteCall(url, flags, `/api/issues?${qs}`);
|
|
6175
|
+
if (flags.json === true) return console.log(JSON.stringify(issues, null, 2)), 0;
|
|
6176
|
+
if (!issues.length) {
|
|
6177
|
+
console.log(`\n ${c.dim(status === "active" ? "no open issues" : `no ${status} issues`)}\n`);
|
|
6178
|
+
return 0;
|
|
6179
|
+
}
|
|
6180
|
+
console.log();
|
|
6181
|
+
for (const i of issues) printIssue(i, false);
|
|
6182
|
+
console.log(` ${c.dim("foldrun issues show <id> for its runs and history; doctor <id> asks the run doctor why; ack, snooze --for 1d, resolve or reopen <id> act on one; --all lists resolved too")}\n`);
|
|
6183
|
+
return 0;
|
|
6184
|
+
}
|
|
6185
|
+
|
|
6186
|
+
function printIssue(i, full) {
|
|
6187
|
+
const mark = i.kind === "platform" ? c.bold("▲") : i.kind === "failed" ? c.red("✗") : i.kind === "blocked" ? c.red("⛔") : i.kind === "missed" ? c.amber("◷") : c.amber("!");
|
|
6188
|
+
const where = i.kind === "platform" ? i.workspaces.join(", ") : `${i.workspace} · ${i.what}${i.agent ? ` at ${i.agent}` : ""}`;
|
|
6189
|
+
const label = i.kind === "missed" ? "missed schedule" : i.kind;
|
|
6190
|
+
const state = (i.status === "open" ? "" : c.dim(` [${i.status}${i.snoozedUntil ? ` until ${when(i.snoozedUntil)}` : ""}${i.snoozedFor ? ` for ${i.snoozedFor} more` : ""}${i.resolved?.by?.startsWith("auto:") ? ", auto" : ""}]`)) + (i.regressed ? ` ${c.red(`regressed${i.sinceRevision ? ` since ${String(i.sinceRevision).slice(0, 7)}` : ""}`)}` : "");
|
|
6191
|
+
console.log(` ${mark} ${c.bold(i.kind === "platform" ? i.what : label)} ${where}${state} ${c.dim(i.id)}`);
|
|
6192
|
+
console.log(` ${c.dim(`${i.count}× · first ${ago(i.firstSeen)} ago · latest ${ago(i.lastSeen)} ago${i.recoveredAt ? ` · fine for ${ago(i.recoveredAt)}` : ""}`)}`);
|
|
6193
|
+
for (const line of wrap(i.reason, 84).slice(0, full ? 20 : 2)) console.log(` ${line}`);
|
|
6194
|
+
console.log(` ${c.dim("→")} ${i.action}`);
|
|
6195
|
+
if (full) {
|
|
6196
|
+
console.log(` ${c.dim(`runs: ${i.runs.map((r) => `${r.workspace}/${r.id}`).join(", ")}`)}`);
|
|
6197
|
+
for (const e of i.history) console.log(` ${c.dim(`${when(e.at)} ${e.action} by ${e.by}${e.until ? ` until ${when(e.until)}` : ""}${e.note ? ` — ${e.note}` : ""}`)}`);
|
|
6198
|
+
}
|
|
6199
|
+
console.log();
|
|
6200
|
+
}
|
|
6201
|
+
|
|
5932
6202
|
async function approvalsCmd(flags, layout) {
|
|
5933
6203
|
const url = platformFor(flags, "approvals");
|
|
5934
6204
|
const only = typeof flags.to === "string" ? flags.to : null;
|
|
@@ -6151,6 +6421,23 @@ async function stopCmd(runId, flags, layout) {
|
|
|
6151
6421
|
*/
|
|
6152
6422
|
async function runsCmd(flags, layout) {
|
|
6153
6423
|
const url = platformFor(flags, "runs");
|
|
6424
|
+
// --view <name>: a saved search (the dashboard's) read first, so every
|
|
6425
|
+
// filter in it — status, since, workspace, flow, q and the search's own —
|
|
6426
|
+
// applies as if typed; a flag typed too wins (Codex review).
|
|
6427
|
+
if (typeof flags.view === "string") {
|
|
6428
|
+
const { runViews = [] } = await remoteCall(url, flags, "/api/me/preferences");
|
|
6429
|
+
const view = runViews.find((v) => v.name === flags.view);
|
|
6430
|
+
if (!view) throw new Error(`no saved view called ${flags.view}${runViews.length ? ` — yours: ${runViews.map((v) => v.name).join(", ")}` : " — save one on the runs page"}`);
|
|
6431
|
+
const as = { workspace: "to", min_tokens: "min-tokens", min_secs: "min-secs" };
|
|
6432
|
+
const fromView = {};
|
|
6433
|
+
for (const [k, v] of new URLSearchParams(view.query)) {
|
|
6434
|
+
if (k === "tool_error") fromView["tool-error"] = v === "1" || v === "true";
|
|
6435
|
+
// The dashboard's Today chip is its 24-hour window (Codex review).
|
|
6436
|
+
else if (k === "since" && v === "today") fromView.since = "24h";
|
|
6437
|
+
else fromView[as[k] ?? k] = v;
|
|
6438
|
+
}
|
|
6439
|
+
flags = { ...fromView, ...flags };
|
|
6440
|
+
}
|
|
6154
6441
|
const names = typeof flags.to === "string" ? [flags.to] : await remoteWorkspaceNames(url, flags);
|
|
6155
6442
|
if (!names.length) {
|
|
6156
6443
|
console.log(`\n ${c.dim(`no workspaces on ${url}`)}\n`);
|
|
@@ -6162,6 +6449,17 @@ async function runsCmd(flags, layout) {
|
|
|
6162
6449
|
typeof flags.status === "string" ? new Set(flags.status.split(",").map((s) => s.trim()).filter(Boolean)) : null;
|
|
6163
6450
|
const verdicts =
|
|
6164
6451
|
typeof flags.verdict === "string" ? new Set(flags.verdict.split(",").map((s) => s.trim().toUpperCase()).filter(Boolean)) : null;
|
|
6452
|
+
// --outcome (FOL-54): the one word every surface shows — needs-you is the
|
|
6453
|
+
// dashboard's name for waiting. A platform from before it sends none.
|
|
6454
|
+
const OUTCOME_WORDS = ["running", "queued", "waiting", "good", "done", "quiet", "blocked", "bad", "failed", "stopped"];
|
|
6455
|
+
const outcomes =
|
|
6456
|
+
typeof flags.outcome === "string"
|
|
6457
|
+
? new Set(flags.outcome.split(",").map((s) => s.trim().toLowerCase().replace(/^needs-?you$/, "waiting")).filter(Boolean))
|
|
6458
|
+
: null;
|
|
6459
|
+
if (outcomes) {
|
|
6460
|
+
const unknown = [...outcomes].filter((o) => !OUTCOME_WORDS.includes(o));
|
|
6461
|
+
if (unknown.length) throw new Error(`--outcome takes ${OUTCOME_WORDS.join(", ")} (or needs-you) — not ${unknown.join(", ")}`);
|
|
6462
|
+
}
|
|
6165
6463
|
|
|
6166
6464
|
// Each desk is asked for its own newest rows before the merge, so one
|
|
6167
6465
|
// busy workspace cannot crowd every quiet one out of the answer. With a
|
|
@@ -6169,11 +6467,23 @@ async function runsCmd(flags, layout) {
|
|
|
6169
6467
|
// the filter runs here, after the fetch, and a desk with twenty recent
|
|
6170
6468
|
// successes was hiding its older failures from `--status failed` because
|
|
6171
6469
|
// only its newest twenty of any status ever arrived.
|
|
6172
|
-
|
|
6470
|
+
// Search (FOL-72 step 9): by what happened inside a run, filtered by the
|
|
6471
|
+
// platform; --view <name> starts from a saved search (the dashboard's).
|
|
6472
|
+
const search = new URLSearchParams();
|
|
6473
|
+
for (const [flag, key] of [["tool", "tool"], ["agent", "agent"], ["model", "model"], ["min-tokens", "min_tokens"], ["min-secs", "min_secs"]]) {
|
|
6474
|
+
if (flags[flag] !== undefined && flags[flag] !== true) search.set(key, String(flags[flag]));
|
|
6475
|
+
}
|
|
6476
|
+
if (flags["tool-error"] === true) search.set("tool_error", "1");
|
|
6477
|
+
const searching = [...search.keys()].some((k) => ["tool", "tool_error", "agent", "model", "min_tokens", "min_secs"].includes(k));
|
|
6478
|
+
// A view's flow or text filters here, after the fetch: ask for the ceiling
|
|
6479
|
+
// as for a status, or matches past the newest few go missing (Codex review).
|
|
6480
|
+
const localText = typeof flags.flow === "string" || typeof flags.q === "string";
|
|
6481
|
+
const perWorkspace = wanted || verdicts || outcomes || cutoff !== null || searching || localText ? 500 : limit;
|
|
6173
6482
|
const unreachable = [];
|
|
6174
6483
|
const lists = await pool(names, 8, async (ws) => {
|
|
6175
6484
|
try {
|
|
6176
|
-
const
|
|
6485
|
+
const forApi = new URLSearchParams([...search].filter(([k]) => ["tool", "tool_error", "agent", "model", "min_tokens", "min_secs"].includes(k)));
|
|
6486
|
+
const { runs = [] } = await remoteCall(url, flags, `/api/workspaces/${ws}/runs?limit=${perWorkspace}${forApi.size ? `&${forApi}` : ""}`);
|
|
6177
6487
|
return runs.map((r) => ({ ...r, workspace: ws }));
|
|
6178
6488
|
} catch (err) {
|
|
6179
6489
|
unreachable.push([ws, explain(err)]);
|
|
@@ -6183,14 +6493,34 @@ async function runsCmd(flags, layout) {
|
|
|
6183
6493
|
|
|
6184
6494
|
const rows = lists
|
|
6185
6495
|
.flat()
|
|
6186
|
-
|
|
6496
|
+
// The status the dashboard shows: a stopped run is "stopped", not the
|
|
6497
|
+
// "failed" its record says (Codex review — a saved status=stopped view).
|
|
6498
|
+
.filter((r) => (wanted ? wanted.has(r.outcome === "stopped" ? "stopped" : r.status) : true))
|
|
6187
6499
|
.filter((r) => (verdicts ? r.status === "completed" && verdicts.has(verdictOf(r)) : true))
|
|
6500
|
+
.filter((r) => (outcomes ? typeof r.outcome === "string" && outcomes.has(r.outcome) : true))
|
|
6501
|
+
// A view's flow and free text (the runs page's): "agent: x" is a direct run.
|
|
6502
|
+
// The dashboard names a run by what ran (whatRan): a direct agent run's
|
|
6503
|
+
// or an eval's own name, without adhoc:/eval: (Codex review).
|
|
6504
|
+
.filter((r) => (typeof flags.flow === "string" ? r.flow === flags.flow || r.flow.replace(/^(adhoc|eval):/, "") === flags.flow : true))
|
|
6505
|
+
// The runs page's text search, field for field (its runSearchText): every
|
|
6506
|
+
// word, any order, across id, what ran, workspace, the status it shows,
|
|
6507
|
+
// verdict, summary and the failure (Codex review).
|
|
6508
|
+
.filter((r) => {
|
|
6509
|
+
if (typeof flags.q !== "string") return true;
|
|
6510
|
+
const words = flags.q.toLowerCase().replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
|
|
6511
|
+
const shown = r.outcome === "stopped" ? "stopped" : r.status;
|
|
6512
|
+
const hay = [r.id, r.flow.replace(/^(adhoc|eval):/, ""), r.workspace, shown, verdictOf(r) ?? "", r.summary ?? "", r.failure ? `${r.failure.agent} ${r.failure.reason}` : ""]
|
|
6513
|
+
.join(" ")
|
|
6514
|
+
.toLowerCase()
|
|
6515
|
+
.replace(/\s+/g, " ");
|
|
6516
|
+
return words.every((w) => hay.includes(w));
|
|
6517
|
+
})
|
|
6188
6518
|
.filter((r) => (cutoff === null ? true : Date.parse(r.startedAt) >= cutoff))
|
|
6189
6519
|
.sort((a, b) => (a.startedAt < b.startedAt ? 1 : -1))
|
|
6190
6520
|
.slice(0, limit);
|
|
6191
6521
|
|
|
6192
6522
|
if (!rows.length) {
|
|
6193
|
-
const how = [wanted ? `status ${[...wanted].join(", ")}` : null, verdicts ? `verdict ${[...verdicts].join(", ")}` : null, flags.since ? `the last ${flags.since}` : null]
|
|
6523
|
+
const how = [wanted ? `status ${[...wanted].join(", ")}` : null, verdicts ? `verdict ${[...verdicts].join(", ")}` : null, outcomes ? `outcome ${[...outcomes].join(", ")}` : null, flags.since ? `the last ${flags.since}` : null]
|
|
6194
6524
|
.filter(Boolean)
|
|
6195
6525
|
.join(" in ");
|
|
6196
6526
|
console.log(`\n ${c.dim(`no runs${how ? ` matching ${how}` : ""}${flags.to ? ` in ${flags.to}` : ""}`)}\n`);
|
|
@@ -6210,7 +6540,9 @@ async function runsCmd(flags, layout) {
|
|
|
6210
6540
|
verdictWidth: (r.status === "completed" ? verdictOf(r) ?? "" : "").length,
|
|
6211
6541
|
steps: `${done}/${(r.steps ?? []).length}`,
|
|
6212
6542
|
took: runDuration(r),
|
|
6213
|
-
|
|
6543
|
+
// FOL-54: credits charged, as the dashboard shows; a run not settled
|
|
6544
|
+
// yet (or a platform from before chargedUsd) shows its token cost, in dollars.
|
|
6545
|
+
cost: typeof r.chargedUsd === "number" ? `${Math.round(r.chargedUsd * 100)} credits` : `$${runCost(r).toFixed(2)}`,
|
|
6214
6546
|
id: r.id,
|
|
6215
6547
|
summary: firstLine(r.summary, 110),
|
|
6216
6548
|
};
|
|
@@ -6354,6 +6686,28 @@ function measuredShare(list) {
|
|
|
6354
6686
|
return all > 0 ? read / all : null;
|
|
6355
6687
|
}
|
|
6356
6688
|
const cachedLabel = (rate) => (rate == null ? null : `${Math.round(rate * 100)}% cached`);
|
|
6689
|
+
/** Why a step's prompt cache missed — core's rule (tokens.ts promptCacheNote),
|
|
6690
|
+
* repeated here as `cachedShare` is: a step that wrote a fifth or more of its
|
|
6691
|
+
* input to the cache says which layer of its prompt changed since the same
|
|
6692
|
+
* agent's previous step in the run. Nothing for a step without layers on
|
|
6693
|
+
* record (before 20 Oct 2026) or one the cache served. */
|
|
6694
|
+
function promptCacheNote(steps, index) {
|
|
6695
|
+
const step = steps[index];
|
|
6696
|
+
const t = step?.tokens;
|
|
6697
|
+
if (!step?.promptLayers || !t || t.cacheWrite === undefined || !(t.input > 0)) return null;
|
|
6698
|
+
const share = t.cacheWrite / t.input;
|
|
6699
|
+
if (share < 0.2) return null;
|
|
6700
|
+
const head = `${Math.round(share * 100)}% of input written to the cache`;
|
|
6701
|
+
let prev = -1;
|
|
6702
|
+
for (let j = index - 1; j >= 0; j--) if (steps[j].agent === step.agent && steps[j].promptLayers) { prev = j; break; }
|
|
6703
|
+
if (prev < 0) return `${head} — the agent's first step in this run`;
|
|
6704
|
+
const before = new Map(steps[prev].promptLayers.map((l) => [l.name, l.hash]));
|
|
6705
|
+
const after = new Map(step.promptLayers.map((l) => [l.name, l.hash]));
|
|
6706
|
+
const changed = [...after].filter(([name, hash]) => before.get(name) !== hash).map(([name]) => name);
|
|
6707
|
+
for (const name of before.keys()) if (!after.has(name)) changed.push(name);
|
|
6708
|
+
if (!changed.length) return `${head} — the same prompt as step ${prev + 1}; the entry had expired or was still being written`;
|
|
6709
|
+
return `${head} — ${changed.join(", ")} changed since step ${prev + 1}`;
|
|
6710
|
+
}
|
|
6357
6711
|
function sumTokens(list) {
|
|
6358
6712
|
const out = { input: 0, output: 0 };
|
|
6359
6713
|
for (const t of list) {
|
|
@@ -6368,10 +6722,86 @@ function sumTokens(list) {
|
|
|
6368
6722
|
return out;
|
|
6369
6723
|
}
|
|
6370
6724
|
|
|
6725
|
+
/** `foldrun report <run-id> call <call-id>` — one tool call's recorded input
|
|
6726
|
+
* and output, secrets masked (FOL-54 step 4). */
|
|
6727
|
+
async function reportCallCmd(runId, callId, flags, layout) {
|
|
6728
|
+
const url = platformFor(flags, "report");
|
|
6729
|
+
if (!runId || !callId) throw new Error("`foldrun report <run-id> call <call-id>` — `foldrun report <run-id> --calls` lists the recorded calls");
|
|
6730
|
+
const ws = await workspaceOfRun(url, flags, layout, runId);
|
|
6731
|
+
const { call } = await remoteCall(url, flags, `/api/workspaces/${ws}/runs/${runId}/calls/${encodeURIComponent(callId)}`);
|
|
6732
|
+
if (flags.json === true) return console.log(JSON.stringify(call, null, 2)), 0;
|
|
6733
|
+
console.log(`\n ${c.bold(call.tool)} ${c.dim(call.id)}`);
|
|
6734
|
+
for (const [label, text, whole, wasCut] of [["input", call.input, call.inputChars, call.inputCut], ["output", call.output, call.outputChars, call.outputCut]]) {
|
|
6735
|
+
if (text === undefined) continue;
|
|
6736
|
+
const cut = wasCut === true && typeof whole === "number" && whole > text.length ? c.dim(` (the first ${text.length.toLocaleString()} of ${whole.toLocaleString()} characters)`) : "";
|
|
6737
|
+
console.log(`\n ${c.dim(label)}${cut}`);
|
|
6738
|
+
for (const line of String(text).split("\n")) console.log(` ${line}`);
|
|
6739
|
+
}
|
|
6740
|
+
console.log();
|
|
6741
|
+
return 0;
|
|
6742
|
+
}
|
|
6743
|
+
|
|
6744
|
+
/** `foldrun report <run-id> --tree` — the run as one tree, run → step →
|
|
6745
|
+
* model call → tool call, from GET …/runs/<id>/trace (FOL-72 step 5). */
|
|
6746
|
+
async function reportTreeCmd(url, flags, ws, runId) {
|
|
6747
|
+
const { tree, failure = [] } = await remoteCall(url, flags, `/api/workspaces/${ws}/runs/${runId}/trace`);
|
|
6748
|
+
if (flags.json === true) return console.log(JSON.stringify({ tree, failure }, null, 2)), 0;
|
|
6749
|
+
// The deepest span on the way to the failure is the one to point at.
|
|
6750
|
+
const culprit = failure.length ? failure[failure.length - 1] : null;
|
|
6751
|
+
const k = (n) => (n >= 1000 ? `${(n / 1000).toFixed(n >= 100_000 ? 0 : 1)}k` : String(n));
|
|
6752
|
+
const mark = { run: "▣", step: "◆", model: "✦", tool: "→", error: "✗" };
|
|
6753
|
+
const lines = [];
|
|
6754
|
+
const walk = (s, prefix, last, root) => {
|
|
6755
|
+
const dur = s.start != null && s.end != null ? humanDuration(Math.max(0, s.end - s.start)) : "";
|
|
6756
|
+
const facts = [
|
|
6757
|
+
s.kind === "model" && s.tokens ? `${k(s.tokens.input)} in · ${k(s.tokens.output)} out` : null,
|
|
6758
|
+
s.stop ?? null,
|
|
6759
|
+
s.subagent ? `↳ ${s.subagent}` : null,
|
|
6760
|
+
s.kind === "tool" && s.captured && s.call ? s.call : null,
|
|
6761
|
+
].filter(Boolean);
|
|
6762
|
+
const text = s.kind === "error" ? firstLine(s.text ?? "", 90) : s.label;
|
|
6763
|
+
const head = `${root ? "" : `${prefix}${last ? "└ " : "├ "}`}${mark[s.kind] ?? "·"} ${s.status === "error" ? c.red(text) : s.kind === "step" || s.kind === "run" ? c.bold(text) : text}`;
|
|
6764
|
+
lines.push(` ${head}${facts.length ? ` ${c.dim(facts.join(" · "))}` : ""}${dur && s.kind !== "error" ? ` ${c.dim(dur)}` : ""}${s.id === culprit ? ` ${c.red("← what went wrong")}` : ""}`);
|
|
6765
|
+
const kids = s.children ?? [];
|
|
6766
|
+
kids.forEach((ch, i) => walk(ch, root ? "" : `${prefix}${last ? " " : "│ "}`, i === kids.length - 1, false));
|
|
6767
|
+
};
|
|
6768
|
+
walk(tree, "", true, true);
|
|
6769
|
+
console.log(`\n${lines.join("\n")}\n`);
|
|
6770
|
+
if ((tree.children ?? []).some((s) => (s.children ?? []).some((ch) => (ch.children ?? []).some((t) => t.captured)))) console.log(` ${c.dim(`\`foldrun report ${runId} call <id>\` shows a call's input and output`)}\n`);
|
|
6771
|
+
return 0;
|
|
6772
|
+
}
|
|
6773
|
+
|
|
6774
|
+
/** `foldrun report <run-id> --compare [<other>]` — this run beside the flow's
|
|
6775
|
+
* last good run (or the one named), from GET …/compare (FOL-72 step 6). */
|
|
6776
|
+
async function reportCompareCmd(url, flags, ws, runId) {
|
|
6777
|
+
const other = typeof flags.compare === "string" ? flags.compare : null;
|
|
6778
|
+
const { comparison: cmp, chosen } = await remoteCall(url, flags, `/api/workspaces/${ws}/runs/${runId}/compare${other ? `?with=${encodeURIComponent(other)}` : ""}`);
|
|
6779
|
+
if (flags.json === true) return console.log(JSON.stringify({ comparison: cmp, chosen }, null, 2)), 0;
|
|
6780
|
+
console.log(`\n ${c.bold(runId)} ${c.dim(`(${cmp.run.status})`)} beside ${c.bold(cmp.base.id)} ${c.dim(`(${chosen}, ${cmp.base.status})`)}`);
|
|
6781
|
+
const files = !cmp.revisionChanged
|
|
6782
|
+
? "both ran the same version of the files"
|
|
6783
|
+
: cmp.changedFiles === null
|
|
6784
|
+
? "they ran different versions of the files"
|
|
6785
|
+
: cmp.changedFiles.length
|
|
6786
|
+
? `files changed between them: ${cmp.changedFiles.slice(0, 8).join(", ")}${cmp.changedFiles.length > 8 ? ` and ${cmp.changedFiles.length - 8} more` : ""}`
|
|
6787
|
+
: "different revisions, no file changes between them";
|
|
6788
|
+
console.log(` ${c.dim(files)}\n`);
|
|
6789
|
+
const changed = cmp.steps.filter((s) => s.changes.length);
|
|
6790
|
+
if (!changed.length) console.log(` ${c.dim("no step differs in status, instruction, model, tools, errors, time or tokens")}`);
|
|
6791
|
+
for (const s of changed) {
|
|
6792
|
+
console.log(` ${c.bold(`step ${s.step} · ${s.agent}`)}`);
|
|
6793
|
+
for (const ch of s.changes) console.log(` ${/^(status|new error|tool errors)/.test(ch) ? c.red("•") : c.dim("•")} ${ch}`);
|
|
6794
|
+
}
|
|
6795
|
+
console.log();
|
|
6796
|
+
return 0;
|
|
6797
|
+
}
|
|
6798
|
+
|
|
6371
6799
|
async function reportCmd(runId, flags, layout) {
|
|
6372
6800
|
const url = platformFor(flags, "report");
|
|
6373
6801
|
if (!runId) throw new Error("which run? `foldrun report <run-id>` — `foldrun runs` lists them");
|
|
6374
6802
|
const ws = await workspaceOfRun(url, flags, layout, runId);
|
|
6803
|
+
if (flags.tree === true) return reportTreeCmd(url, flags, ws, runId);
|
|
6804
|
+
if (flags.compare !== undefined) return reportCompareCmd(url, flags, ws, runId);
|
|
6375
6805
|
const run = await remoteCall(url, flags, `/api/workspaces/${ws}/runs/${runId}`);
|
|
6376
6806
|
|
|
6377
6807
|
if (flags.json === true) {
|
|
@@ -6391,6 +6821,9 @@ async function reportCmd(runId, flags, layout) {
|
|
|
6391
6821
|
console.log(` ${c.dim(`${tokens.input.toLocaleString()} in / ${tokens.output.toLocaleString()} out tokens${cached ? ` · ${cached}` : ""}`)}`);
|
|
6392
6822
|
}
|
|
6393
6823
|
if (run.budgetUsd) console.log(` ${c.dim(`budget $${Number(run.budgetUsd).toFixed(2)}`)}`);
|
|
6824
|
+
// Which files it ran and how long it waited for a worker (FOL-54 step 4).
|
|
6825
|
+
const ranOn = [run.revision ? `ran on ${String(run.revision).slice(0, 7)}` : null, run.drivenAt ? `queued ${humanDuration(Math.max(0, Date.parse(run.drivenAt) - Date.parse(run.startedAt)))}` : null].filter(Boolean);
|
|
6826
|
+
if (ranOn.length) console.log(` ${c.dim(ranOn.join(" · "))}`);
|
|
6394
6827
|
if (run.summary) {
|
|
6395
6828
|
console.log();
|
|
6396
6829
|
for (const line of wrap(run.summary, 84)) console.log(` ${line}`);
|
|
@@ -6413,6 +6846,10 @@ async function reportCmd(runId, flags, layout) {
|
|
|
6413
6846
|
console.log(
|
|
6414
6847
|
` ${statusMark(step.status)} ${pad(`${i + 1}.`, 4)}${c.dim(`g${step.group ?? 1}`)} ${c.bold(step.agent)} ${c.dim(bits.join(" · "))}`,
|
|
6415
6848
|
);
|
|
6849
|
+
// A step the prompt cache did not serve says why — which layer of its
|
|
6850
|
+
// prompt changed since the agent's last step — the way the run page does.
|
|
6851
|
+
const cacheNote = promptCacheNote(run.steps ?? [], i);
|
|
6852
|
+
if (cacheNote) console.log(` ${c.dim("·")} ${c.dim(`prompt cache: ${cacheNote}`)}`);
|
|
6416
6853
|
if (step.verify) {
|
|
6417
6854
|
// A step that completed with a verify held it — the runtime fails the
|
|
6418
6855
|
// step otherwise, so status IS the verdict, and saying it out loud is
|
|
@@ -6433,6 +6870,26 @@ async function reportCmd(runId, flags, layout) {
|
|
|
6433
6870
|
if (step.status === "awaiting-approval") {
|
|
6434
6871
|
console.log(` ${c.amber("⏸")} ${c.dim(step.waitFor === "event" ? "waiting on an event" : `waiting on a person: ${firstLine(step.ask ?? step.instruction, 70)}`)}`);
|
|
6435
6872
|
}
|
|
6873
|
+
// Each model call (FOL-54 step 4): one line, or every call with --calls,
|
|
6874
|
+
// and the tool calls whose input and output were recorded.
|
|
6875
|
+
const calls = Array.isArray(step.calls) ? step.calls : [];
|
|
6876
|
+
if (calls.length) {
|
|
6877
|
+
const slowest = Math.max(...calls.map((m) => m.ms ?? 0));
|
|
6878
|
+
console.log(` ${c.dim(`${calls.length} model call${calls.length === 1 ? "" : "s"} · slowest ${humanDuration(slowest)} · largest ${Math.max(...calls.map((m) => m.tokens?.input ?? 0)).toLocaleString()} in`)}`);
|
|
6879
|
+
if (flags.calls === true) {
|
|
6880
|
+
for (const m of calls) {
|
|
6881
|
+
const t = m.tokens ?? {};
|
|
6882
|
+
const cached = t.input > 0 && t.cacheRead > 0 ? ` (${Math.round((100 * t.cacheRead) / t.input)}% cached)` : "";
|
|
6883
|
+
console.log(` ${c.dim(`${m.n}. ${m.model ?? "model"}${m.subagent ? ` ↳ ${m.subagent}` : ""} · ${humanDuration(m.ms ?? 0)} · ${(t.input ?? 0).toLocaleString()} in${cached} · ${(t.output ?? 0).toLocaleString()} out${m.stop ? ` · ${m.stop}` : ""}${m.tools ? ` · ${m.tools} tool${m.tools === 1 ? "" : "s"}` : ""}`)}`);
|
|
6884
|
+
}
|
|
6885
|
+
}
|
|
6886
|
+
}
|
|
6887
|
+
if (flags.calls === true) {
|
|
6888
|
+
// Either half recorded counts, once per call.
|
|
6889
|
+
const recorded = [...new Map((step.events ?? []).filter((e) => e.type === "tool" && e.captured && e.call).map((e) => [e.call, e])).values()];
|
|
6890
|
+
for (const e of recorded) console.log(` ${c.dim(`→ ${e.text} ${e.call}`)}`);
|
|
6891
|
+
if (recorded.length) console.log(` ${c.dim(`\`foldrun report ${run.id} call <id>\` shows one call's input and output`)}`);
|
|
6892
|
+
}
|
|
6436
6893
|
const said = firstLine(step.conclusion ?? step.result, 96);
|
|
6437
6894
|
if (said) console.log(` ${said}`);
|
|
6438
6895
|
const err = stepError(step);
|
|
@@ -6634,6 +7091,38 @@ async function agentRunCmd(positional, flags, layout) {
|
|
|
6634
7091
|
return reportCmd(runId, { ...flags, to: ws }, layout);
|
|
6635
7092
|
}
|
|
6636
7093
|
|
|
7094
|
+
/**
|
|
7095
|
+
* `foldrun check --to <ws> --online` (FOL-58): the network checks, from the
|
|
7096
|
+
* platform — known vulnerabilities for exact runtime pins (OSV) and what
|
|
7097
|
+
* remote MCP servers tell the model about their tools — and, as loudly,
|
|
7098
|
+
* what could not be checked. Exit 1 on a finding at --fail-on or above
|
|
7099
|
+
* (warn under --security), as the local check does.
|
|
7100
|
+
*/
|
|
7101
|
+
async function checkOnline(url, flags, ws) {
|
|
7102
|
+
const answer = await remoteCall(
|
|
7103
|
+
url,
|
|
7104
|
+
flags,
|
|
7105
|
+
`/api/workspaces/${enc(ws)}/check?online=1&rule=supply-chain/known-vulnerability,content/mcp-tool-text`,
|
|
7106
|
+
{},
|
|
7107
|
+
{ seconds: Math.max(timeoutSeconds(flags), 120) },
|
|
7108
|
+
);
|
|
7109
|
+
// --rule narrows these as it narrows the offline ones (Codex review).
|
|
7110
|
+
const { rules } = checkOptions(flags);
|
|
7111
|
+
const want = new Set(rules);
|
|
7112
|
+
const findings = (answer.findings ?? []).filter(
|
|
7113
|
+
(f) => !rules.length || want.has(f.rule) || want.has(f.rule.split("/")[0]) || want.has(categoryOf(f.rule)),
|
|
7114
|
+
);
|
|
7115
|
+
const problems = answer.online?.problems ?? [];
|
|
7116
|
+
console.log(` ${c.bold("online checks")} ${c.dim("OSV for exact runtime pins · remote MCP servers' tool text")}`);
|
|
7117
|
+
for (const f of findings) console.log(` ${c.amber(" warn")} ${c.bold(`${f.file}${f.line ? `:${f.line}` : ""}`)} ${f.message} ${c.dim(`[${f.rule}]`)}`);
|
|
7118
|
+
if (!findings.length) console.log(` ${c.green("✓")} ${c.dim(problems.length ? "nothing found in what could be checked" : "nothing found")}`);
|
|
7119
|
+
for (const p of problems) console.log(` ${c.amber("!")} ${c.dim("not checked:")} ${p}`);
|
|
7120
|
+
if (answer.rescan?.checkedAt) console.log(` ${c.dim(`the daily rescan last ran ${answer.rescan.checkedAt}`)}`);
|
|
7121
|
+
console.log();
|
|
7122
|
+
const { failOn } = checkOptions(flags);
|
|
7123
|
+
return findings.some((f) => (RANK[f.level] ?? 0) >= FAIL_ON[failOn]) ? 1 : 0;
|
|
7124
|
+
}
|
|
7125
|
+
|
|
6637
7126
|
/**
|
|
6638
7127
|
* `foldrun check --to <workspace>` — validate the copy that is DEPLOYED.
|
|
6639
7128
|
*
|
|
@@ -6679,6 +7168,7 @@ async function checkRemote(flags, layout) {
|
|
|
6679
7168
|
// The baseline belongs to the folder it was written in, not to a temp copy.
|
|
6680
7169
|
const code = await check(dir, { ...flags, "no-baseline": true });
|
|
6681
7170
|
say(` ${c.dim(`checked what is deployed, not this folder — ${ws} · ${url}`)}\n`);
|
|
7171
|
+
if (flags.online === true) return Math.max(code, await checkOnline(url, flags, ws));
|
|
6682
7172
|
return code;
|
|
6683
7173
|
} finally {
|
|
6684
7174
|
if (before.workspace === undefined) delete process.env.FOLDRUN_WORKSPACE;
|
|
@@ -7984,6 +8474,8 @@ async function promoteCmd(runId, flags, layout) {
|
|
|
7984
8474
|
...(typeof flags.eval === "string" ? { evalName: flags.eval } : {}),
|
|
7985
8475
|
...(typeof flags.case === "string" ? { caseName: flags.case } : {}),
|
|
7986
8476
|
...(expect.length ? { expect } : {}),
|
|
8477
|
+
// A run that went wrong: what it should have done (FOL-72 step 10).
|
|
8478
|
+
...(typeof flags.should === "string" ? { should: flags.should } : {}),
|
|
7987
8479
|
}),
|
|
7988
8480
|
});
|
|
7989
8481
|
const evalName = path.basename(String(r.file ?? ""), ".md");
|
|
@@ -8574,7 +9066,7 @@ async function workspaceCmd(positional, flags, layout) {
|
|
|
8574
9066
|
const url = platformFor(flags, "workspace");
|
|
8575
9067
|
const ws = platformWorkspace(flags, layout, "workspace");
|
|
8576
9068
|
const [verb, key, ...rest] = positional;
|
|
8577
|
-
const KEYS = ["name", "description", "timezone", "budget", "notify"];
|
|
9069
|
+
const KEYS = ["name", "description", "timezone", "budget", "notify", "network_enforce"];
|
|
8578
9070
|
const show = (d, title) => {
|
|
8579
9071
|
const notify = d.notify ? [d.notify.email, d.notify.url].filter(Boolean).join(", ") + c.dim(` on ${(d.notify.events ?? []).join(", ") || "nothing"}`) : c.dim("the account's");
|
|
8580
9072
|
const budget = d.budget == null ? c.dim("the account's") : typeof d.budget === "object" ? `$${d.budget.usd} per ${d.budget.period}` : String(d.budget).includes("/") ? `$${String(d.budget).replace("/", " per ")}` : `$${d.budget} per month`;
|
|
@@ -8583,15 +9075,24 @@ async function workspaceCmd(positional, flags, layout) {
|
|
|
8583
9075
|
console.log(` timezone ${d.timezone ?? c.dim("the account's")}`);
|
|
8584
9076
|
console.log(` notify ${notify}`);
|
|
8585
9077
|
console.log(` budget ${budget}`);
|
|
9078
|
+
if (d.networkEnforce !== undefined) console.log(` network ${networkEnforceText(d.networkEnforce, "the account")}`);
|
|
8586
9079
|
};
|
|
8587
9080
|
|
|
8588
9081
|
if (!verb) {
|
|
8589
9082
|
const d = await workspaceDefaults(url, flags, ws);
|
|
8590
|
-
show({ description: typeof d.description === "string" ? d.description : "", timezone: d.timezone ?? null, budget: d.budget ?? null, notify: d.notify ?? null }, ws);
|
|
9083
|
+
show({ description: typeof d.description === "string" ? d.description : "", timezone: d.timezone ?? null, budget: d.budget ?? null, notify: d.notify ?? null, networkEnforce: d.network_enforce === true }, ws);
|
|
9084
|
+
const halt = await pauseState(remoteCall(url, flags, `/api/workspaces/${enc(ws)}/pause`));
|
|
9085
|
+
if (halt) console.log(` paused ${halt.paused ? c.red(`${halt.paused.scope === "account" ? "by the account: " : ""}${pauseLine(halt.paused)}`) : c.dim("no")}`);
|
|
8591
9086
|
console.log(`\n ${c.dim(`foldrun workspace set timezone Australia/Sydney --to ${ws} · set budget 20/week · set notify email you@example.com · set name <new> · clear budget`)}\n`);
|
|
8592
9087
|
return 0;
|
|
8593
9088
|
}
|
|
8594
|
-
if (verb
|
|
9089
|
+
if (verb === "pause" || verb === "resume") {
|
|
9090
|
+
return pauseCmd(flags, verb, `workspace ${ws}`, {
|
|
9091
|
+
post: (body) => remoteCall(url, flags, `/api/workspaces/${enc(ws)}/pause`, { method: "POST", body }),
|
|
9092
|
+
del: () => remoteCall(url, flags, `/api/workspaces/${enc(ws)}/pause`, { method: "DELETE" }),
|
|
9093
|
+
});
|
|
9094
|
+
}
|
|
9095
|
+
if (verb !== "set" && verb !== "clear") throw new Error(`unknown workspace verb "${verb}" — set <key> <value>, clear <key>, pause, resume (bare: show them)`);
|
|
8595
9096
|
if (!KEYS.includes(key) || (verb === "clear" && key === "name")) {
|
|
8596
9097
|
throw new Error(`which setting? \`foldrun workspace ${verb} <${(verb === "clear" ? KEYS.filter((k) => k !== "name") : KEYS).join("|")}> … --to <workspace>\``);
|
|
8597
9098
|
}
|
|
@@ -8612,7 +9113,7 @@ async function workspaceCmd(positional, flags, layout) {
|
|
|
8612
9113
|
} else {
|
|
8613
9114
|
const value = rest.join(" ").trim();
|
|
8614
9115
|
if (!value) throw new Error(`\`foldrun workspace set ${key} <value> --to ${ws}\`${key === "name" ? "" : ` — or \`clear ${key}\``}`);
|
|
8615
|
-
patch = { [key]: value };
|
|
9116
|
+
patch = { [key]: key === "network_enforce" ? onOff(key, value, `foldrun workspace set network_enforce on --to ${ws}`) : value };
|
|
8616
9117
|
}
|
|
8617
9118
|
|
|
8618
9119
|
if (key === "name" && verb === "set") {
|
|
@@ -9286,6 +9787,7 @@ async function flowDraftCmd(positional, flags, layout) {
|
|
|
9286
9787
|
const existing = typeof flags.flow === "string" ? flags.flow : undefined;
|
|
9287
9788
|
console.log(`\n ${c.dim(`drafting in ${ws}${existing ? ` (redraft of ${existing})` : ""} — one model call, charged like a step…`)}`);
|
|
9288
9789
|
const d = await remoteCall(url, flags, `/api/workspaces/${enc(ws)}/flows/draft`, { method: "POST", body: JSON.stringify({ description, ...(existing ? { flow: existing } : {}) }) }, { seconds: Math.max(timeoutSeconds(flags), 180) });
|
|
9790
|
+
/** @type {{ path: string, content: string, before: string | null, sha256?: string | null }[]} */
|
|
9289
9791
|
const files = d.files ?? [];
|
|
9290
9792
|
if (d.notes) console.log(`\n ${d.notes}`);
|
|
9291
9793
|
console.log();
|
|
@@ -9656,11 +10158,14 @@ async function historyRestoreCmd(positional, flags, layout) {
|
|
|
9656
10158
|
return 0;
|
|
9657
10159
|
}
|
|
9658
10160
|
// The dashboard's own message, so the two read alike in the history.
|
|
9659
|
-
|
|
10161
|
+
// A restore is an exact version someone picked: saved unchecked, and it
|
|
10162
|
+
// says so when today's checks would refuse it, as the dashboard does (FOL-57).
|
|
10163
|
+
const saved = await remoteCall(url, flags, `/api/workspaces/${enc(ws)}/source`, {
|
|
9660
10164
|
method: "PUT",
|
|
9661
10165
|
body: JSON.stringify({ path: file, content: f.after, message: `restored ${label} from history`, ifMatch: current.sha256 ?? null }),
|
|
9662
10166
|
});
|
|
9663
10167
|
console.log(` ${c.green("✓")} ${file} ${c.dim(`reads as it did at ${label} — a new revision; what it said before is still in the history`)}\n`);
|
|
10168
|
+
for (const i of (saved?.issues ?? []).filter((x) => x.level === "error")) console.log(` ${c.amber("!")} ${c.dim("does not pass today's checks:")} ${i.where} — ${i.message}`);
|
|
9664
10169
|
return 0;
|
|
9665
10170
|
}
|
|
9666
10171
|
|
|
@@ -10175,12 +10680,13 @@ async function evalNewCmd(name, flags, layout) {
|
|
|
10175
10680
|
/**
|
|
10176
10681
|
* `foldrun team` — who is in the account, their role and which workspaces
|
|
10177
10682
|
* they may open (GET /api/team). Read-only on purpose: inviting, changing a
|
|
10178
|
-
* role, removing someone and transferring the account are
|
|
10179
|
-
* and the platform refuses them from a key
|
|
10683
|
+
* role or workspace access, removing someone and transferring the account are
|
|
10684
|
+
* a person's acts, and the platform refuses them from a key (requirePerson,
|
|
10685
|
+
* server/api.ts) — so there is nothing for a key-holding CLI to send.
|
|
10180
10686
|
*/
|
|
10181
10687
|
async function teamCmd(positional, flags) {
|
|
10182
10688
|
if (positional[0]) {
|
|
10183
|
-
throw new Error("`foldrun team` lists the members —
|
|
10689
|
+
throw new Error("`foldrun team` lists the members — inviting, changing a role or workspace access, removing a member and transferring the account are a signed-in person's acts, done in Settings → Team; the platform refuses them from a key");
|
|
10184
10690
|
}
|
|
10185
10691
|
const url = platformFor(flags, "team");
|
|
10186
10692
|
const { members = [] } = await remoteCall(url, flags, "/api/team");
|
|
@@ -10402,6 +10908,14 @@ export async function run(command, positional, flags, workspace, layout) {
|
|
|
10402
10908
|
say(` ${c.dim(`coding-agent rules written for ${codingAgent()} — AGENTS.md ${r.agentsMd}, CLAUDE.md ${r.claudeMd}; read \`foldrun docs\` before editing`)}`);
|
|
10403
10909
|
}
|
|
10404
10910
|
if (flags.rules === true) return checkRulesCmd(flags);
|
|
10911
|
+
// --online (FOL-58): the network checks run on the platform, which has
|
|
10912
|
+
// the guarded fetch and the deployed copy — so it needs --to.
|
|
10913
|
+
if (flags.online === true && typeof flags.to !== "string") {
|
|
10914
|
+
throw new Error("`foldrun check --to <workspace> --online` — the online checks (OSV, remote MCP tool text) run on the platform, against the deployed copy");
|
|
10915
|
+
}
|
|
10916
|
+
if (flags.online === true && (flags.json === true || (typeof flags.format === "string" && flags.format !== "text"))) {
|
|
10917
|
+
throw new Error("--online prints text; for JSON ask the API: GET /api/workspaces/<ws>/check?online=1");
|
|
10918
|
+
}
|
|
10405
10919
|
// --to names a workspace on a PLATFORM, so there is nothing local to
|
|
10406
10920
|
// check and the folder this terminal stands in is beside the point.
|
|
10407
10921
|
return typeof flags.to === "string"
|
|
@@ -10485,14 +10999,28 @@ export async function run(command, positional, flags, workspace, layout) {
|
|
|
10485
10999
|
// the local store has no table to draw across workspaces, because a
|
|
10486
11000
|
// folder on a laptop is one workspace.
|
|
10487
11001
|
if (positional[0] === "rm") return runRmCmd(positional[1], flags, layout);
|
|
11002
|
+
if (positional[0] === "views") return runViewsCmd(flags);
|
|
11003
|
+
// The run doctor (FOL-22): the assistant reads the run and its files,
|
|
11004
|
+
// says why it failed and proposes the fix as diffs — saved on yes.
|
|
11005
|
+
if (positional[0] === "doctor") {
|
|
11006
|
+
if (positional.length !== 2 || !/^[A-Za-z0-9_.-]{1,80}$/.test(positional[1])) {
|
|
11007
|
+
throw new Error("`foldrun runs doctor <run-id> --to <workspace>` — the run doctor reads that run and proposes the fix");
|
|
11008
|
+
}
|
|
11009
|
+
return assistantCmd([`Doctor: why did run ${positional[1]} fail, and how do I fix it?`], flags, layout);
|
|
11010
|
+
}
|
|
10488
11011
|
return flags.local === true ? logsCmd(positional, flags, layout) : runsCmd(flags, layout);
|
|
10489
11012
|
case "approvals":
|
|
10490
11013
|
return approvalsCmd(flags, layout);
|
|
11014
|
+
case "issues":
|
|
11015
|
+
return issuesCmd(positional, flags, layout);
|
|
11016
|
+
case "insights":
|
|
11017
|
+
return insightsCmd(flags);
|
|
10491
11018
|
case "approve":
|
|
10492
11019
|
case "reject":
|
|
10493
11020
|
return decideCmd(command, positional[0], flags, layout);
|
|
10494
11021
|
case "report":
|
|
10495
11022
|
if (positional[1] === "live") return reportLiveCmd(positional[0], flags, layout);
|
|
11023
|
+
if (positional[1] === "call") return reportCallCmd(positional[0], positional[2], flags, layout);
|
|
10496
11024
|
return positional[1] === "get" ? reportGetCmd(positional[0], positional[2], flags, layout) : reportCmd(positional[0], flags, layout);
|
|
10497
11025
|
case "stop":
|
|
10498
11026
|
// No run id and a filter: every run the filter matches (runs/bulk).
|