@loompad/cli 1.2.5 → 1.2.7
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/CHANGELOG.md +33 -0
- package/dist/adapters/antigravity-cli.js +1 -1
- package/dist/adapters/antigravity-cli.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +25 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/cli/doctor.js +3 -1
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +309 -11
- package/dist/cli/index.js.map +1 -1
- package/dist/core/orchestra.js +17 -1
- package/dist/core/orchestra.js.map +1 -1
- package/dist/core/registry.d.ts +5 -0
- package/dist/core/registry.js +31 -1
- package/dist/core/registry.js.map +1 -1
- package/dist/core/review-triage.js +3 -13
- package/dist/core/review-triage.js.map +1 -1
- package/dist/core/secret-scan.d.ts +14 -0
- package/dist/core/secret-scan.js +42 -0
- package/dist/core/secret-scan.js.map +1 -0
- package/dist/core/shares.d.ts +33 -0
- package/dist/core/shares.js +54 -0
- package/dist/core/shares.js.map +1 -0
- package/dist/core/turn-gate.d.ts +53 -0
- package/dist/core/turn-gate.js +126 -0
- package/dist/core/turn-gate.js.map +1 -0
- package/dist/daemon/client.d.ts +19 -0
- package/dist/daemon/client.js +81 -5
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/routes/chats.js +18 -0
- package/dist/daemon/routes/chats.js.map +1 -1
- package/dist/daemon/routes/public.d.ts +1 -1
- package/dist/daemon/routes/public.js +21 -0
- package/dist/daemon/routes/public.js.map +1 -1
- package/dist/daemon/runner.js +3 -1
- package/dist/daemon/runner.js.map +1 -1
- package/docs/release-notes/v1.2.6.md +18 -0
- package/docs/release-notes/v1.2.7.md +6 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -320,6 +320,10 @@ program
|
|
|
320
320
|
console.log(pc.green(`✓ project "${project.name}" (${project.id})`));
|
|
321
321
|
for (const a of project.agents)
|
|
322
322
|
console.log(formatAgentRow(a));
|
|
323
|
+
const { agentConfigWarnings, readProjectConfig } = await import("../core/registry.js");
|
|
324
|
+
const cfg = readProjectConfig(dir);
|
|
325
|
+
for (const w of cfg ? agentConfigWarnings(cfg) : [])
|
|
326
|
+
console.log(pc.yellow(`! ${w}`));
|
|
323
327
|
await offerGitignore(dir, opts.gitignore);
|
|
324
328
|
console.log(pc.dim("\nedit .loom/config.json to add/remove agents or change roles"));
|
|
325
329
|
console.log(pc.dim("then: loom chat"));
|
|
@@ -1465,17 +1469,52 @@ const TASK_COLOR = {
|
|
|
1465
1469
|
needs_input: pc.yellow,
|
|
1466
1470
|
cancelled: pc.dim,
|
|
1467
1471
|
};
|
|
1468
|
-
|
|
1469
|
-
|
|
1472
|
+
/**
|
|
1473
|
+
* A run's tasks by outcome (#342: "completed 9/13" said nothing about the other four — dropped,
|
|
1474
|
+
* merged elsewhere, failed?). Only the non-zero ones, done first.
|
|
1475
|
+
*/
|
|
1476
|
+
function taskCounts(run) {
|
|
1477
|
+
const order = ["done", "running", "pending", "needs_input", "conflict", "failed", "cancelled"];
|
|
1478
|
+
const n = {};
|
|
1479
|
+
for (const t of run.tasks)
|
|
1480
|
+
n[t.status] = (n[t.status] ?? 0) + 1;
|
|
1481
|
+
return Object.fromEntries(order.filter((k) => n[k]).map((k) => [k, n[k]]).concat(Object.entries(n).filter(([k]) => !order.includes(k))));
|
|
1482
|
+
}
|
|
1483
|
+
const countsLine = (run) => {
|
|
1484
|
+
const c = taskCounts(run);
|
|
1485
|
+
return Object.keys(c).length ? Object.entries(c).map(([k, v]) => `${v} ${k.replace("_", " ")}`).join(" · ") : "no tasks";
|
|
1486
|
+
};
|
|
1487
|
+
/** What `loom orchestra --json` says about a run — status is one of OrchestraStatus (#341). */
|
|
1488
|
+
function runJson(run) {
|
|
1489
|
+
return {
|
|
1490
|
+
id: run.id, goal: run.goal, status: run.status, ...(run.status === "aborted" && run.interrupted ? { resumable: true } : {}),
|
|
1491
|
+
// every outcome key, 0 when none: a script reads tasks.failed without checking it exists
|
|
1492
|
+
terminal: isTerminalStatus(run.status), tasks: { total: run.tasks.length, done: 0, running: 0, pending: 0, needs_input: 0, conflict: 0, failed: 0, cancelled: 0, ...taskCounts(run) },
|
|
1493
|
+
branch: run.branch, costUsd: run.costUsd,
|
|
1494
|
+
...(run.landing?.pr ? { pr: run.landing.pr } : {}), ...(run.question ? { question: run.question } : {}), createdAt: run.createdAt, updatedAt: run.updatedAt,
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
const isTerminalStatus = (s) => s === "completed" || s === "failed" || s === "aborted" || s === "moved";
|
|
1498
|
+
/** Each worker's access mode (bypass | auto | ask), from the project's roster, for showing beside the run. */
|
|
1499
|
+
function workerModes(run, agents) {
|
|
1500
|
+
const out = {};
|
|
1501
|
+
for (const w of run.workers) {
|
|
1502
|
+
const a = agents.find((x) => x.id === w || x.kind === w);
|
|
1503
|
+
if (a)
|
|
1504
|
+
out[w] = a.permissions ?? "default";
|
|
1505
|
+
}
|
|
1506
|
+
return out;
|
|
1507
|
+
}
|
|
1508
|
+
function printRun(run, modes = {}) {
|
|
1470
1509
|
console.log(run.race
|
|
1471
|
-
? `${pc.magenta("🏁")} ${pc.bold(run.id)} ${pc.dim(runStatus(run))} race · ${
|
|
1472
|
-
: `${pc.magenta("🎼")} ${pc.bold(run.id)} ${pc.dim(runStatus(run))} ${
|
|
1510
|
+
? `${pc.magenta("🏁")} ${pc.bold(run.id)} ${pc.dim(runStatus(run))} race · ${countsLine(run)} · ${fmtUsd(run.costUsd)}`
|
|
1511
|
+
: `${pc.magenta("🎼")} ${pc.bold(run.id)} ${pc.dim(runStatus(run))} ${countsLine(run)} · ` +
|
|
1473
1512
|
`round ${run.round}/${run.maxRounds} · ${fmtUsd(run.costUsd)} · ${pc.dim(run.branch)}`);
|
|
1474
1513
|
console.log(` ${pc.dim("goal")} ${run.goal}`);
|
|
1475
1514
|
if (run.race)
|
|
1476
1515
|
console.log(` ${pc.dim("entrants")} ${run.workers.join(", ")}`);
|
|
1477
1516
|
else
|
|
1478
|
-
console.log(` ${pc.dim("orchestrator")} ${run.orchestrator.agent} ${pc.dim("workers")} ${run.workers.join(", ")}`);
|
|
1517
|
+
console.log(` ${pc.dim("orchestrator")} ${run.orchestrator.agent} ${pc.dim("workers")} ${run.workers.map((w) => (modes[w] ? `${w} (${modes[w]})` : w)).join(", ")}`);
|
|
1479
1518
|
for (const t of run.tasks) {
|
|
1480
1519
|
const color = TASK_COLOR[t.status] ?? ((x) => x);
|
|
1481
1520
|
const deps = t.dependsOn.length ? pc.dim(` after ${t.dependsOn.join(",")}`) : "";
|
|
@@ -1504,6 +1543,89 @@ function printRun(run) {
|
|
|
1504
1543
|
if (run.race && run.applied?.task)
|
|
1505
1544
|
console.log(` ${pc.green("kept:")} ${run.applied.task} → ${run.applied.into}`);
|
|
1506
1545
|
}
|
|
1546
|
+
function parseRunFlags(text) {
|
|
1547
|
+
const t = text.trim().split(/\s+/).filter(Boolean);
|
|
1548
|
+
const f = {};
|
|
1549
|
+
for (let i = 0; i < t.length; i++) {
|
|
1550
|
+
const k = t[i], v = () => { const x = t[++i]; if (!x)
|
|
1551
|
+
throw new Error(`${k} needs a value`); return x; };
|
|
1552
|
+
if (k === "-o" || k === "--orchestrator")
|
|
1553
|
+
f.orchestrator = v();
|
|
1554
|
+
else if (k === "-w" || k === "--workers")
|
|
1555
|
+
f.workers = v();
|
|
1556
|
+
else if (k === "-p" || k === "--parallel")
|
|
1557
|
+
f.parallel = v();
|
|
1558
|
+
else if (k === "--max-usd")
|
|
1559
|
+
f.maxUsd = v();
|
|
1560
|
+
else if (k === "--solo")
|
|
1561
|
+
f.solo = true;
|
|
1562
|
+
else
|
|
1563
|
+
throw new Error(`unknown run option ${k} (-o, -w, -p, --solo, --max-usd)`);
|
|
1564
|
+
}
|
|
1565
|
+
return f;
|
|
1566
|
+
}
|
|
1567
|
+
/** RunFlags as startOrchestra's body. */
|
|
1568
|
+
function runOptions(f) {
|
|
1569
|
+
return {
|
|
1570
|
+
...(f.orchestrator ? { orchestrator: f.orchestrator } : {}),
|
|
1571
|
+
...(f.workers ? { workers: f.workers.split(",").map((w) => w.trim()).filter(Boolean) } : {}),
|
|
1572
|
+
...(f.parallel ? { maxParallel: Number(f.parallel) } : {}),
|
|
1573
|
+
...(f.maxUsd ? { maxUsd: Number(f.maxUsd) } : {}),
|
|
1574
|
+
...(f.solo ? { team: false } : {}),
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* #340: wait for a run to end; if it completed, scan everything it changed for secrets, merge it
|
|
1579
|
+
* into the project's current branch, and (asked to) push. Stops at the first thing that's wrong,
|
|
1580
|
+
* and says what — a run that failed, a key in the diff, a merge or push that didn't go.
|
|
1581
|
+
*/
|
|
1582
|
+
async function waitScanApply(client, projectId, dir, runId, opts) {
|
|
1583
|
+
let run = (await client.orchestraRun(projectId, runId)).run;
|
|
1584
|
+
while (!isTerminalStatus(run.status) && run.status !== "waiting_human") {
|
|
1585
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
1586
|
+
run = (await client.orchestraRun(projectId, runId)).run;
|
|
1587
|
+
}
|
|
1588
|
+
if (run.status !== "completed")
|
|
1589
|
+
return { ok: false, runId, status: run.status, error: run.status === "waiting_human" ? `it's waiting on you: ${run.question ?? "a question"}` : (run.error ?? run.status) };
|
|
1590
|
+
const { execFileSync } = await import("node:child_process");
|
|
1591
|
+
const git = (...args) => execFileSync("git", args, { cwd: dir, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
1592
|
+
let diff = "";
|
|
1593
|
+
try {
|
|
1594
|
+
diff = git("diff", `${run.baseCommit}..${run.branch}`);
|
|
1595
|
+
}
|
|
1596
|
+
catch (e) {
|
|
1597
|
+
return { ok: false, runId, status: run.status, error: `couldn't read the run's changes: ${e.message.split("\n")[0]}` };
|
|
1598
|
+
}
|
|
1599
|
+
const { findSecrets } = await import("../core/secret-scan.js");
|
|
1600
|
+
const secrets = findSecrets(diff);
|
|
1601
|
+
if (secrets.length)
|
|
1602
|
+
return { ok: false, runId, status: run.status, secrets, error: "secrets in the run's changes — not applied" };
|
|
1603
|
+
let out;
|
|
1604
|
+
try {
|
|
1605
|
+
out = await client.orchestraAction(projectId, runId, "apply");
|
|
1606
|
+
}
|
|
1607
|
+
catch (e) {
|
|
1608
|
+
return { ok: false, runId, status: run.status, error: `merge failed: ${e.message}` };
|
|
1609
|
+
}
|
|
1610
|
+
const res = { ok: true, runId, status: run.status, merged: String(out.merged ?? run.branch), into: String(out.into ?? "") };
|
|
1611
|
+
if (opts.push) {
|
|
1612
|
+
try {
|
|
1613
|
+
git("push", "origin", "HEAD");
|
|
1614
|
+
res.pushed = true;
|
|
1615
|
+
}
|
|
1616
|
+
catch (e) {
|
|
1617
|
+
return { ...res, ok: false, pushed: false, error: `merged, but the push failed: ${e.message.split("\n")[0]}` };
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return res;
|
|
1621
|
+
}
|
|
1622
|
+
function printApplyResult(r) {
|
|
1623
|
+
if (r.ok)
|
|
1624
|
+
return void console.log(`${pc.green("✓")} ${r.runId} merged into ${pc.bold(r.into || "your branch")}${r.pushed ? " and pushed" : ""} — no secrets in its changes`);
|
|
1625
|
+
console.log(`${pc.red("✗")} ${r.runId} (${r.status}): ${r.error}`);
|
|
1626
|
+
for (const s of r.secrets ?? [])
|
|
1627
|
+
console.log(pc.red(` ${s.file}${s.line ? `:${s.line}` : ""} — ${s.what}`));
|
|
1628
|
+
}
|
|
1507
1629
|
async function watchRun(client, projectId, runId) {
|
|
1508
1630
|
let last = "";
|
|
1509
1631
|
for (;;) {
|
|
@@ -1536,6 +1658,8 @@ program
|
|
|
1536
1658
|
.option("--max-usd <n>", "stop this goal when it has spent this much, and say so")
|
|
1537
1659
|
.option("--race", "race: every worker takes the whole goal in its own worktree; pick one with orchestra:apply <run> --task race-<agent>")
|
|
1538
1660
|
.option("--solo", "keep every task on this machine, even when teammates' runners could take some")
|
|
1661
|
+
.option("--apply", "when the run completes, scan its changes for secrets and merge it into your branch (#340)")
|
|
1662
|
+
.option("--push", "with --apply: push the branch after merging")
|
|
1539
1663
|
.option("--no-watch", "start it and return instead of following it to the end")
|
|
1540
1664
|
.action(async (goal, opts) => {
|
|
1541
1665
|
const client = await ensureDaemon();
|
|
@@ -1561,8 +1685,14 @@ program
|
|
|
1561
1685
|
`${run.workers.join(", ")} (${run.maxParallel} in parallel) on ${pc.dim(run.branch)}`);
|
|
1562
1686
|
if (opts.watch)
|
|
1563
1687
|
await watchRun(client, project.id, run.id);
|
|
1564
|
-
else
|
|
1688
|
+
else if (!opts.apply)
|
|
1565
1689
|
console.log(pc.dim(` follow it: loom orchestra ${run.id} --watch`));
|
|
1690
|
+
if (opts.apply) {
|
|
1691
|
+
const res = await waitScanApply(client, project.id, project.dir, run.id, { push: Boolean(opts.push) });
|
|
1692
|
+
printApplyResult(res);
|
|
1693
|
+
if (!res.ok)
|
|
1694
|
+
process.exitCode = 1;
|
|
1695
|
+
}
|
|
1566
1696
|
}
|
|
1567
1697
|
catch (err) {
|
|
1568
1698
|
console.error(pc.red(err instanceof Error ? err.message : String(err)));
|
|
@@ -1571,24 +1701,43 @@ program
|
|
|
1571
1701
|
});
|
|
1572
1702
|
program
|
|
1573
1703
|
.command("orchestra [runId]")
|
|
1574
|
-
.description("orchestra runs in this project — or one run in detail")
|
|
1704
|
+
.description("orchestra runs in this project — or one run in detail. Status: starting | planning | running | reviewing | waiting_human | completed | failed | aborted | moved")
|
|
1575
1705
|
.option("--watch", "follow the run until it finishes")
|
|
1706
|
+
.option("--json", "print runs (or one run) as JSON")
|
|
1707
|
+
.option("--wait", "with a run id: wait until it's completed, failed or aborted (or asks you something); exit 0 completed, 1 failed/aborted, 2 waiting on you")
|
|
1708
|
+
.option("--timeout <seconds>", "with --wait: give up after this long (exit 124)", "7200")
|
|
1576
1709
|
.action(async (runId, opts) => {
|
|
1577
1710
|
const client = await ensureDaemon();
|
|
1578
1711
|
const project = await currentProject(client);
|
|
1579
1712
|
try {
|
|
1580
1713
|
if (runId) {
|
|
1714
|
+
if (opts.wait) {
|
|
1715
|
+
// "reviewing" is the orchestrator reading results, not the end (#341) — only these are
|
|
1716
|
+
const deadline = Date.now() + Math.max(1, Number(opts.timeout) || 7200) * 1000;
|
|
1717
|
+
let run = (await client.orchestraRun(project.id, runId)).run;
|
|
1718
|
+
while (!isTerminalStatus(run.status) && run.status !== "waiting_human" && Date.now() < deadline) {
|
|
1719
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1720
|
+
run = (await client.orchestraRun(project.id, runId)).run;
|
|
1721
|
+
}
|
|
1722
|
+
const done = isTerminalStatus(run.status) || run.status === "waiting_human";
|
|
1723
|
+
process.exitCode = !done ? 124 : run.status === "completed" ? 0 : run.status === "waiting_human" ? 2 : 1;
|
|
1724
|
+
return void (opts.json ? console.log(JSON.stringify(runJson(run))) : printRun(run));
|
|
1725
|
+
}
|
|
1581
1726
|
if (opts.watch)
|
|
1582
1727
|
return void (await watchRun(client, project.id, runId));
|
|
1583
|
-
|
|
1728
|
+
const { run } = await client.orchestraRun(project.id, runId);
|
|
1729
|
+
// which access each worker runs with: "bypass on the agent" that doesn't reach a worker shows here
|
|
1730
|
+
const modes = workerModes(run, project.agents);
|
|
1731
|
+
return void (opts.json ? console.log(JSON.stringify({ ...runJson(run), workerModes: modes })) : printRun(run, modes));
|
|
1584
1732
|
}
|
|
1585
1733
|
const { runs } = await client.orchestraRuns(project.id);
|
|
1734
|
+
if (opts.json)
|
|
1735
|
+
return void console.log(JSON.stringify(runs.map(runJson)));
|
|
1586
1736
|
if (!runs.length)
|
|
1587
1737
|
return void console.log(pc.dim('no orchestra runs yet — loom orchestrate "<goal>"'));
|
|
1588
1738
|
for (const r of runs.slice(0, 15)) {
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
`${pc.dim(new Date(r.createdAt).toLocaleString())} ${r.goal.slice(0, 70)}`);
|
|
1739
|
+
console.log(`${pc.bold(r.id)} ${runStatus(r).padEnd(13)} ${countsLine(r).padEnd(30)} ` +
|
|
1740
|
+
`${pc.dim(new Date(r.createdAt).toLocaleString())} ${r.goal.slice(0, 60)}`);
|
|
1592
1741
|
}
|
|
1593
1742
|
}
|
|
1594
1743
|
catch (err) {
|
|
@@ -2557,6 +2706,155 @@ program
|
|
|
2557
2706
|
process.exitCode = 1;
|
|
2558
2707
|
}
|
|
2559
2708
|
});
|
|
2709
|
+
program
|
|
2710
|
+
.command("share [chat]")
|
|
2711
|
+
.description("a read-only link to a chat (default: main) for a teammate: prompts, replies, what ran and changed. It expires; --list shows yours, --revoke ends one (#199)")
|
|
2712
|
+
.option("--hours <n>", "how long the link works (1 to 720, default 24)")
|
|
2713
|
+
.option("--list", "the links that still work")
|
|
2714
|
+
.option("--revoke <id>", "end a link now")
|
|
2715
|
+
.option("--json", "print as JSON")
|
|
2716
|
+
.action(async (chat, opts) => {
|
|
2717
|
+
const client = await ensureDaemon();
|
|
2718
|
+
if (opts.revoke) {
|
|
2719
|
+
const { revoked } = await client.revokeShare(opts.revoke);
|
|
2720
|
+
if (!revoked)
|
|
2721
|
+
fail(`no link ${opts.revoke} — loom share --list`);
|
|
2722
|
+
return void console.log(`${pc.green("✓")} link ${opts.revoke} no longer opens`);
|
|
2723
|
+
}
|
|
2724
|
+
if (opts.list) {
|
|
2725
|
+
const { shares } = await client.shares();
|
|
2726
|
+
if (opts.json)
|
|
2727
|
+
return void console.log(JSON.stringify(shares));
|
|
2728
|
+
if (!shares.length)
|
|
2729
|
+
return void console.log(pc.dim("no links open — loom share [chat]"));
|
|
2730
|
+
for (const s of shares)
|
|
2731
|
+
console.log(`${pc.bold(s.id)} ${s.title} ${pc.dim(`until ${new Date(s.expiresAt).toLocaleString()}`)}`);
|
|
2732
|
+
return;
|
|
2733
|
+
}
|
|
2734
|
+
const project = await currentProject(client);
|
|
2735
|
+
let chatId = "main";
|
|
2736
|
+
if (chat && chat !== "main") {
|
|
2737
|
+
const { chats } = await client.chats(project.id);
|
|
2738
|
+
const hit = chats.find((c) => c.id === chat || c.title.toLowerCase() === chat.toLowerCase());
|
|
2739
|
+
if (!hit)
|
|
2740
|
+
fail(`no chat "${chat}" here — ${chats.slice(0, 6).map((c) => c.title).join(", ") || "only main"}`);
|
|
2741
|
+
chatId = hit.id;
|
|
2742
|
+
}
|
|
2743
|
+
const out = await client.shareChat(project.id, chatId, opts.hours ? Number(opts.hours) : undefined);
|
|
2744
|
+
if (opts.json)
|
|
2745
|
+
return void console.log(JSON.stringify(out));
|
|
2746
|
+
console.log(`${pc.green("✓")} ${out.title} — read-only, until ${new Date(out.expiresAt).toLocaleString()}\n\n ${out.url}\n`);
|
|
2747
|
+
console.log(pc.dim(`anyone who can reach this machine with the link can read it (on another computer: start the daemon with loom up --tailnet). loom share --revoke ${out.id} ends it`));
|
|
2748
|
+
});
|
|
2749
|
+
program
|
|
2750
|
+
.command("chain [file]")
|
|
2751
|
+
.description("run goals in several projects, one after another: each line is `<project> :: <goal>` (project = name or folder). Each completed run is scanned for secrets and merged before the next starts (#340)")
|
|
2752
|
+
.option("--push", "push each project's branch after merging its run")
|
|
2753
|
+
.option("--keep-going", "carry on to the next project when one fails")
|
|
2754
|
+
.option("--json", "print a JSON summary at the end")
|
|
2755
|
+
.option("-o, --orchestrator <agent>", "who plans every run (a line can say otherwise: `<project> :: [-o grok-code] <goal>`)")
|
|
2756
|
+
.option("-w, --workers <agents>", "comma-separated workers for every run")
|
|
2757
|
+
.option("-p, --parallel <n>", "tasks at once in each run")
|
|
2758
|
+
.option("--solo", "keep every run's tasks on this machine")
|
|
2759
|
+
.option("--max-usd <n>", "stop a run when it has spent this much")
|
|
2760
|
+
.action(async (file, opts) => {
|
|
2761
|
+
const text = file && file !== "-" ? fs.readFileSync(file, "utf8") : await new Promise((resolve) => {
|
|
2762
|
+
let buf = "";
|
|
2763
|
+
process.stdin.setEncoding("utf8");
|
|
2764
|
+
process.stdin.on("data", (d) => (buf += d));
|
|
2765
|
+
process.stdin.on("end", () => resolve(buf));
|
|
2766
|
+
});
|
|
2767
|
+
const steps = text.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).map((l) => {
|
|
2768
|
+
const at = l.indexOf("::");
|
|
2769
|
+
if (at < 0)
|
|
2770
|
+
fail(`each line is "<project> :: <goal>" — got: ${l}`);
|
|
2771
|
+
// a line's own run options, in brackets before its goal, win over the command's
|
|
2772
|
+
const rest = l.slice(at + 2).trim();
|
|
2773
|
+
const own = /^\[([^\]]*)\]\s*/.exec(rest);
|
|
2774
|
+
let flags = {};
|
|
2775
|
+
if (own) {
|
|
2776
|
+
try {
|
|
2777
|
+
flags = parseRunFlags(own[1]);
|
|
2778
|
+
}
|
|
2779
|
+
catch (e) {
|
|
2780
|
+
fail(`${e.message} — in: ${l}`);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
return { project: l.slice(0, at).trim(), goal: own ? rest.slice(own[0].length).trim() : rest, flags };
|
|
2784
|
+
});
|
|
2785
|
+
if (!steps.length)
|
|
2786
|
+
fail("nothing to run — one `<project> :: <goal>` per line");
|
|
2787
|
+
const client = await ensureDaemon();
|
|
2788
|
+
const { projects } = await client.listProjects();
|
|
2789
|
+
const results = [];
|
|
2790
|
+
for (const step of steps) {
|
|
2791
|
+
const p = projects.find((x) => x.name === step.project || x.id === step.project || path.resolve(x.dir) === path.resolve(step.project));
|
|
2792
|
+
if (!p) {
|
|
2793
|
+
results.push({ project: step.project, ok: false, runId: "-", status: "not started", error: `no project "${step.project}" — loom projects` });
|
|
2794
|
+
if (!opts.keepGoing)
|
|
2795
|
+
break;
|
|
2796
|
+
continue;
|
|
2797
|
+
}
|
|
2798
|
+
if (!opts.json)
|
|
2799
|
+
console.log(`${pc.magenta("▶")} ${pc.bold(p.name)} — ${step.goal.slice(0, 80)}`);
|
|
2800
|
+
try {
|
|
2801
|
+
const { run } = await client.startOrchestra(p.id, { goal: step.goal, ...runOptions({ ...opts, ...step.flags }) });
|
|
2802
|
+
const r = { project: p.name, ...(await waitScanApply(client, p.id, p.dir, run.id, { push: Boolean(opts.push) })) };
|
|
2803
|
+
results.push(r);
|
|
2804
|
+
if (!opts.json)
|
|
2805
|
+
printApplyResult(r);
|
|
2806
|
+
if (!r.ok && !opts.keepGoing)
|
|
2807
|
+
break;
|
|
2808
|
+
}
|
|
2809
|
+
catch (e) {
|
|
2810
|
+
results.push({ project: p.name, ok: false, runId: "-", status: "not started", error: e.message });
|
|
2811
|
+
if (!opts.json)
|
|
2812
|
+
console.log(`${pc.red("✗")} ${p.name}: ${e.message}`);
|
|
2813
|
+
if (!opts.keepGoing)
|
|
2814
|
+
break;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
2818
|
+
if (opts.json)
|
|
2819
|
+
console.log(JSON.stringify({ ok: failed === 0, steps: results }));
|
|
2820
|
+
else
|
|
2821
|
+
console.log(pc.dim(`\n${results.length - failed}/${steps.length} merged${failed ? ` · ${failed} failed` : ""}${results.length < steps.length ? ` · ${steps.length - results.length} not run` : ""}`));
|
|
2822
|
+
if (failed || results.length < steps.length)
|
|
2823
|
+
process.exitCode = 1;
|
|
2824
|
+
});
|
|
2825
|
+
program
|
|
2826
|
+
.command("limits")
|
|
2827
|
+
.description("how many agent turns run at once across every project, and how much memory stays free — past either, turns queue (#339)")
|
|
2828
|
+
.option("--turns <n>", "turns at once across all projects (0 = no cap)")
|
|
2829
|
+
.option("--min-free <pct>", "don't start a turn while less than this % of memory is free (0 = off)")
|
|
2830
|
+
.option("--json", "print as JSON")
|
|
2831
|
+
.action(async (opts) => {
|
|
2832
|
+
const { readLimits, writeLimits, availableMemoryPct, defaultLimits } = await import("../core/turn-gate.js");
|
|
2833
|
+
const patch = {};
|
|
2834
|
+
if (opts.turns !== undefined) {
|
|
2835
|
+
const n = Number(opts.turns);
|
|
2836
|
+
if (!Number.isInteger(n) || n < 0)
|
|
2837
|
+
fail("--turns takes a whole number (0 = no cap)");
|
|
2838
|
+
patch.maxTurns = n;
|
|
2839
|
+
}
|
|
2840
|
+
if (opts.minFree !== undefined) {
|
|
2841
|
+
const n = Number(opts.minFree);
|
|
2842
|
+
if (!(n >= 0 && n < 100))
|
|
2843
|
+
fail("--min-free takes a % from 0 to 99");
|
|
2844
|
+
patch.minFreePct = n;
|
|
2845
|
+
}
|
|
2846
|
+
const limits = Object.keys(patch).length ? writeLimits(patch) : readLimits();
|
|
2847
|
+
const free = Math.round(availableMemoryPct());
|
|
2848
|
+
const cfg = await daemonRunning();
|
|
2849
|
+
const running = cfg ? await turnsInFlight(new DaemonClient(cfg)) : [];
|
|
2850
|
+
if (opts.json)
|
|
2851
|
+
return void console.log(JSON.stringify({ ...limits, freePct: free, running, defaults: defaultLimits() }));
|
|
2852
|
+
if (Object.keys(patch).length)
|
|
2853
|
+
console.log(pc.green("✓ saved — the daemon picks it up within a few seconds"));
|
|
2854
|
+
console.log(`turns at once ${limits.maxTurns ? pc.bold(String(limits.maxTurns)) : pc.dim("no cap")} ${pc.dim(`(running now: ${running.length}${running.length ? ` — ${running.slice(0, 4).join(", ")}` : ""})`)}`);
|
|
2855
|
+
console.log(`memory kept free ${limits.minFreePct ? pc.bold(`${limits.minFreePct}%`) : pc.dim("off")} ${pc.dim(`(free now: ${free}%)`)}`);
|
|
2856
|
+
console.log(pc.dim("past either, a new turn waits for a slot and says so in its thread; one that waits 10 min starts anyway"));
|
|
2857
|
+
});
|
|
2560
2858
|
program
|
|
2561
2859
|
.command("agents:mode <agentId> [mode]")
|
|
2562
2860
|
.description("an agent's access: bypass (no sandbox — can commit, push, use the network), auto (the default), or ask. No mode: show what each means for it")
|