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.
Files changed (3) hide show
  1. package/dist/VERSION +1 -1
  2. package/dist/cli.js +338 -37
  3. package/package.json +1 -1
package/dist/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.8
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. Do it. When you are finished, hand it on: move it to the state that`,
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. Pick it up and do your part.`
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
- log(` ${why} \u2014 finishing the current run, then stopping. Again to leave now.`);
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 new Promise((r) => setTimeout(r, options.intervalMs));
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] run the agent's supervisor, in this terminal
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
- A bare \`cruo\` still runs \`${config3.default}\`.
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 ? ` A bare \`cruo\` now runs \`${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 a bare \`cruo\` runs. Use \`--as <name>\` for another.
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
- return rows.filter((r) => r.live.state === "running").map((r) => r.live.record);
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
- if (live.state === "running") return [live.record];
48120
- if (live.state === "gone") {
48121
- console.error(`
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
- } else if (live.state === "recycled") {
48125
- console.error(`
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
- } else {
48129
- console.error(`
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] : 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
- if (rows.length === 0) {
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, give no command: cruo [options]
48337
- Everything it takes: cruo --help
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(argv2).filter((a, i, all) => a !== "--as" && all[i - 1] !== "--as");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cruo-agent",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Run a Cruo agent: it watches your board, picks up the cards you assign it, and works them.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://cruo.space/docs#agents",