cruo-agent 0.1.8 → 0.1.10
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/dist/VERSION +1 -1
- package/dist/cli.js +338 -37
- package/package.json +1 -1
package/dist/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.10
|
package/dist/cli.js
CHANGED
|
@@ -45913,6 +45913,36 @@ function isKnownCommandWord(first) {
|
|
|
45913
45913
|
function nearestCommands(first) {
|
|
45914
45914
|
return CRUO_COMMANDS.filter((k) => k[0] === first[0]).slice(0, 3);
|
|
45915
45915
|
}
|
|
45916
|
+
function editDistance(a, b) {
|
|
45917
|
+
const row = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
45918
|
+
for (let i = 1; i <= a.length; i++) {
|
|
45919
|
+
let diagonal = row[0];
|
|
45920
|
+
row[0] = i;
|
|
45921
|
+
for (let j = 1; j <= b.length; j++) {
|
|
45922
|
+
const above = row[j];
|
|
45923
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
45924
|
+
diagonal = above;
|
|
45925
|
+
}
|
|
45926
|
+
}
|
|
45927
|
+
return row[b.length];
|
|
45928
|
+
}
|
|
45929
|
+
function unknownFlags(argv2, valued = [...SUPERVISOR_VALUED, ...CLI_VALUED], switches = SUPERVISOR_SWITCHES) {
|
|
45930
|
+
const known = [...valued, ...switches];
|
|
45931
|
+
const found = [];
|
|
45932
|
+
for (let i = 0; i < argv2.length; i++) {
|
|
45933
|
+
const arg = argv2[i];
|
|
45934
|
+
if (!arg.startsWith("-")) continue;
|
|
45935
|
+
const name = arg.replace(/^-+/, "");
|
|
45936
|
+
if (valued.includes(name)) {
|
|
45937
|
+
i++;
|
|
45938
|
+
continue;
|
|
45939
|
+
}
|
|
45940
|
+
if (switches.includes(name)) continue;
|
|
45941
|
+
const ranked = known.map((k) => ({ k, d: editDistance(name.split("=")[0], k) })).sort((x, y) => x.d - y.d)[0];
|
|
45942
|
+
found.push({ flag: arg, nearest: ranked && ranked.d <= 2 ? `--${ranked.k}` : null });
|
|
45943
|
+
}
|
|
45944
|
+
return found;
|
|
45945
|
+
}
|
|
45916
45946
|
function numericIn(argv2, name, spec = {}) {
|
|
45917
45947
|
const i = argv2.indexOf(`--${name}`);
|
|
45918
45948
|
if (i < 0) return spec.fallback;
|
|
@@ -45947,7 +45977,7 @@ function requiredNumericIn(argv2, name, fallback, spec = {}) {
|
|
|
45947
45977
|
function secondsIn(argv2, name, fallbackSeconds) {
|
|
45948
45978
|
return requiredNumericIn(argv2, name, fallbackSeconds, { unit: "seconds" }) * 1e3;
|
|
45949
45979
|
}
|
|
45950
|
-
var CRUO_COMMANDS, OptionError, flagIn, optIn;
|
|
45980
|
+
var CRUO_COMMANDS, SUPERVISOR_SWITCHES, SUPERVISOR_VALUED, CLI_VALUED, OptionError, flagIn, optIn;
|
|
45951
45981
|
var init_options = __esm({
|
|
45952
45982
|
"src/options.ts"() {
|
|
45953
45983
|
"use strict";
|
|
@@ -45963,8 +45993,32 @@ var init_options = __esm({
|
|
|
45963
45993
|
"stop",
|
|
45964
45994
|
"pause",
|
|
45965
45995
|
"resume",
|
|
45966
|
-
"logs"
|
|
45996
|
+
"logs",
|
|
45997
|
+
"run",
|
|
45998
|
+
"version",
|
|
45999
|
+
"?"
|
|
45967
46000
|
];
|
|
46001
|
+
SUPERVISOR_SWITCHES = ["once", "dry-run", "worktree", "keep-failed", "push", "usage"];
|
|
46002
|
+
SUPERVISOR_VALUED = [
|
|
46003
|
+
"allow",
|
|
46004
|
+
"base",
|
|
46005
|
+
"cwd",
|
|
46006
|
+
"function",
|
|
46007
|
+
"harness",
|
|
46008
|
+
"harness-timeout",
|
|
46009
|
+
"interval",
|
|
46010
|
+
"limit",
|
|
46011
|
+
"max-attempts",
|
|
46012
|
+
"max-interval",
|
|
46013
|
+
"model",
|
|
46014
|
+
"prepare",
|
|
46015
|
+
"prepare-timeout",
|
|
46016
|
+
"project",
|
|
46017
|
+
"push-remote",
|
|
46018
|
+
"repo",
|
|
46019
|
+
"worktree-root"
|
|
46020
|
+
];
|
|
46021
|
+
CLI_VALUED = ["as", "token"];
|
|
45968
46022
|
OptionError = class extends Error {
|
|
45969
46023
|
constructor(message) {
|
|
45970
46024
|
super(message);
|
|
@@ -46051,6 +46105,83 @@ async function listRecords() {
|
|
|
46051
46105
|
}
|
|
46052
46106
|
return out;
|
|
46053
46107
|
}
|
|
46108
|
+
function entryKindOf(token) {
|
|
46109
|
+
const normalised = token.replace(/\\/g, "/");
|
|
46110
|
+
const basename = normalised.slice(normalised.lastIndexOf("/") + 1);
|
|
46111
|
+
if (CLI_BIN_NAMES.has(basename)) return "cli";
|
|
46112
|
+
if (SUPERVISOR_BIN_NAMES.has(basename)) return "supervisor";
|
|
46113
|
+
const isCliFile = /^cli\.(?:js|ts)$/.test(basename);
|
|
46114
|
+
const isSupervisorFile = /^supervisor\.(?:js|ts)$/.test(basename);
|
|
46115
|
+
if (!isCliFile && !isSupervisorFile) return null;
|
|
46116
|
+
if (!OWN_PACKAGE_MARKERS.some((marker) => normalised.includes(marker))) return null;
|
|
46117
|
+
return isCliFile ? "cli" : "supervisor";
|
|
46118
|
+
}
|
|
46119
|
+
function parsePsLine(line) {
|
|
46120
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
|
|
46121
|
+
return m ? { pid: Number(m[1]), uid: Number(m[2]), args: m[3] } : null;
|
|
46122
|
+
}
|
|
46123
|
+
function asNameFrom(tokens) {
|
|
46124
|
+
const i = tokens.indexOf("--as");
|
|
46125
|
+
const value = i >= 0 ? tokens[i + 1] : void 0;
|
|
46126
|
+
return value && !value.startsWith("-") ? safeName(value) : null;
|
|
46127
|
+
}
|
|
46128
|
+
async function processArgv(pid) {
|
|
46129
|
+
try {
|
|
46130
|
+
const { stdout } = await exec("ps", ["-p", String(pid), "-o", "args="]);
|
|
46131
|
+
const value = stdout.trim();
|
|
46132
|
+
return value === "" ? null : value.split(/\s+/).filter(Boolean);
|
|
46133
|
+
} catch {
|
|
46134
|
+
return null;
|
|
46135
|
+
}
|
|
46136
|
+
}
|
|
46137
|
+
async function findSupervisorProcesses() {
|
|
46138
|
+
const uid = process.getuid?.();
|
|
46139
|
+
if (uid === void 0) return [];
|
|
46140
|
+
let stdout;
|
|
46141
|
+
try {
|
|
46142
|
+
({ stdout } = await exec("ps", ["-axwwo", "pid=,uid=,args="]));
|
|
46143
|
+
} catch {
|
|
46144
|
+
return [];
|
|
46145
|
+
}
|
|
46146
|
+
const out = [];
|
|
46147
|
+
for (const line of stdout.split("\n")) {
|
|
46148
|
+
const parsed = parsePsLine(line);
|
|
46149
|
+
if (!parsed || parsed.uid !== uid || parsed.pid === process.pid) continue;
|
|
46150
|
+
const tokens = parsed.args.trim().split(/\s+/).filter(Boolean);
|
|
46151
|
+
let scriptAt = -1;
|
|
46152
|
+
let kind = null;
|
|
46153
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
46154
|
+
kind = entryKindOf(tokens[i]);
|
|
46155
|
+
if (kind) {
|
|
46156
|
+
scriptAt = i;
|
|
46157
|
+
break;
|
|
46158
|
+
}
|
|
46159
|
+
}
|
|
46160
|
+
if (!kind) continue;
|
|
46161
|
+
if (kind === "cli") {
|
|
46162
|
+
const next = tokens[scriptAt + 1];
|
|
46163
|
+
const isSubcommand = next !== void 0 && !next.startsWith("-") && NON_LOOPING_CLI_COMMANDS.includes(next);
|
|
46164
|
+
if (isSubcommand) continue;
|
|
46165
|
+
}
|
|
46166
|
+
out.push({
|
|
46167
|
+
pid: parsed.pid,
|
|
46168
|
+
asName: asNameFrom(tokens),
|
|
46169
|
+
procStartedAt: await processStartedAt(parsed.pid),
|
|
46170
|
+
argv: tokens
|
|
46171
|
+
});
|
|
46172
|
+
}
|
|
46173
|
+
return out;
|
|
46174
|
+
}
|
|
46175
|
+
async function untrackedSupervisors() {
|
|
46176
|
+
const found = await findSupervisorProcesses();
|
|
46177
|
+
if (found.length === 0) return found;
|
|
46178
|
+
const tracked = await listRecords();
|
|
46179
|
+
const trackedPids = /* @__PURE__ */ new Set();
|
|
46180
|
+
for (const { live } of tracked) {
|
|
46181
|
+
if (live.state === "running") trackedPids.add(live.record.pid);
|
|
46182
|
+
}
|
|
46183
|
+
return found.filter((p) => !trackedPids.has(p.pid));
|
|
46184
|
+
}
|
|
46054
46185
|
function signal(pid, which) {
|
|
46055
46186
|
try {
|
|
46056
46187
|
process.kill(pid, SIGNALS[which]);
|
|
@@ -46067,14 +46198,19 @@ function since(iso) {
|
|
|
46067
46198
|
if (ms2 < 864e5) return `${Math.round(ms2 / 36e5)}h`;
|
|
46068
46199
|
return `${Math.round(ms2 / 864e5)}d`;
|
|
46069
46200
|
}
|
|
46070
|
-
var exec, runDir, logDir, recordPath, SIGNALS;
|
|
46201
|
+
var exec, runDir, logDir, recordPath, CLI_BIN_NAMES, SUPERVISOR_BIN_NAMES, OWN_PACKAGE_MARKERS, NON_LOOPING_CLI_COMMANDS, SIGNALS;
|
|
46071
46202
|
var init_process = __esm({
|
|
46072
46203
|
"src/process.ts"() {
|
|
46073
46204
|
"use strict";
|
|
46205
|
+
init_options();
|
|
46074
46206
|
exec = promisify(execFile);
|
|
46075
46207
|
runDir = () => join(homedir(), ".cruo", "run");
|
|
46076
46208
|
logDir = () => join(homedir(), ".cruo", "logs");
|
|
46077
46209
|
recordPath = (agent) => join(runDir(), `${safeName(agent)}.json`);
|
|
46210
|
+
CLI_BIN_NAMES = /* @__PURE__ */ new Set(["cruo", "cruo-agent"]);
|
|
46211
|
+
SUPERVISOR_BIN_NAMES = /* @__PURE__ */ new Set(["cruo-supervisor"]);
|
|
46212
|
+
OWN_PACKAGE_MARKERS = ["@cruo/mcp", "cruo-agent", "apps/mcp", "packages/cli"];
|
|
46213
|
+
NON_LOOPING_CLI_COMMANDS = CRUO_COMMANDS.filter((c) => c !== "run");
|
|
46078
46214
|
SIGNALS = {
|
|
46079
46215
|
stop: "SIGTERM",
|
|
46080
46216
|
pause: "SIGUSR1",
|
|
@@ -47057,6 +47193,14 @@ function systemPrompt(ctx, identity, worktree) {
|
|
|
47057
47193
|
`- If you cannot do the work \u2014 missing information, ambiguous brief, outside`,
|
|
47058
47194
|
` your capabilities \u2014 say so in a comment and assign it to someone who can,`,
|
|
47059
47195
|
` or leave it for a human. Do not guess, and do not silently do nothing.`,
|
|
47196
|
+
`- Claim the work before you do it: move the issue to the state your workflow`,
|
|
47197
|
+
` uses for work in progress, and do that BEFORE you start, not when you`,
|
|
47198
|
+
` finish. However short the work looks \u2014 a run of a few minutes is exactly`,
|
|
47199
|
+
` the case nobody sees. A board that only changes at the end shows an idle`,
|
|
47200
|
+
` queue while the work is happening, and neither a colleague nor another`,
|
|
47201
|
+
` agent choosing what to pick up can tell a card nobody has touched from one`,
|
|
47202
|
+
` being worked right now. If the workflow has no such state, leave it where`,
|
|
47203
|
+
` it is. A mention is the exception \u2014 see below.`,
|
|
47060
47204
|
`- You may be given an issue because someone MENTIONED you on it rather than`,
|
|
47061
47205
|
` because your function owns its state. The prompt says which. A mention is`,
|
|
47062
47206
|
` a question from a colleague: read the comments with list_comments, answer`,
|
|
@@ -47077,14 +47221,16 @@ function userPrompt(hit) {
|
|
|
47077
47221
|
if (hit.reason === "assigned") {
|
|
47078
47222
|
return [
|
|
47079
47223
|
`${hit.ref} \u2014 "${hit.issue.title}" \u2014 is assigned to you, in the "${hit.state.name}"`,
|
|
47080
|
-
`state.
|
|
47224
|
+
`state. Claim it first \u2014 move it to the state your workflow uses for work in`,
|
|
47225
|
+
`progress \u2014 then do it. When you are finished, hand it on: move it to the state that`,
|
|
47081
47226
|
`comes next and assign it to whoever owns that step. Leaving it assigned to`,
|
|
47082
47227
|
`you, in this state, means it comes back to you.`
|
|
47083
47228
|
].join(" ");
|
|
47084
47229
|
}
|
|
47085
47230
|
return [
|
|
47086
47231
|
`${hit.ref} \u2014 "${hit.issue.title}" \u2014 is in the "${hit.state.name}" state,`,
|
|
47087
|
-
`which your function owns.
|
|
47232
|
+
`which your function owns. Claim it first \u2014 move it to the state your workflow`,
|
|
47233
|
+
`uses for work in progress \u2014 then do your part.`
|
|
47088
47234
|
].join(" ");
|
|
47089
47235
|
}
|
|
47090
47236
|
function headFor(head2) {
|
|
@@ -47436,6 +47582,7 @@ async function tick(ctx, identity, deadTicks, allowance) {
|
|
|
47436
47582
|
let committed = 0;
|
|
47437
47583
|
let salvaged = null;
|
|
47438
47584
|
const stopBusyBeat = beatWhileBusy(ctx, deadTicks);
|
|
47585
|
+
currentRun = { ref: hit.ref, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
47439
47586
|
try {
|
|
47440
47587
|
run = await invokeHarness(ctx, identity, hit, worktree);
|
|
47441
47588
|
if (worktree) committed = await worktree.commits();
|
|
@@ -47459,6 +47606,7 @@ async function tick(ctx, identity, deadTicks, allowance) {
|
|
|
47459
47606
|
}
|
|
47460
47607
|
} finally {
|
|
47461
47608
|
stopBusyBeat();
|
|
47609
|
+
currentRun = null;
|
|
47462
47610
|
await release(ctx, hit);
|
|
47463
47611
|
}
|
|
47464
47612
|
const code = run.code;
|
|
@@ -47527,6 +47675,22 @@ async function tick(ctx, identity, deadTicks, allowance) {
|
|
|
47527
47675
|
});
|
|
47528
47676
|
return { invoked, failedToRun, suppressed: false, waiting: actionable.length };
|
|
47529
47677
|
}
|
|
47678
|
+
function interruptibleWait(ms2) {
|
|
47679
|
+
return new Promise((resolve2) => {
|
|
47680
|
+
const timer = setTimeout(() => {
|
|
47681
|
+
pendingWait = null;
|
|
47682
|
+
resolve2();
|
|
47683
|
+
}, ms2);
|
|
47684
|
+
pendingWait = { timer, resolve: resolve2 };
|
|
47685
|
+
});
|
|
47686
|
+
}
|
|
47687
|
+
function interruptWait() {
|
|
47688
|
+
if (!pendingWait) return;
|
|
47689
|
+
clearTimeout(pendingWait.timer);
|
|
47690
|
+
const { resolve: resolve2 } = pendingWait;
|
|
47691
|
+
pendingWait = null;
|
|
47692
|
+
resolve2();
|
|
47693
|
+
}
|
|
47530
47694
|
function installControls() {
|
|
47531
47695
|
const requestStop = (why) => {
|
|
47532
47696
|
if (stopping && Date.now() - stopRequestedAt < 500) return;
|
|
@@ -47536,7 +47700,14 @@ function installControls() {
|
|
|
47536
47700
|
}
|
|
47537
47701
|
stopping = true;
|
|
47538
47702
|
stopRequestedAt = Date.now();
|
|
47539
|
-
|
|
47703
|
+
if (currentRun) {
|
|
47704
|
+
log(
|
|
47705
|
+
` ${why} \u2014 finishing the current run (${currentRun.ref}), running ${since(currentRun.startedAt)} so far, up to ${Math.round(options.harnessTimeoutMs / 1e3)}s before it is killed. Again to leave now.`
|
|
47706
|
+
);
|
|
47707
|
+
} else {
|
|
47708
|
+
log(` ${why} \u2014 idle, nothing to finish, leaving now.`);
|
|
47709
|
+
}
|
|
47710
|
+
interruptWait();
|
|
47540
47711
|
};
|
|
47541
47712
|
process.on("SIGTERM", () => requestStop("stop requested"));
|
|
47542
47713
|
process.on("SIGINT", () => requestStop("interrupted"));
|
|
@@ -47584,6 +47755,27 @@ first with no way to be stopped except by pid.
|
|
|
47584
47755
|
|
|
47585
47756
|
stop it cruo stop ${runName}
|
|
47586
47757
|
look at it cruo ps
|
|
47758
|
+
`
|
|
47759
|
+
);
|
|
47760
|
+
process.exit(2);
|
|
47761
|
+
}
|
|
47762
|
+
const ownArgv = await processArgv(process.pid);
|
|
47763
|
+
const implicit = !ownArgv || asNameFrom(ownArgv) === null;
|
|
47764
|
+
const runningUntracked = (await untrackedSupervisors()).filter(
|
|
47765
|
+
(p) => p.asName === runName || implicit && p.asName === null
|
|
47766
|
+
);
|
|
47767
|
+
if (runningUntracked.length > 0) {
|
|
47768
|
+
const pids = runningUntracked.map((p) => p.pid).join(", ");
|
|
47769
|
+
console.error(
|
|
47770
|
+
`
|
|
47771
|
+
${runName} looks like it is already running here \u2014 untracked, pid ${pids} (found in the process table, no run record).
|
|
47772
|
+
|
|
47773
|
+
Two supervisors for one agent share a token, a queue and a heartbeat,
|
|
47774
|
+
and starting this one anyway repeats the exact incident this check exists
|
|
47775
|
+
to catch: a second supervisor nothing could see or stop.
|
|
47776
|
+
|
|
47777
|
+
look at it cruo ps
|
|
47778
|
+
stop it cruo stop ${runName}
|
|
47587
47779
|
`
|
|
47588
47780
|
);
|
|
47589
47781
|
process.exit(2);
|
|
@@ -47678,11 +47870,11 @@ first with no way to be stopped except by pid.
|
|
|
47678
47870
|
}
|
|
47679
47871
|
if (options.once) return;
|
|
47680
47872
|
if (stopping) return;
|
|
47681
|
-
await
|
|
47873
|
+
await interruptibleWait(options.intervalMs);
|
|
47682
47874
|
if (stopping) return;
|
|
47683
47875
|
}
|
|
47684
47876
|
}
|
|
47685
|
-
var argv, flag, opt, num2, ms, readOptions, options, log, worktreeConfig, lastRefusal, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV, HEAD_STORED, lastHolding, warnedAboutSpendTable, stopping, paused, stopRequestedAt;
|
|
47877
|
+
var argv, flag, opt, num2, ms, readOptions, options, log, worktreeConfig, lastRefusal, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV, HEAD_STORED, lastHolding, warnedAboutSpendTable, stopping, paused, stopRequestedAt, currentRun, pendingWait;
|
|
47686
47878
|
var init_supervisor = __esm({
|
|
47687
47879
|
"src/supervisor.ts"() {
|
|
47688
47880
|
"use strict";
|
|
@@ -47829,6 +48021,12 @@ var init_supervisor = __esm({
|
|
|
47829
48021
|
});
|
|
47830
48022
|
options = (() => {
|
|
47831
48023
|
try {
|
|
48024
|
+
const [unknown2] = unknownFlags(argv, SUPERVISOR_VALUED);
|
|
48025
|
+
if (unknown2) {
|
|
48026
|
+
throw new OptionError(
|
|
48027
|
+
`${unknown2.flag} is not a flag the supervisor reads` + (unknown2.nearest ? ` \u2014 did you mean ${unknown2.nearest}?` : ".") + ` Nothing was started.`
|
|
48028
|
+
);
|
|
48029
|
+
}
|
|
47832
48030
|
return readOptions();
|
|
47833
48031
|
} catch (error51) {
|
|
47834
48032
|
if (error51 instanceof OptionError) {
|
|
@@ -47870,6 +48068,8 @@ cruo-supervisor: ${error51.message}
|
|
|
47870
48068
|
stopping = false;
|
|
47871
48069
|
paused = false;
|
|
47872
48070
|
stopRequestedAt = 0;
|
|
48071
|
+
currentRun = null;
|
|
48072
|
+
pendingWait = null;
|
|
47873
48073
|
main().then(async () => {
|
|
47874
48074
|
if (!options.once && !options.dryRun) {
|
|
47875
48075
|
try {
|
|
@@ -47914,8 +48114,10 @@ var USAGE = `cruo \u2014 run a Cruo agent
|
|
|
47914
48114
|
cruo agents list what is remembered (never the tokens)
|
|
47915
48115
|
cruo usage [--days 7] what the agents have been spending
|
|
47916
48116
|
cruo logout [name] forget one, or --all
|
|
47917
|
-
cruo [options]
|
|
47918
|
-
cruo --as <name> \u2026 run a particular one
|
|
48117
|
+
cruo run [options] run the agent's supervisor, in this terminal
|
|
48118
|
+
cruo --as <name> \u2026 run a particular one (flags alone also run it)
|
|
48119
|
+
cruo version which build this is
|
|
48120
|
+
cruo help this list (also: cruo, cruo -h, cruo ?)
|
|
47919
48121
|
|
|
47920
48122
|
Running one in the background
|
|
47921
48123
|
cruo start [options] same, detached \u2014 survives closing the terminal
|
|
@@ -48027,7 +48229,7 @@ ${CONFIG_PATH} is readable only by you.
|
|
|
48027
48229
|
|
|
48028
48230
|
` + (config3.default === name ? `Run \`cruo --dry-run\` to see what it would pick up.
|
|
48029
48231
|
` : `Run \`cruo --as ${name} --dry-run\` to see what it would pick up.
|
|
48030
|
-
|
|
48232
|
+
\`cruo run\` still runs \`${config3.default}\`.
|
|
48031
48233
|
`)
|
|
48032
48234
|
);
|
|
48033
48235
|
}
|
|
@@ -48078,7 +48280,7 @@ Nothing stored for \`${which}\`. Stored: ${names.join(", ") || "(none)"}
|
|
|
48078
48280
|
await writeConfig(config3);
|
|
48079
48281
|
console.log(
|
|
48080
48282
|
`
|
|
48081
|
-
Forgot the token for \`${which}\`.` + (config3.default ? `
|
|
48283
|
+
Forgot the token for \`${which}\`.` + (config3.default ? ` \`cruo run\` now runs \`${config3.default}\`.
|
|
48082
48284
|
` : ` Nothing is stored now.
|
|
48083
48285
|
`)
|
|
48084
48286
|
);
|
|
@@ -48100,7 +48302,7 @@ No tokens stored. \`cruo login <token>\` to add one.
|
|
|
48100
48302
|
console.log(` ${mark} ${name}${who}`);
|
|
48101
48303
|
}
|
|
48102
48304
|
console.log(`
|
|
48103
|
-
* is what
|
|
48305
|
+
* is what \`cruo run\` runs. Use \`--as <name>\` for another.
|
|
48104
48306
|
`);
|
|
48105
48307
|
}
|
|
48106
48308
|
async function targetName(explicit, asName) {
|
|
@@ -48109,28 +48311,72 @@ async function targetName(explicit, asName) {
|
|
|
48109
48311
|
const config3 = await readConfig2();
|
|
48110
48312
|
return safeName(config3.default ?? "default");
|
|
48111
48313
|
}
|
|
48314
|
+
async function untrackedMatchingForRefusal(name, implicit) {
|
|
48315
|
+
return (await untrackedSupervisors()).filter((p) => p.asName === name || implicit && p.asName === null);
|
|
48316
|
+
}
|
|
48317
|
+
function reportUnidentified(procs) {
|
|
48318
|
+
if (procs.length === 0) return false;
|
|
48319
|
+
const pids = procs.map((p) => p.pid).join(", ");
|
|
48320
|
+
console.error(
|
|
48321
|
+
`
|
|
48322
|
+
${procs.length} more untracked ${procs.length === 1 ? "process" : "processes"} found with no \`--as\` of its own to confirm identity: pid ${pids}.
|
|
48323
|
+
Not signalled \u2014 a wrong \`stop\` is far more costly than a missed one. Look at it yourself:
|
|
48324
|
+
|
|
48325
|
+
cruo ps
|
|
48326
|
+
kill -TERM <pid> (if you are sure)
|
|
48327
|
+
`
|
|
48328
|
+
);
|
|
48329
|
+
return true;
|
|
48330
|
+
}
|
|
48112
48331
|
async function targets(explicit, asName, all) {
|
|
48113
48332
|
if (all) {
|
|
48114
48333
|
const rows = await listRecords();
|
|
48115
|
-
|
|
48334
|
+
const tracked = rows.filter((r) => r.live.state === "running").map((r) => ({ agent: r.agent, pid: r.live.record.pid, untracked: false }));
|
|
48335
|
+
const discovered = await untrackedSupervisors();
|
|
48336
|
+
const untracked = discovered.filter((p) => p.asName !== null).map((p) => ({ agent: p.asName, pid: p.pid, untracked: true }));
|
|
48337
|
+
const reported = reportUnidentified(discovered.filter((p) => p.asName === null));
|
|
48338
|
+
if (!reported && tracked.length === 0 && untracked.length === 0) console.log(`
|
|
48339
|
+
Nothing running.
|
|
48340
|
+
`);
|
|
48341
|
+
return [...tracked, ...untracked];
|
|
48116
48342
|
}
|
|
48117
48343
|
const name = await targetName(explicit, asName);
|
|
48344
|
+
const implicit = explicit === null && asName === null;
|
|
48118
48345
|
const live = await liveness(name, true);
|
|
48119
|
-
|
|
48120
|
-
if (live.state === "
|
|
48121
|
-
|
|
48346
|
+
const out = [];
|
|
48347
|
+
if (live.state === "running") out.push({ agent: name, pid: live.record.pid, untracked: false });
|
|
48348
|
+
const matches = (await untrackedSupervisors()).filter((p) => p.asName === name);
|
|
48349
|
+
out.push(...matches.map((p) => ({ agent: name, pid: p.pid, untracked: true })));
|
|
48350
|
+
if (out.length === 0) {
|
|
48351
|
+
const unidentified = implicit ? (await untrackedSupervisors()).filter((p) => p.asName === null) : [];
|
|
48352
|
+
if (unidentified.length > 0) {
|
|
48353
|
+
const pids = unidentified.map((p) => p.pid).join(", ");
|
|
48354
|
+
console.error(
|
|
48355
|
+
`
|
|
48356
|
+
${name} \u2014 can't confirm. ${unidentified.length} untracked ${unidentified.length === 1 ? "process" : "processes"} found with no \`--as\` of its own (pid ${pids}) \u2014 exactly how ${name} would look if it is running as this machine's default identity, but nothing here can tell that apart from an
|
|
48357
|
+
unrelated process sharing this account.
|
|
48358
|
+
|
|
48359
|
+
Not signalled. Look at it yourself, then act if you are sure:
|
|
48360
|
+
|
|
48361
|
+
cruo ps
|
|
48362
|
+
kill -TERM <pid>
|
|
48363
|
+
`
|
|
48364
|
+
);
|
|
48365
|
+
} else if (live.state === "gone") {
|
|
48366
|
+
console.error(`
|
|
48122
48367
|
${name} is not running (it left a record behind; cleared).
|
|
48123
48368
|
`);
|
|
48124
|
-
|
|
48125
|
-
|
|
48369
|
+
} else if (live.state === "recycled") {
|
|
48370
|
+
console.error(`
|
|
48126
48371
|
${name} is not running \u2014 its pid now belongs to something else. Cleared.
|
|
48127
48372
|
`);
|
|
48128
|
-
|
|
48129
|
-
|
|
48373
|
+
} else {
|
|
48374
|
+
console.error(`
|
|
48130
48375
|
${name} is not running. Start it with \`cruo start --as ${name}\`.
|
|
48131
48376
|
`);
|
|
48377
|
+
}
|
|
48132
48378
|
}
|
|
48133
|
-
return
|
|
48379
|
+
return out;
|
|
48134
48380
|
}
|
|
48135
48381
|
async function startDetached(argv2, asName) {
|
|
48136
48382
|
const { spawn: spawn2 } = await import("node:child_process");
|
|
@@ -48158,6 +48404,22 @@ Stop it first, or use --as <name> to run a different agent.
|
|
|
48158
48404
|
);
|
|
48159
48405
|
process.exit(1);
|
|
48160
48406
|
}
|
|
48407
|
+
const runningUntracked = await untrackedMatchingForRefusal(name, !positional && !asName);
|
|
48408
|
+
if (runningUntracked.length > 0) {
|
|
48409
|
+
const pids = runningUntracked.map((p) => p.pid).join(", ");
|
|
48410
|
+
console.error(
|
|
48411
|
+
`
|
|
48412
|
+
${name} looks like it is already running here \u2014 untracked, pid ${pids} (found in the process table, no run record).
|
|
48413
|
+
|
|
48414
|
+
Two supervisors for one agent share a token, a queue and a heartbeat.
|
|
48415
|
+
Look at it, then stop it if it should not be there:
|
|
48416
|
+
|
|
48417
|
+
cruo ps
|
|
48418
|
+
cruo stop ${name}
|
|
48419
|
+
`
|
|
48420
|
+
);
|
|
48421
|
+
process.exit(2);
|
|
48422
|
+
}
|
|
48161
48423
|
const config3 = await readConfig2();
|
|
48162
48424
|
const storedKey = Object.keys(config3.agents).find((k) => safeName(k) === name) ?? null;
|
|
48163
48425
|
const hasToken = Boolean(process.env.CRUO_TOKEN?.trim()) || argv2.includes("--token") || storedKey !== null;
|
|
@@ -48175,13 +48437,14 @@ Either \`cruo start ${stored[0]}\`, or add this one:
|
|
|
48175
48437
|
);
|
|
48176
48438
|
process.exit(2);
|
|
48177
48439
|
}
|
|
48440
|
+
refuseUnknownFlags(argv2.slice(positional ? 2 : 1).filter((a) => a !== "--all"));
|
|
48178
48441
|
await mkdir4(logDir(), { recursive: true });
|
|
48179
48442
|
const logPath = join4(logDir(), `${name}.log`);
|
|
48180
48443
|
const handle = await open(logPath, "a");
|
|
48181
48444
|
const rest = argv2.slice(positional ? 2 : 1).filter((a) => a !== "--all");
|
|
48182
48445
|
const chosen = storedKey ?? positional ?? asName;
|
|
48183
48446
|
const withoutAs = rest.filter((a, i, all) => a !== "--as" && all[i - 1] !== "--as");
|
|
48184
|
-
const forwarded = chosen ? ["--as", chosen, ...withoutAs]
|
|
48447
|
+
const forwarded = ["run", ...chosen ? ["--as", chosen] : [], ...withoutAs];
|
|
48185
48448
|
const child = spawn2(process.execPath, [process.argv[1], ...forwarded], {
|
|
48186
48449
|
detached: true,
|
|
48187
48450
|
stdio: ["ignore", handle.fd, handle.fd],
|
|
@@ -48206,7 +48469,8 @@ https://cruo.space/docs#agents.
|
|
|
48206
48469
|
async function listProcesses() {
|
|
48207
48470
|
const rows = await listRecords();
|
|
48208
48471
|
const running = rows.filter((r) => r.live.state === "running");
|
|
48209
|
-
|
|
48472
|
+
const untracked = await untrackedSupervisors();
|
|
48473
|
+
if (rows.length === 0 && untracked.length === 0) {
|
|
48210
48474
|
console.log(`
|
|
48211
48475
|
Nothing running. Start one with \`cruo start\`.
|
|
48212
48476
|
`);
|
|
@@ -48228,9 +48492,25 @@ Nothing running. Start one with \`cruo start\`.
|
|
|
48228
48492
|
await removeRecord(agent);
|
|
48229
48493
|
}
|
|
48230
48494
|
}
|
|
48495
|
+
for (const proc of untracked) {
|
|
48496
|
+
const name = proc.asName ?? "(unknown identity)";
|
|
48497
|
+
const flags = proc.argv.filter((a) => a.startsWith("--")).join(" ") || "(no flags)";
|
|
48498
|
+
const up = proc.procStartedAt ? since(proc.procStartedAt) : "?";
|
|
48499
|
+
console.log(
|
|
48500
|
+
` ${name.padEnd(14)} ${String(proc.pid).padEnd(8)} ${up.padEnd(8)} ${flags} \u2014 UNTRACKED (no run record; found in the process table)`
|
|
48501
|
+
);
|
|
48502
|
+
}
|
|
48503
|
+
if (untracked.length > 0) {
|
|
48504
|
+
const named = untracked.filter((p) => p.asName !== null).length;
|
|
48505
|
+
const unnamed = untracked.length - named;
|
|
48506
|
+
console.log(
|
|
48507
|
+
`
|
|
48508
|
+
${untracked.length} untracked ${untracked.length === 1 ? "process" : "processes"} found with no run record.` + (named > 0 ? ` \`cruo stop <name>\` can reach ${named === 1 ? "the one" : `${named}`} whose own argv named it with --as.` : "") + (unnamed > 0 ? ` ${unnamed} ${unnamed === 1 ? "has" : "have"} no \`--as\` of its own \u2014 \`cruo stop\` will report ${unnamed === 1 ? "its" : "their"} pid but never signal ${unnamed === 1 ? "it" : "them"} without one.` : "")
|
|
48509
|
+
);
|
|
48510
|
+
}
|
|
48231
48511
|
console.log(
|
|
48232
48512
|
`
|
|
48233
|
-
${running.length} running. Whether each is actually picking up work is on the board, not here \u2014
|
|
48513
|
+
${running.length + untracked.length} running. Whether each is actually picking up work is on the board, not here \u2014
|
|
48234
48514
|
Cruo \u2192 Settings \u2192 Members shows what it is holding and when it last looked.
|
|
48235
48515
|
`
|
|
48236
48516
|
);
|
|
@@ -48238,29 +48518,27 @@ Nothing running. Start one with \`cruo start\`.
|
|
|
48238
48518
|
async function control(which, explicit, asName, all) {
|
|
48239
48519
|
const records = await targets(explicit, asName, all);
|
|
48240
48520
|
if (records.length === 0) {
|
|
48241
|
-
if (all) console.log(`
|
|
48242
|
-
Nothing running.
|
|
48243
|
-
`);
|
|
48244
48521
|
process.exitCode = all ? 0 : 1;
|
|
48245
48522
|
return;
|
|
48246
48523
|
}
|
|
48247
48524
|
for (const record2 of records) {
|
|
48525
|
+
const tag = record2.untracked ? ` (untracked, pid ${record2.pid})` : "";
|
|
48248
48526
|
const sent = signal(record2.pid, which);
|
|
48249
48527
|
if (!sent) {
|
|
48250
|
-
console.error(` ${record2.agent}: could not signal pid ${record2.pid} \u2014 it may have just exited`);
|
|
48251
|
-
await removeRecord(record2.agent);
|
|
48528
|
+
console.error(` ${record2.agent}${tag}: could not signal pid ${record2.pid} \u2014 it may have just exited`);
|
|
48529
|
+
if (!record2.untracked) await removeRecord(record2.agent);
|
|
48252
48530
|
continue;
|
|
48253
48531
|
}
|
|
48254
48532
|
if (which === "stop") {
|
|
48255
48533
|
console.log(
|
|
48256
|
-
` ${record2.agent}: asked to stop. It will finish the run it is on first \u2014
|
|
48534
|
+
` ${record2.agent}${tag}: asked to stop. It will finish the run it is on first \u2014
|
|
48257
48535
|
a harness mid-run keeps its work, which is the point of asking rather than killing.
|
|
48258
48536
|
Watch it leave with: cruo logs ${record2.agent} -f`
|
|
48259
48537
|
);
|
|
48260
48538
|
} else if (which === "pause") {
|
|
48261
|
-
console.log(` ${record2.agent}: paused. Still watching the board and still reporting; starts nothing new.`);
|
|
48539
|
+
console.log(` ${record2.agent}${tag}: paused. Still watching the board and still reporting; starts nothing new.`);
|
|
48262
48540
|
} else {
|
|
48263
|
-
console.log(` ${record2.agent}: resumed.`);
|
|
48541
|
+
console.log(` ${record2.agent}${tag}: resumed.`);
|
|
48264
48542
|
}
|
|
48265
48543
|
}
|
|
48266
48544
|
console.log("");
|
|
@@ -48285,6 +48563,26 @@ A detached run writes one; a foreground run prints to its terminal.
|
|
|
48285
48563
|
});
|
|
48286
48564
|
await new Promise((resolve2) => child.on("close", resolve2));
|
|
48287
48565
|
}
|
|
48566
|
+
async function printVersion() {
|
|
48567
|
+
try {
|
|
48568
|
+
const pkg = JSON.parse(await readFile3(new URL("../package.json", import.meta.url), "utf8"));
|
|
48569
|
+
console.log(`${pkg.name} ${pkg.version}`);
|
|
48570
|
+
} catch {
|
|
48571
|
+
console.log("unknown \u2014 no package.json beside this build");
|
|
48572
|
+
}
|
|
48573
|
+
}
|
|
48574
|
+
function refuseUnknownFlags(argv2) {
|
|
48575
|
+
const unknown2 = unknownFlags(argv2);
|
|
48576
|
+
if (unknown2.length === 0) return;
|
|
48577
|
+
console.error(
|
|
48578
|
+
`
|
|
48579
|
+
${unknown2.map((u) => `\`${u.flag}\` is not a cruo flag.` + (u.nearest ? ` Did you mean ${u.nearest}?` : "")).join("\n")}
|
|
48580
|
+
|
|
48581
|
+
Nothing was started. Everything it takes: cruo help
|
|
48582
|
+
`
|
|
48583
|
+
);
|
|
48584
|
+
process.exit(2);
|
|
48585
|
+
}
|
|
48288
48586
|
async function main2() {
|
|
48289
48587
|
const argv2 = process.argv.slice(2);
|
|
48290
48588
|
const [first] = argv2;
|
|
@@ -48321,10 +48619,11 @@ Usage: cruo login <token> [--as <name>]
|
|
|
48321
48619
|
}
|
|
48322
48620
|
if (first === "usage") {
|
|
48323
48621
|
}
|
|
48324
|
-
if (first === "help" || argv2.includes("--help") || argv2.includes("-h")) {
|
|
48622
|
+
if (first === void 0 || first === "help" || first === "?" || argv2.includes("--help") || argv2.includes("-h")) {
|
|
48325
48623
|
console.log(USAGE);
|
|
48326
48624
|
return;
|
|
48327
48625
|
}
|
|
48626
|
+
if (first === "version" || first === "--version" || first === "-v") return printVersion();
|
|
48328
48627
|
if (!isKnownCommandWord(first)) {
|
|
48329
48628
|
const near = nearestCommands(first);
|
|
48330
48629
|
console.error(
|
|
@@ -48333,13 +48632,15 @@ Usage: cruo login <token> [--as <name>]
|
|
|
48333
48632
|
|
|
48334
48633
|
` + (near.length > 0 ? `Did you mean: ${near.map((k) => `cruo ${k}`).join(" ")}
|
|
48335
48634
|
|
|
48336
|
-
` : "") + `To run the agent
|
|
48337
|
-
Everything it takes:
|
|
48635
|
+
` : "") + `To run the agent: cruo run [options]
|
|
48636
|
+
Everything it takes: cruo help
|
|
48338
48637
|
`
|
|
48339
48638
|
);
|
|
48340
48639
|
process.exit(2);
|
|
48341
48640
|
}
|
|
48342
48641
|
if (first === "start") return startDetached(argv2, flagValue("--as"));
|
|
48642
|
+
const runArgv = first === "run" ? argv2.slice(1) : argv2;
|
|
48643
|
+
if (first !== "usage") refuseUnknownFlags(runArgv);
|
|
48343
48644
|
const flagIndex = argv2.indexOf("--token");
|
|
48344
48645
|
const fromFlag = flagIndex >= 0 ? argv2[flagIndex + 1] : void 0;
|
|
48345
48646
|
if (fromFlag) {
|
|
@@ -48379,7 +48680,7 @@ Get one from Cruo \u2192 Settings \u2192 Members \u2192 Add an agent.
|
|
|
48379
48680
|
process.env.CRUO_IDENTITY = asName ?? config3.default ?? "";
|
|
48380
48681
|
}
|
|
48381
48682
|
process.env.CRUO_TOKEN = token;
|
|
48382
|
-
const passthrough = stripTokenFlag(
|
|
48683
|
+
const passthrough = stripTokenFlag(runArgv).filter((a, i, all) => a !== "--as" && all[i - 1] !== "--as");
|
|
48383
48684
|
process.argv = [process.argv[0], process.argv[1], ...passthrough];
|
|
48384
48685
|
if (first === "usage") {
|
|
48385
48686
|
const { reportUsage: reportUsage2 } = await Promise.resolve().then(() => (init_usage(), usage_exports));
|
package/package.json
CHANGED