cruo-agent 0.1.9 → 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 +240 -25
- package/package.json +1 -1
package/dist/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.10
|
package/dist/cli.js
CHANGED
|
@@ -46105,6 +46105,83 @@ async function listRecords() {
|
|
|
46105
46105
|
}
|
|
46106
46106
|
return out;
|
|
46107
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
|
+
}
|
|
46108
46185
|
function signal(pid, which) {
|
|
46109
46186
|
try {
|
|
46110
46187
|
process.kill(pid, SIGNALS[which]);
|
|
@@ -46121,14 +46198,19 @@ function since(iso) {
|
|
|
46121
46198
|
if (ms2 < 864e5) return `${Math.round(ms2 / 36e5)}h`;
|
|
46122
46199
|
return `${Math.round(ms2 / 864e5)}d`;
|
|
46123
46200
|
}
|
|
46124
|
-
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;
|
|
46125
46202
|
var init_process = __esm({
|
|
46126
46203
|
"src/process.ts"() {
|
|
46127
46204
|
"use strict";
|
|
46205
|
+
init_options();
|
|
46128
46206
|
exec = promisify(execFile);
|
|
46129
46207
|
runDir = () => join(homedir(), ".cruo", "run");
|
|
46130
46208
|
logDir = () => join(homedir(), ".cruo", "logs");
|
|
46131
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");
|
|
46132
46214
|
SIGNALS = {
|
|
46133
46215
|
stop: "SIGTERM",
|
|
46134
46216
|
pause: "SIGUSR1",
|
|
@@ -47111,6 +47193,14 @@ function systemPrompt(ctx, identity, worktree) {
|
|
|
47111
47193
|
`- If you cannot do the work \u2014 missing information, ambiguous brief, outside`,
|
|
47112
47194
|
` your capabilities \u2014 say so in a comment and assign it to someone who can,`,
|
|
47113
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.`,
|
|
47114
47204
|
`- You may be given an issue because someone MENTIONED you on it rather than`,
|
|
47115
47205
|
` because your function owns its state. The prompt says which. A mention is`,
|
|
47116
47206
|
` a question from a colleague: read the comments with list_comments, answer`,
|
|
@@ -47131,14 +47221,16 @@ function userPrompt(hit) {
|
|
|
47131
47221
|
if (hit.reason === "assigned") {
|
|
47132
47222
|
return [
|
|
47133
47223
|
`${hit.ref} \u2014 "${hit.issue.title}" \u2014 is assigned to you, in the "${hit.state.name}"`,
|
|
47134
|
-
`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`,
|
|
47135
47226
|
`comes next and assign it to whoever owns that step. Leaving it assigned to`,
|
|
47136
47227
|
`you, in this state, means it comes back to you.`
|
|
47137
47228
|
].join(" ");
|
|
47138
47229
|
}
|
|
47139
47230
|
return [
|
|
47140
47231
|
`${hit.ref} \u2014 "${hit.issue.title}" \u2014 is in the "${hit.state.name}" state,`,
|
|
47141
|
-
`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.`
|
|
47142
47234
|
].join(" ");
|
|
47143
47235
|
}
|
|
47144
47236
|
function headFor(head2) {
|
|
@@ -47490,6 +47582,7 @@ async function tick(ctx, identity, deadTicks, allowance) {
|
|
|
47490
47582
|
let committed = 0;
|
|
47491
47583
|
let salvaged = null;
|
|
47492
47584
|
const stopBusyBeat = beatWhileBusy(ctx, deadTicks);
|
|
47585
|
+
currentRun = { ref: hit.ref, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
47493
47586
|
try {
|
|
47494
47587
|
run = await invokeHarness(ctx, identity, hit, worktree);
|
|
47495
47588
|
if (worktree) committed = await worktree.commits();
|
|
@@ -47513,6 +47606,7 @@ async function tick(ctx, identity, deadTicks, allowance) {
|
|
|
47513
47606
|
}
|
|
47514
47607
|
} finally {
|
|
47515
47608
|
stopBusyBeat();
|
|
47609
|
+
currentRun = null;
|
|
47516
47610
|
await release(ctx, hit);
|
|
47517
47611
|
}
|
|
47518
47612
|
const code = run.code;
|
|
@@ -47581,6 +47675,22 @@ async function tick(ctx, identity, deadTicks, allowance) {
|
|
|
47581
47675
|
});
|
|
47582
47676
|
return { invoked, failedToRun, suppressed: false, waiting: actionable.length };
|
|
47583
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
|
+
}
|
|
47584
47694
|
function installControls() {
|
|
47585
47695
|
const requestStop = (why) => {
|
|
47586
47696
|
if (stopping && Date.now() - stopRequestedAt < 500) return;
|
|
@@ -47590,7 +47700,14 @@ function installControls() {
|
|
|
47590
47700
|
}
|
|
47591
47701
|
stopping = true;
|
|
47592
47702
|
stopRequestedAt = Date.now();
|
|
47593
|
-
|
|
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();
|
|
47594
47711
|
};
|
|
47595
47712
|
process.on("SIGTERM", () => requestStop("stop requested"));
|
|
47596
47713
|
process.on("SIGINT", () => requestStop("interrupted"));
|
|
@@ -47638,6 +47755,27 @@ first with no way to be stopped except by pid.
|
|
|
47638
47755
|
|
|
47639
47756
|
stop it cruo stop ${runName}
|
|
47640
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}
|
|
47641
47779
|
`
|
|
47642
47780
|
);
|
|
47643
47781
|
process.exit(2);
|
|
@@ -47732,11 +47870,11 @@ first with no way to be stopped except by pid.
|
|
|
47732
47870
|
}
|
|
47733
47871
|
if (options.once) return;
|
|
47734
47872
|
if (stopping) return;
|
|
47735
|
-
await
|
|
47873
|
+
await interruptibleWait(options.intervalMs);
|
|
47736
47874
|
if (stopping) return;
|
|
47737
47875
|
}
|
|
47738
47876
|
}
|
|
47739
|
-
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;
|
|
47740
47878
|
var init_supervisor = __esm({
|
|
47741
47879
|
"src/supervisor.ts"() {
|
|
47742
47880
|
"use strict";
|
|
@@ -47930,6 +48068,8 @@ cruo-supervisor: ${error51.message}
|
|
|
47930
48068
|
stopping = false;
|
|
47931
48069
|
paused = false;
|
|
47932
48070
|
stopRequestedAt = 0;
|
|
48071
|
+
currentRun = null;
|
|
48072
|
+
pendingWait = null;
|
|
47933
48073
|
main().then(async () => {
|
|
47934
48074
|
if (!options.once && !options.dryRun) {
|
|
47935
48075
|
try {
|
|
@@ -48171,28 +48311,72 @@ async function targetName(explicit, asName) {
|
|
|
48171
48311
|
const config3 = await readConfig2();
|
|
48172
48312
|
return safeName(config3.default ?? "default");
|
|
48173
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
|
+
}
|
|
48174
48331
|
async function targets(explicit, asName, all) {
|
|
48175
48332
|
if (all) {
|
|
48176
48333
|
const rows = await listRecords();
|
|
48177
|
-
|
|
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];
|
|
48178
48342
|
}
|
|
48179
48343
|
const name = await targetName(explicit, asName);
|
|
48344
|
+
const implicit = explicit === null && asName === null;
|
|
48180
48345
|
const live = await liveness(name, true);
|
|
48181
|
-
|
|
48182
|
-
if (live.state === "
|
|
48183
|
-
|
|
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(`
|
|
48184
48367
|
${name} is not running (it left a record behind; cleared).
|
|
48185
48368
|
`);
|
|
48186
|
-
|
|
48187
|
-
|
|
48369
|
+
} else if (live.state === "recycled") {
|
|
48370
|
+
console.error(`
|
|
48188
48371
|
${name} is not running \u2014 its pid now belongs to something else. Cleared.
|
|
48189
48372
|
`);
|
|
48190
|
-
|
|
48191
|
-
|
|
48373
|
+
} else {
|
|
48374
|
+
console.error(`
|
|
48192
48375
|
${name} is not running. Start it with \`cruo start --as ${name}\`.
|
|
48193
48376
|
`);
|
|
48377
|
+
}
|
|
48194
48378
|
}
|
|
48195
|
-
return
|
|
48379
|
+
return out;
|
|
48196
48380
|
}
|
|
48197
48381
|
async function startDetached(argv2, asName) {
|
|
48198
48382
|
const { spawn: spawn2 } = await import("node:child_process");
|
|
@@ -48220,6 +48404,22 @@ Stop it first, or use --as <name> to run a different agent.
|
|
|
48220
48404
|
);
|
|
48221
48405
|
process.exit(1);
|
|
48222
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
|
+
}
|
|
48223
48423
|
const config3 = await readConfig2();
|
|
48224
48424
|
const storedKey = Object.keys(config3.agents).find((k) => safeName(k) === name) ?? null;
|
|
48225
48425
|
const hasToken = Boolean(process.env.CRUO_TOKEN?.trim()) || argv2.includes("--token") || storedKey !== null;
|
|
@@ -48269,7 +48469,8 @@ https://cruo.space/docs#agents.
|
|
|
48269
48469
|
async function listProcesses() {
|
|
48270
48470
|
const rows = await listRecords();
|
|
48271
48471
|
const running = rows.filter((r) => r.live.state === "running");
|
|
48272
|
-
|
|
48472
|
+
const untracked = await untrackedSupervisors();
|
|
48473
|
+
if (rows.length === 0 && untracked.length === 0) {
|
|
48273
48474
|
console.log(`
|
|
48274
48475
|
Nothing running. Start one with \`cruo start\`.
|
|
48275
48476
|
`);
|
|
@@ -48291,9 +48492,25 @@ Nothing running. Start one with \`cruo start\`.
|
|
|
48291
48492
|
await removeRecord(agent);
|
|
48292
48493
|
}
|
|
48293
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
|
+
}
|
|
48294
48511
|
console.log(
|
|
48295
48512
|
`
|
|
48296
|
-
${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
|
|
48297
48514
|
Cruo \u2192 Settings \u2192 Members shows what it is holding and when it last looked.
|
|
48298
48515
|
`
|
|
48299
48516
|
);
|
|
@@ -48301,29 +48518,27 @@ Nothing running. Start one with \`cruo start\`.
|
|
|
48301
48518
|
async function control(which, explicit, asName, all) {
|
|
48302
48519
|
const records = await targets(explicit, asName, all);
|
|
48303
48520
|
if (records.length === 0) {
|
|
48304
|
-
if (all) console.log(`
|
|
48305
|
-
Nothing running.
|
|
48306
|
-
`);
|
|
48307
48521
|
process.exitCode = all ? 0 : 1;
|
|
48308
48522
|
return;
|
|
48309
48523
|
}
|
|
48310
48524
|
for (const record2 of records) {
|
|
48525
|
+
const tag = record2.untracked ? ` (untracked, pid ${record2.pid})` : "";
|
|
48311
48526
|
const sent = signal(record2.pid, which);
|
|
48312
48527
|
if (!sent) {
|
|
48313
|
-
console.error(` ${record2.agent}: could not signal pid ${record2.pid} \u2014 it may have just exited`);
|
|
48314
|
-
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);
|
|
48315
48530
|
continue;
|
|
48316
48531
|
}
|
|
48317
48532
|
if (which === "stop") {
|
|
48318
48533
|
console.log(
|
|
48319
|
-
` ${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
|
|
48320
48535
|
a harness mid-run keeps its work, which is the point of asking rather than killing.
|
|
48321
48536
|
Watch it leave with: cruo logs ${record2.agent} -f`
|
|
48322
48537
|
);
|
|
48323
48538
|
} else if (which === "pause") {
|
|
48324
|
-
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.`);
|
|
48325
48540
|
} else {
|
|
48326
|
-
console.log(` ${record2.agent}: resumed.`);
|
|
48541
|
+
console.log(` ${record2.agent}${tag}: resumed.`);
|
|
48327
48542
|
}
|
|
48328
48543
|
}
|
|
48329
48544
|
console.log("");
|
package/package.json
CHANGED