cruo-agent 0.1.4 → 0.1.6
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/README.md +1 -1
- package/dist/VERSION +1 -1
- package/dist/cli.js +630 -89
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ given up on work nobody abandoned.
|
|
|
93
93
|
| `--worktree` | a checkout per card |
|
|
94
94
|
| `--allow <tools>` | what the harness may use |
|
|
95
95
|
| `--push` | publish the branch after a run that commits |
|
|
96
|
-
| `--harness-timeout <seconds>` | kill a run that wedges (default
|
|
96
|
+
| `--harness-timeout <seconds>` | kill a run that wedges (default 1800) |
|
|
97
97
|
| `--max-attempts <n>` | give up on a card after n unproductive runs (default 3) |
|
|
98
98
|
|
|
99
99
|
The full list, and what each is for: <https://cruo.space/docs#agents>
|
package/dist/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.6
|
package/dist/cli.js
CHANGED
|
@@ -15247,6 +15247,13 @@ var init_plans = __esm({
|
|
|
15247
15247
|
}
|
|
15248
15248
|
});
|
|
15249
15249
|
|
|
15250
|
+
// ../../packages/core/dist/reports.js
|
|
15251
|
+
var init_reports = __esm({
|
|
15252
|
+
"../../packages/core/dist/reports.js"() {
|
|
15253
|
+
"use strict";
|
|
15254
|
+
}
|
|
15255
|
+
});
|
|
15256
|
+
|
|
15250
15257
|
// ../../packages/core/dist/mcp-connect.js
|
|
15251
15258
|
var DOCS_URL, AGENT_DOCS_URL, MCP_DOCS_URL, CRUO_CLOUD;
|
|
15252
15259
|
var init_mcp_connect = __esm({
|
|
@@ -15293,6 +15300,7 @@ var init_dist = __esm({
|
|
|
15293
15300
|
init_identity();
|
|
15294
15301
|
init_effort();
|
|
15295
15302
|
init_plans();
|
|
15303
|
+
init_reports();
|
|
15296
15304
|
init_mcp_connect();
|
|
15297
15305
|
init_tree();
|
|
15298
15306
|
init_place();
|
|
@@ -16158,7 +16166,7 @@ var require_FunctionsClient = __commonJS({
|
|
|
16158
16166
|
let timeoutId;
|
|
16159
16167
|
let timeoutController;
|
|
16160
16168
|
try {
|
|
16161
|
-
const { headers, method, body: functionArgs, signal, timeout } = options2;
|
|
16169
|
+
const { headers, method, body: functionArgs, signal: signal2, timeout } = options2;
|
|
16162
16170
|
let _headers = {};
|
|
16163
16171
|
let { region } = options2;
|
|
16164
16172
|
if (!region) {
|
|
@@ -16191,13 +16199,13 @@ var require_FunctionsClient = __commonJS({
|
|
|
16191
16199
|
body = functionArgs;
|
|
16192
16200
|
}
|
|
16193
16201
|
}
|
|
16194
|
-
let effectiveSignal =
|
|
16202
|
+
let effectiveSignal = signal2;
|
|
16195
16203
|
if (timeout) {
|
|
16196
16204
|
timeoutController = new AbortController();
|
|
16197
16205
|
timeoutId = setTimeout(() => timeoutController.abort(), timeout);
|
|
16198
|
-
if (
|
|
16206
|
+
if (signal2) {
|
|
16199
16207
|
effectiveSignal = timeoutController.signal;
|
|
16200
|
-
|
|
16208
|
+
signal2.addEventListener("abort", () => timeoutController.abort());
|
|
16201
16209
|
} else {
|
|
16202
16210
|
effectiveSignal = timeoutController.signal;
|
|
16203
16211
|
}
|
|
@@ -16283,21 +16291,21 @@ var require_main = __commonJS({
|
|
|
16283
16291
|
});
|
|
16284
16292
|
|
|
16285
16293
|
// ../../node_modules/.pnpm/@supabase+postgrest-js@2.110.7/node_modules/@supabase/postgrest-js/dist/index.mjs
|
|
16286
|
-
function sleep(ms2,
|
|
16294
|
+
function sleep(ms2, signal2) {
|
|
16287
16295
|
return new Promise((resolve2) => {
|
|
16288
|
-
if (
|
|
16296
|
+
if (signal2 === null || signal2 === void 0 ? void 0 : signal2.aborted) {
|
|
16289
16297
|
resolve2();
|
|
16290
16298
|
return;
|
|
16291
16299
|
}
|
|
16292
16300
|
const id = setTimeout(() => {
|
|
16293
|
-
|
|
16301
|
+
signal2 === null || signal2 === void 0 || signal2.removeEventListener("abort", onAbort);
|
|
16294
16302
|
resolve2();
|
|
16295
16303
|
}, ms2);
|
|
16296
16304
|
function onAbort() {
|
|
16297
16305
|
clearTimeout(id);
|
|
16298
16306
|
resolve2();
|
|
16299
16307
|
}
|
|
16300
|
-
|
|
16308
|
+
signal2 === null || signal2 === void 0 || signal2.addEventListener("abort", onAbort);
|
|
16301
16309
|
});
|
|
16302
16310
|
}
|
|
16303
16311
|
function shouldRetry(method, status, attemptCount, retryEnabled) {
|
|
@@ -17326,8 +17334,8 @@ ${cause.stack}`;
|
|
|
17326
17334
|
*
|
|
17327
17335
|
* ```
|
|
17328
17336
|
*/
|
|
17329
|
-
abortSignal(
|
|
17330
|
-
this.signal =
|
|
17337
|
+
abortSignal(signal2) {
|
|
17338
|
+
this.signal = signal2;
|
|
17331
17339
|
return this;
|
|
17332
17340
|
}
|
|
17333
17341
|
/**
|
|
@@ -23486,8 +23494,8 @@ var require_RealtimeChannel = __commonJS({
|
|
|
23486
23494
|
}
|
|
23487
23495
|
/** @internal */
|
|
23488
23496
|
_notThisChannelEvent(event, ref) {
|
|
23489
|
-
const { close, error: error51, leave, join:
|
|
23490
|
-
const events = [close, error51, leave,
|
|
23497
|
+
const { close, error: error51, leave, join: join5 } = constants_1.CHANNEL_EVENTS;
|
|
23498
|
+
const events = [close, error51, leave, join5];
|
|
23491
23499
|
return ref && events.includes(event) && ref !== this.joinPush.ref;
|
|
23492
23500
|
}
|
|
23493
23501
|
/** @internal */
|
|
@@ -30509,7 +30517,7 @@ var require_webauthn = __commonJS({
|
|
|
30509
30517
|
* @see {@link https://w3c.github.io/webauthn/#sctn-credential-creation W3C WebAuthn Spec - Credential Creation}
|
|
30510
30518
|
* @see {@link https://w3c.github.io/webauthn/#sctn-verifying-assertion W3C WebAuthn Spec - Verifying Assertion}
|
|
30511
30519
|
*/
|
|
30512
|
-
async _challenge({ factorId, webauthn, friendlyName, signal }, overrides) {
|
|
30520
|
+
async _challenge({ factorId, webauthn, friendlyName, signal: signal2 }, overrides) {
|
|
30513
30521
|
var _a3;
|
|
30514
30522
|
try {
|
|
30515
30523
|
const { data: challengeResponse, error: challengeError } = await this.client.mfa.challenge({
|
|
@@ -30519,7 +30527,7 @@ var require_webauthn = __commonJS({
|
|
|
30519
30527
|
if (!challengeResponse) {
|
|
30520
30528
|
return { data: null, error: challengeError };
|
|
30521
30529
|
}
|
|
30522
|
-
const abortSignal =
|
|
30530
|
+
const abortSignal = signal2 !== null && signal2 !== void 0 ? signal2 : exports.webAuthnAbortService.createNewAbortSignal();
|
|
30523
30531
|
if (challengeResponse.webauthn.type === "create") {
|
|
30524
30532
|
const { user } = challengeResponse.webauthn.credential_options.publicKey;
|
|
30525
30533
|
if (!user.name) {
|
|
@@ -30623,7 +30631,7 @@ var require_webauthn = __commonJS({
|
|
|
30623
30631
|
* @see {@link https://w3c.github.io/webauthn/#sctn-authentication W3C WebAuthn Spec - Authentication Ceremony}
|
|
30624
30632
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions MDN - PublicKeyCredentialRequestOptions}
|
|
30625
30633
|
*/
|
|
30626
|
-
async _authenticate({ factorId, webauthn: { rpId = typeof window !== "undefined" ? window.location.hostname : void 0, rpOrigins = typeof window !== "undefined" ? [window.location.origin] : void 0, signal } = {} }, overrides) {
|
|
30634
|
+
async _authenticate({ factorId, webauthn: { rpId = typeof window !== "undefined" ? window.location.hostname : void 0, rpOrigins = typeof window !== "undefined" ? [window.location.origin] : void 0, signal: signal2 } = {} }, overrides) {
|
|
30627
30635
|
if (!rpId) {
|
|
30628
30636
|
return {
|
|
30629
30637
|
data: null,
|
|
@@ -30640,7 +30648,7 @@ var require_webauthn = __commonJS({
|
|
|
30640
30648
|
const { data: challengeResponse, error: challengeError } = await this.challenge({
|
|
30641
30649
|
factorId,
|
|
30642
30650
|
webauthn: { rpId, rpOrigins },
|
|
30643
|
-
signal
|
|
30651
|
+
signal: signal2
|
|
30644
30652
|
}, { request: overrides });
|
|
30645
30653
|
if (!challengeResponse) {
|
|
30646
30654
|
return { data: null, error: challengeError };
|
|
@@ -30681,7 +30689,7 @@ var require_webauthn = __commonJS({
|
|
|
30681
30689
|
* @see {@link https://w3c.github.io/webauthn/#sctn-registering-a-new-credential W3C WebAuthn Spec - Registration Ceremony}
|
|
30682
30690
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions MDN - PublicKeyCredentialCreationOptions}
|
|
30683
30691
|
*/
|
|
30684
|
-
async _register({ friendlyName, webauthn: { rpId = typeof window !== "undefined" ? window.location.hostname : void 0, rpOrigins = typeof window !== "undefined" ? [window.location.origin] : void 0, signal } = {} }, overrides) {
|
|
30692
|
+
async _register({ friendlyName, webauthn: { rpId = typeof window !== "undefined" ? window.location.hostname : void 0, rpOrigins = typeof window !== "undefined" ? [window.location.origin] : void 0, signal: signal2 } = {} }, overrides) {
|
|
30685
30693
|
if (!rpId) {
|
|
30686
30694
|
return {
|
|
30687
30695
|
data: null,
|
|
@@ -30709,7 +30717,7 @@ var require_webauthn = __commonJS({
|
|
|
30709
30717
|
factorId: factor.id,
|
|
30710
30718
|
friendlyName: factor.friendly_name,
|
|
30711
30719
|
webauthn: { rpId, rpOrigins },
|
|
30712
|
-
signal
|
|
30720
|
+
signal: signal2
|
|
30713
30721
|
}, {
|
|
30714
30722
|
create: overrides
|
|
30715
30723
|
});
|
|
@@ -35585,10 +35593,10 @@ var require_GoTrueClient = __commonJS({
|
|
|
35585
35593
|
return this._returnResult({ data: null, error: optionsError });
|
|
35586
35594
|
}
|
|
35587
35595
|
const publicKeyOptions = (0, webauthn_1.deserializeCredentialRequestOptions)(options2.options);
|
|
35588
|
-
const
|
|
35596
|
+
const signal2 = (_c = (_b = credentials === null || credentials === void 0 ? void 0 : credentials.options) === null || _b === void 0 ? void 0 : _b.signal) !== null && _c !== void 0 ? _c : webauthn_1.webAuthnAbortService.createNewAbortSignal();
|
|
35589
35597
|
const { data: credential, error: credentialError } = await (0, webauthn_1.getCredential)({
|
|
35590
35598
|
publicKey: publicKeyOptions,
|
|
35591
|
-
signal
|
|
35599
|
+
signal: signal2
|
|
35592
35600
|
});
|
|
35593
35601
|
if (credentialError || !credential) {
|
|
35594
35602
|
return this._returnResult({
|
|
@@ -35633,10 +35641,10 @@ var require_GoTrueClient = __commonJS({
|
|
|
35633
35641
|
return this._returnResult({ data: null, error: optionsError });
|
|
35634
35642
|
}
|
|
35635
35643
|
const publicKeyOptions = (0, webauthn_1.deserializeCredentialCreationOptions)(options2.options);
|
|
35636
|
-
const
|
|
35644
|
+
const signal2 = (_b = (_a3 = credentials === null || credentials === void 0 ? void 0 : credentials.options) === null || _a3 === void 0 ? void 0 : _a3.signal) !== null && _b !== void 0 ? _b : webauthn_1.webAuthnAbortService.createNewAbortSignal();
|
|
35637
35645
|
const { data: credential, error: credentialError } = await (0, webauthn_1.createCredential)({
|
|
35638
35646
|
publicKey: publicKeyOptions,
|
|
35639
|
-
signal
|
|
35647
|
+
signal: signal2
|
|
35640
35648
|
});
|
|
35641
35649
|
if (credentialError || !credential) {
|
|
35642
35650
|
return this._returnResult({
|
|
@@ -36019,8 +36027,8 @@ function parseTraceParent(traceparent) {
|
|
|
36019
36027
|
isSampled: (parseInt(traceFlags, 16) & 1) === 1
|
|
36020
36028
|
};
|
|
36021
36029
|
}
|
|
36022
|
-
function shouldPropagateToTarget(targetUrl,
|
|
36023
|
-
if (!targetUrl || !
|
|
36030
|
+
function shouldPropagateToTarget(targetUrl, targets2) {
|
|
36031
|
+
if (!targetUrl || !targets2 || targets2.length === 0) return false;
|
|
36024
36032
|
let url3;
|
|
36025
36033
|
if (targetUrl instanceof URL) url3 = targetUrl;
|
|
36026
36034
|
else try {
|
|
@@ -36028,7 +36036,7 @@ function shouldPropagateToTarget(targetUrl, targets) {
|
|
|
36028
36036
|
} catch (error51) {
|
|
36029
36037
|
return false;
|
|
36030
36038
|
}
|
|
36031
|
-
for (const target of
|
|
36039
|
+
for (const target of targets2) try {
|
|
36032
36040
|
if (typeof target === "string") {
|
|
36033
36041
|
if (matchStringTarget(url3.hostname, target)) return true;
|
|
36034
36042
|
} else if (target instanceof RegExp) {
|
|
@@ -36052,15 +36060,15 @@ function matchStringTarget(hostname3, target) {
|
|
|
36052
36060
|
return false;
|
|
36053
36061
|
}
|
|
36054
36062
|
function getDefaultPropagationTargets(supabaseUrl) {
|
|
36055
|
-
const
|
|
36063
|
+
const targets2 = [];
|
|
36056
36064
|
try {
|
|
36057
36065
|
const url3 = new URL(supabaseUrl);
|
|
36058
|
-
|
|
36066
|
+
targets2.push(url3.hostname);
|
|
36059
36067
|
} catch (error51) {
|
|
36060
36068
|
}
|
|
36061
|
-
|
|
36062
|
-
|
|
36063
|
-
return
|
|
36069
|
+
targets2.push("*.supabase.co", "*.supabase.in");
|
|
36070
|
+
targets2.push("localhost", "127.0.0.1", "[::1]");
|
|
36071
|
+
return targets2;
|
|
36064
36072
|
}
|
|
36065
36073
|
function _typeof3(o) {
|
|
36066
36074
|
"@babel/helpers - typeof";
|
|
@@ -36113,8 +36121,8 @@ function _objectSpread23(e) {
|
|
|
36113
36121
|
}
|
|
36114
36122
|
return e;
|
|
36115
36123
|
}
|
|
36116
|
-
async function getTraceHeaders(input,
|
|
36117
|
-
if (!shouldPropagateToTarget(typeof input === "string" ? input : input instanceof URL ? input : input.url,
|
|
36124
|
+
async function getTraceHeaders(input, targets2, respectSampling) {
|
|
36125
|
+
if (!shouldPropagateToTarget(typeof input === "string" ? input : input instanceof URL ? input : input.url, targets2)) return null;
|
|
36118
36126
|
const traceContext = await extractTraceContext();
|
|
36119
36127
|
if (!traceContext || !traceContext.traceparent) return null;
|
|
36120
36128
|
if (respectSampling) {
|
|
@@ -45949,6 +45957,110 @@ var init_options = __esm({
|
|
|
45949
45957
|
}
|
|
45950
45958
|
});
|
|
45951
45959
|
|
|
45960
|
+
// src/process.ts
|
|
45961
|
+
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
45962
|
+
import { execFile } from "node:child_process";
|
|
45963
|
+
import { homedir } from "node:os";
|
|
45964
|
+
import { join } from "node:path";
|
|
45965
|
+
import { promisify } from "node:util";
|
|
45966
|
+
function safeName(agent) {
|
|
45967
|
+
const cleaned = agent.toLowerCase().replace(/[^a-z0-9._-]/g, "").replace(/^\.+/, "");
|
|
45968
|
+
return cleaned === "" ? "default" : cleaned;
|
|
45969
|
+
}
|
|
45970
|
+
async function processStartedAt(pid) {
|
|
45971
|
+
try {
|
|
45972
|
+
const { stdout } = await exec("ps", ["-p", String(pid), "-o", "lstart="]);
|
|
45973
|
+
const value = stdout.trim();
|
|
45974
|
+
return value === "" ? null : value;
|
|
45975
|
+
} catch {
|
|
45976
|
+
return null;
|
|
45977
|
+
}
|
|
45978
|
+
}
|
|
45979
|
+
function isAlive(pid) {
|
|
45980
|
+
try {
|
|
45981
|
+
process.kill(pid, 0);
|
|
45982
|
+
return true;
|
|
45983
|
+
} catch (error51) {
|
|
45984
|
+
return error51.code === "EPERM";
|
|
45985
|
+
}
|
|
45986
|
+
}
|
|
45987
|
+
async function writeRecord(record2) {
|
|
45988
|
+
await mkdir(runDir(), { recursive: true });
|
|
45989
|
+
await writeFile(recordPath(record2.agent), JSON.stringify(record2, null, 2) + "\n", {
|
|
45990
|
+
mode: 384
|
|
45991
|
+
});
|
|
45992
|
+
}
|
|
45993
|
+
async function removeRecord(agent) {
|
|
45994
|
+
await rm(recordPath(agent), { force: true });
|
|
45995
|
+
}
|
|
45996
|
+
async function readRecord(agent) {
|
|
45997
|
+
try {
|
|
45998
|
+
const parsed = JSON.parse(await readFile(recordPath(agent), "utf8"));
|
|
45999
|
+
return typeof parsed?.pid === "number" ? parsed : null;
|
|
46000
|
+
} catch {
|
|
46001
|
+
return null;
|
|
46002
|
+
}
|
|
46003
|
+
}
|
|
46004
|
+
async function liveness(agent, tidy = false) {
|
|
46005
|
+
const record2 = await readRecord(agent);
|
|
46006
|
+
if (!record2) return { state: "absent" };
|
|
46007
|
+
if (!isAlive(record2.pid)) {
|
|
46008
|
+
if (tidy) await removeRecord(agent);
|
|
46009
|
+
return { state: "gone", record: record2 };
|
|
46010
|
+
}
|
|
46011
|
+
const now = await processStartedAt(record2.pid);
|
|
46012
|
+
if (record2.procStartedAt !== null && now !== null && now !== record2.procStartedAt) {
|
|
46013
|
+
if (tidy) await removeRecord(agent);
|
|
46014
|
+
return { state: "recycled", record: record2 };
|
|
46015
|
+
}
|
|
46016
|
+
return { state: "running", record: record2 };
|
|
46017
|
+
}
|
|
46018
|
+
async function listRecords() {
|
|
46019
|
+
let names;
|
|
46020
|
+
try {
|
|
46021
|
+
names = (await readdir(runDir())).filter((n2) => n2.endsWith(".json"));
|
|
46022
|
+
} catch {
|
|
46023
|
+
return [];
|
|
46024
|
+
}
|
|
46025
|
+
const out = [];
|
|
46026
|
+
for (const file2 of names.sort()) {
|
|
46027
|
+
const agent = file2.slice(0, -".json".length);
|
|
46028
|
+
out.push({ agent, live: await liveness(agent) });
|
|
46029
|
+
}
|
|
46030
|
+
return out;
|
|
46031
|
+
}
|
|
46032
|
+
function signal(pid, which) {
|
|
46033
|
+
try {
|
|
46034
|
+
process.kill(pid, SIGNALS[which]);
|
|
46035
|
+
return true;
|
|
46036
|
+
} catch {
|
|
46037
|
+
return false;
|
|
46038
|
+
}
|
|
46039
|
+
}
|
|
46040
|
+
function since(iso) {
|
|
46041
|
+
const ms2 = Date.now() - Date.parse(iso);
|
|
46042
|
+
if (!Number.isFinite(ms2) || ms2 < 0) return "?";
|
|
46043
|
+
if (ms2 < 6e4) return `${Math.round(ms2 / 1e3)}s`;
|
|
46044
|
+
if (ms2 < 36e5) return `${Math.round(ms2 / 6e4)}m`;
|
|
46045
|
+
if (ms2 < 864e5) return `${Math.round(ms2 / 36e5)}h`;
|
|
46046
|
+
return `${Math.round(ms2 / 864e5)}d`;
|
|
46047
|
+
}
|
|
46048
|
+
var exec, runDir, logDir, recordPath, SIGNALS;
|
|
46049
|
+
var init_process = __esm({
|
|
46050
|
+
"src/process.ts"() {
|
|
46051
|
+
"use strict";
|
|
46052
|
+
exec = promisify(execFile);
|
|
46053
|
+
runDir = () => join(homedir(), ".cruo", "run");
|
|
46054
|
+
logDir = () => join(homedir(), ".cruo", "logs");
|
|
46055
|
+
recordPath = (agent) => join(runDir(), `${safeName(agent)}.json`);
|
|
46056
|
+
SIGNALS = {
|
|
46057
|
+
stop: "SIGTERM",
|
|
46058
|
+
pause: "SIGUSR1",
|
|
46059
|
+
resume: "SIGUSR2"
|
|
46060
|
+
};
|
|
46061
|
+
}
|
|
46062
|
+
});
|
|
46063
|
+
|
|
45952
46064
|
// src/env.ts
|
|
45953
46065
|
function applyCloudDefaults() {
|
|
45954
46066
|
const url3 = process.env.SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
@@ -46040,8 +46152,8 @@ function compact(value) {
|
|
|
46040
46152
|
}
|
|
46041
46153
|
async function reportUsage(opts) {
|
|
46042
46154
|
const ctx = await contextOrExplain();
|
|
46043
|
-
const
|
|
46044
|
-
let query = ctx.client.from("harness_runs").select("*").gte("started_at",
|
|
46155
|
+
const since2 = new Date(Date.now() - opts.days * 864e5).toISOString();
|
|
46156
|
+
let query = ctx.client.from("harness_runs").select("*").gte("started_at", since2).order("started_at", { ascending: false });
|
|
46045
46157
|
let issueTitle = null;
|
|
46046
46158
|
if (opts.issue) {
|
|
46047
46159
|
const [key, num3] = opts.issue.split("-");
|
|
@@ -46237,15 +46349,77 @@ function usageIn(text) {
|
|
|
46237
46349
|
const measured = Object.values(out).some((v) => v !== void 0);
|
|
46238
46350
|
return measured ? out : null;
|
|
46239
46351
|
}
|
|
46352
|
+
function renderEvent(line) {
|
|
46353
|
+
let event;
|
|
46354
|
+
try {
|
|
46355
|
+
event = JSON.parse(line);
|
|
46356
|
+
} catch {
|
|
46357
|
+
return { text: line, show: line };
|
|
46358
|
+
}
|
|
46359
|
+
if (typeof event !== "object" || event === null) return { text: line, show: line };
|
|
46360
|
+
const e = event;
|
|
46361
|
+
if (e.type === "assistant" || e.type === "user") {
|
|
46362
|
+
const message = e.message;
|
|
46363
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
46364
|
+
const said = [];
|
|
46365
|
+
const shown = [];
|
|
46366
|
+
for (const part of content) {
|
|
46367
|
+
if (typeof part !== "object" || part === null) continue;
|
|
46368
|
+
const p = part;
|
|
46369
|
+
if (p.type === "text" && typeof p.text === "string") {
|
|
46370
|
+
said.push(p.text);
|
|
46371
|
+
shown.push(p.text);
|
|
46372
|
+
} else if (p.type === "tool_use" && typeof p.name === "string") {
|
|
46373
|
+
shown.push(` \xB7 ${p.name}`);
|
|
46374
|
+
} else if (p.type === "tool_result") {
|
|
46375
|
+
const failed = p.is_error === true;
|
|
46376
|
+
if (failed) shown.push(" \xB7 tool error");
|
|
46377
|
+
}
|
|
46378
|
+
}
|
|
46379
|
+
return {
|
|
46380
|
+
text: said.length > 0 ? said.join("\n") : null,
|
|
46381
|
+
show: shown.length > 0 ? shown.join("\n") : null
|
|
46382
|
+
};
|
|
46383
|
+
}
|
|
46384
|
+
if (e.type === "result") {
|
|
46385
|
+
const summary = typeof e.result === "string" ? e.result : null;
|
|
46386
|
+
const usage = usageIn(line);
|
|
46387
|
+
const spend = usage ? ` \u2014 ${usage.inputTokens ?? "?"} in / ${usage.outputTokens ?? "?"} out` + (usage.costUsd !== void 0 ? `, $${usage.costUsd.toFixed(4)}` : "") : "";
|
|
46388
|
+
return { text: summary, show: ` [${String(e.subtype ?? "result")}${spend}]` };
|
|
46389
|
+
}
|
|
46390
|
+
if (e.type === "system") return { text: null, show: null };
|
|
46391
|
+
return { text: null, show: null };
|
|
46392
|
+
}
|
|
46240
46393
|
var REFUSALS, num;
|
|
46241
46394
|
var init_harness_signal = __esm({
|
|
46242
46395
|
"src/harness-signal.ts"() {
|
|
46243
46396
|
"use strict";
|
|
46244
46397
|
REFUSALS = [
|
|
46245
|
-
|
|
46246
|
-
|
|
46398
|
+
// "hit your" AND "reached your". The original had only the first, and the
|
|
46399
|
+
// harness ships the second — `you have reached your weekly usage limit` is a
|
|
46400
|
+
// literal string in the 2.x binary. That one word cost 8,239 runs (CRA-89):
|
|
46401
|
+
// the refusal went unrecognised, so `neverRan` was false, so the backoff
|
|
46402
|
+
// never engaged and every card was charged for an outage.
|
|
46403
|
+
[
|
|
46404
|
+
/(hit|reached) your (monthly |weekly |daily )?(spend|usage) limit/i,
|
|
46405
|
+
"the account's usage limit is reached"
|
|
46406
|
+
],
|
|
46407
|
+
// The passive voice of the same thing, which is what the harness prints when
|
|
46408
|
+
// it is reporting rather than addressing you.
|
|
46409
|
+
[/(usage|spend|usage credit) limit reached/i, "the account's usage limit is reached"],
|
|
46410
|
+
// `is` optional: the binary carries BOTH "credit balance is too low" and
|
|
46411
|
+
// "credit balance too low", and the pattern only knew the longer one.
|
|
46412
|
+
[
|
|
46413
|
+
/insufficient credit|out of credit|credit balance (is )?too low/i,
|
|
46414
|
+
"the account is out of credit"
|
|
46415
|
+
],
|
|
46247
46416
|
[/upgrade to (a paid plan|claude pro)/i, "the plan does not cover this"],
|
|
46248
46417
|
[/invalid api key|authentication[_ ]error|please run \/login/i, "the harness is not signed in"],
|
|
46418
|
+
// Still requires the follow-up clause. A bare "rate limited" is left
|
|
46419
|
+
// UNMATCHED on purpose — it is a phrase a model writes about a codebase
|
|
46420
|
+
// ("the endpoint is rate limited"), and the note above is explicit that a
|
|
46421
|
+
// false positive is the worse error: it credits an issue an attempt it
|
|
46422
|
+
// should have been charged.
|
|
46249
46423
|
[/rate limit(ed| exceeded|s? reached)\b.*\btry again/i, "the account is rate limited"]
|
|
46250
46424
|
];
|
|
46251
46425
|
num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
@@ -46293,12 +46467,12 @@ var init_pacing = __esm({
|
|
|
46293
46467
|
});
|
|
46294
46468
|
|
|
46295
46469
|
// src/worktree.ts
|
|
46296
|
-
import { execFile } from "node:child_process";
|
|
46297
|
-
import { mkdir, readdir, rm, stat } from "node:fs/promises";
|
|
46298
|
-
import { join } from "node:path";
|
|
46299
|
-
import { promisify } from "node:util";
|
|
46470
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
46471
|
+
import { mkdir as mkdir2, readdir as readdir2, rm as rm2, stat } from "node:fs/promises";
|
|
46472
|
+
import { join as join2 } from "node:path";
|
|
46473
|
+
import { promisify as promisify2 } from "node:util";
|
|
46300
46474
|
async function git(repo, args) {
|
|
46301
|
-
const { stdout } = await
|
|
46475
|
+
const { stdout } = await exec2("git", ["-C", repo, ...args], { maxBuffer: 8 * 1024 * 1024 });
|
|
46302
46476
|
return stdout.trim();
|
|
46303
46477
|
}
|
|
46304
46478
|
async function hasRef(repo, ref) {
|
|
@@ -46337,16 +46511,16 @@ async function remoteUrl(repo, remote) {
|
|
|
46337
46511
|
}
|
|
46338
46512
|
async function createWorktree(ref, config3) {
|
|
46339
46513
|
const branch = branchFor(ref);
|
|
46340
|
-
const path =
|
|
46341
|
-
await
|
|
46342
|
-
await
|
|
46514
|
+
const path = join2(config3.root, ref);
|
|
46515
|
+
await mkdir2(config3.root, { recursive: true });
|
|
46516
|
+
await rm2(path, { recursive: true, force: true });
|
|
46343
46517
|
await git(config3.repo, ["worktree", "prune"]);
|
|
46344
46518
|
const existing = await hasRef(config3.repo, branch);
|
|
46345
46519
|
const startPoint = existing ? [branch] : ["-b", branch, config3.base];
|
|
46346
46520
|
await git(config3.repo, ["worktree", "add", path, ...startPoint]);
|
|
46347
46521
|
const startCommit = await git(config3.repo, ["rev-parse", branch]);
|
|
46348
46522
|
if (config3.prepare) {
|
|
46349
|
-
await
|
|
46523
|
+
await exec2("/bin/sh", ["-c", config3.prepare], {
|
|
46350
46524
|
cwd: path,
|
|
46351
46525
|
maxBuffer: 32 * 1024 * 1024,
|
|
46352
46526
|
timeout: config3.prepareTimeoutMs ?? 15 * 6e4,
|
|
@@ -46357,10 +46531,28 @@ async function createWorktree(ref, config3) {
|
|
|
46357
46531
|
const out = await git(config3.repo, ["rev-list", "--count", `${startCommit}..${branch}`]);
|
|
46358
46532
|
return Number(out) || 0;
|
|
46359
46533
|
};
|
|
46534
|
+
const isDirty = async () => (await git(path, ["status", "--porcelain"])).length > 0;
|
|
46360
46535
|
return {
|
|
46361
46536
|
path,
|
|
46362
46537
|
branch,
|
|
46363
46538
|
commits: countCommits,
|
|
46539
|
+
dirty: isDirty,
|
|
46540
|
+
async saveWip(reason) {
|
|
46541
|
+
if (!await isDirty()) return null;
|
|
46542
|
+
await git(path, ["add", "-A"]);
|
|
46543
|
+
const staged = await git(path, ["diff", "--cached", "--name-only"]);
|
|
46544
|
+
if (staged.length === 0) return null;
|
|
46545
|
+
const message = `wip(${ref}): saved by the supervisor when the run was cut short
|
|
46546
|
+
|
|
46547
|
+
The harness was ${reason} with this in the tree. NOT a handoff: the
|
|
46548
|
+
agent did not choose to commit it and nothing here has been reviewed.
|
|
46549
|
+
The next attempt on this card starts from here rather than from scratch.
|
|
46550
|
+
|
|
46551
|
+
Cruo-Wip: ${reason}
|
|
46552
|
+
`;
|
|
46553
|
+
await git(path, ["commit", "--no-verify", "-m", message]);
|
|
46554
|
+
return await git(path, ["rev-parse", "HEAD"]);
|
|
46555
|
+
},
|
|
46364
46556
|
async push(remote) {
|
|
46365
46557
|
assertPublishable(branch);
|
|
46366
46558
|
if (await countCommits() === 0 && !await hasRef(config3.repo, `${remote}/${branch}`)) {
|
|
@@ -46371,7 +46563,7 @@ async function createWorktree(ref, config3) {
|
|
|
46371
46563
|
},
|
|
46372
46564
|
async remove() {
|
|
46373
46565
|
await git(config3.repo, ["worktree", "remove", "--force", path]).catch(async () => {
|
|
46374
|
-
await
|
|
46566
|
+
await rm2(path, { recursive: true, force: true });
|
|
46375
46567
|
});
|
|
46376
46568
|
await git(config3.repo, ["worktree", "prune"]);
|
|
46377
46569
|
}
|
|
@@ -46381,14 +46573,14 @@ async function sweep(config3, maxRunMs = 0) {
|
|
|
46381
46573
|
await git(config3.repo, ["worktree", "prune"]);
|
|
46382
46574
|
let entries;
|
|
46383
46575
|
try {
|
|
46384
|
-
entries = await
|
|
46576
|
+
entries = await readdir2(config3.root);
|
|
46385
46577
|
} catch {
|
|
46386
46578
|
return [];
|
|
46387
46579
|
}
|
|
46388
46580
|
const cutoff = Date.now() - Math.max(maxRunMs + 5 * 6e4, 60 * 6e4);
|
|
46389
46581
|
const removed = [];
|
|
46390
46582
|
for (const name of entries) {
|
|
46391
|
-
const path =
|
|
46583
|
+
const path = join2(config3.root, name);
|
|
46392
46584
|
let touched;
|
|
46393
46585
|
try {
|
|
46394
46586
|
touched = (await stat(path)).mtimeMs;
|
|
@@ -46397,18 +46589,18 @@ async function sweep(config3, maxRunMs = 0) {
|
|
|
46397
46589
|
}
|
|
46398
46590
|
if (touched >= cutoff) continue;
|
|
46399
46591
|
await git(config3.repo, ["worktree", "remove", "--force", path]).catch(async () => {
|
|
46400
|
-
await
|
|
46592
|
+
await rm2(path, { recursive: true, force: true });
|
|
46401
46593
|
});
|
|
46402
46594
|
removed.push(name);
|
|
46403
46595
|
}
|
|
46404
46596
|
if (removed.length) await git(config3.repo, ["worktree", "prune"]);
|
|
46405
46597
|
return removed;
|
|
46406
46598
|
}
|
|
46407
|
-
var
|
|
46599
|
+
var exec2;
|
|
46408
46600
|
var init_worktree = __esm({
|
|
46409
46601
|
"src/worktree.ts"() {
|
|
46410
46602
|
"use strict";
|
|
46411
|
-
|
|
46603
|
+
exec2 = promisify2(execFile2);
|
|
46412
46604
|
}
|
|
46413
46605
|
});
|
|
46414
46606
|
|
|
@@ -46416,9 +46608,9 @@ var init_worktree = __esm({
|
|
|
46416
46608
|
var supervisor_exports = {};
|
|
46417
46609
|
import { spawn } from "node:child_process";
|
|
46418
46610
|
import { createRequire } from "node:module";
|
|
46419
|
-
import { mkdtemp, readdir as
|
|
46420
|
-
import { homedir, tmpdir } from "node:os";
|
|
46421
|
-
import { join as
|
|
46611
|
+
import { mkdtemp, readdir as readdir3, readFile as readFile2, rm as rm3, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
46612
|
+
import { homedir as homedir2, tmpdir } from "node:os";
|
|
46613
|
+
import { join as join3 } from "node:path";
|
|
46422
46614
|
import { fileURLToPath } from "node:url";
|
|
46423
46615
|
async function identify(ctx) {
|
|
46424
46616
|
const { data: member, error: error51 } = await ctx.client.schema("public").from("members").select("kind, job_function, capabilities").eq("workspace_id", ctx.workspaceId).eq("user_id", ctx.userId).maybeSingle();
|
|
@@ -46443,7 +46635,7 @@ function resolveWorktreeRoot(identity) {
|
|
|
46443
46635
|
if (options.worktreeRoot) return options.worktreeRoot;
|
|
46444
46636
|
const slug = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
46445
46637
|
const name = slug(identity.name) || slug(identity.email.split("@")[0] ?? "") || "agent";
|
|
46446
|
-
return
|
|
46638
|
+
return join3(homedir2(), ".cruo-work", name);
|
|
46447
46639
|
}
|
|
46448
46640
|
async function pollAssigned(ctx) {
|
|
46449
46641
|
const { data: issues, error: error51 } = await ctx.client.from("issues").select("*").eq("assignee_id", ctx.userId).order("created_at", { ascending: true });
|
|
@@ -46591,13 +46783,13 @@ async function markRead(ctx, notificationIds) {
|
|
|
46591
46783
|
}
|
|
46592
46784
|
async function writeMcpConfig() {
|
|
46593
46785
|
const session = await childSession();
|
|
46594
|
-
const dir = await mkdtemp(
|
|
46595
|
-
const path =
|
|
46786
|
+
const dir = await mkdtemp(join3(tmpdir(), CONFIG_DIR_PREFIX));
|
|
46787
|
+
const path = join3(dir, "mcp.json");
|
|
46596
46788
|
const here = fileURLToPath(new URL(".", import.meta.url));
|
|
46597
46789
|
const bundled = /[/\\]dist[/\\]?$/.test(here);
|
|
46598
|
-
const server =
|
|
46790
|
+
const server = join3(here, bundled ? "index.js" : "index.ts");
|
|
46599
46791
|
const loader = bundled ? null : createRequire(import.meta.url).resolve("tsx");
|
|
46600
|
-
await
|
|
46792
|
+
await writeFile2(
|
|
46601
46793
|
path,
|
|
46602
46794
|
JSON.stringify(
|
|
46603
46795
|
{
|
|
@@ -46621,7 +46813,7 @@ async function writeMcpConfig() {
|
|
|
46621
46813
|
),
|
|
46622
46814
|
{ mode: 384 }
|
|
46623
46815
|
);
|
|
46624
|
-
return { path, cleanup: () =>
|
|
46816
|
+
return { path, cleanup: () => rm3(dir, { recursive: true, force: true }) };
|
|
46625
46817
|
}
|
|
46626
46818
|
async function sweepStaleConfigs() {
|
|
46627
46819
|
const dir = tmpdir();
|
|
@@ -46629,18 +46821,18 @@ async function sweepStaleConfigs() {
|
|
|
46629
46821
|
const cutoff = Date.now() - Math.max(maxRunMs + 5 * 6e4, 60 * 6e4);
|
|
46630
46822
|
let entries;
|
|
46631
46823
|
try {
|
|
46632
|
-
entries = await
|
|
46824
|
+
entries = await readdir3(dir);
|
|
46633
46825
|
} catch {
|
|
46634
46826
|
return 0;
|
|
46635
46827
|
}
|
|
46636
46828
|
let removed = 0;
|
|
46637
46829
|
for (const name of entries) {
|
|
46638
46830
|
if (!name.startsWith(CONFIG_DIR_PREFIX)) continue;
|
|
46639
|
-
const path =
|
|
46831
|
+
const path = join3(dir, name);
|
|
46640
46832
|
try {
|
|
46641
46833
|
const info = await stat2(path);
|
|
46642
46834
|
if (info.mtimeMs >= cutoff) continue;
|
|
46643
|
-
await
|
|
46835
|
+
await rm3(path, { recursive: true, force: true });
|
|
46644
46836
|
removed += 1;
|
|
46645
46837
|
} catch {
|
|
46646
46838
|
}
|
|
@@ -46650,7 +46842,7 @@ async function sweepStaleConfigs() {
|
|
|
46650
46842
|
async function preflight() {
|
|
46651
46843
|
const { path: mcpConfig, cleanup } = await writeMcpConfig();
|
|
46652
46844
|
try {
|
|
46653
|
-
const parsed = JSON.parse(await
|
|
46845
|
+
const parsed = JSON.parse(await readFile2(mcpConfig, "utf8"));
|
|
46654
46846
|
const { command, args, env } = parsed.mcpServers.cruo;
|
|
46655
46847
|
await new Promise((resolve2, reject) => {
|
|
46656
46848
|
const child = spawn(command, args, {
|
|
@@ -46778,9 +46970,14 @@ function userPrompt(hit) {
|
|
|
46778
46970
|
`which your function owns. Pick it up and do your part.`
|
|
46779
46971
|
].join(" ");
|
|
46780
46972
|
}
|
|
46973
|
+
function headFor(head2) {
|
|
46974
|
+
const flat = head2.replace(/\s+/g, " ").trim();
|
|
46975
|
+
return flat === "" ? null : flat.slice(0, HEAD_STORED);
|
|
46976
|
+
}
|
|
46781
46977
|
async function invokeHarness(ctx, identity, hit, worktree) {
|
|
46782
46978
|
const cwd = worktree?.path ?? options.cwd;
|
|
46783
46979
|
const { path: mcpConfig, cleanup } = await writeMcpConfig();
|
|
46980
|
+
const streaming = options.usage === true;
|
|
46784
46981
|
const args = [
|
|
46785
46982
|
"-p",
|
|
46786
46983
|
userPrompt(hit),
|
|
@@ -46794,7 +46991,11 @@ async function invokeHarness(ctx, identity, hit, worktree) {
|
|
|
46794
46991
|
options.allow,
|
|
46795
46992
|
"--system-prompt",
|
|
46796
46993
|
systemPrompt(ctx, identity, worktree),
|
|
46797
|
-
...options.model ? ["--model", options.model] : []
|
|
46994
|
+
...options.model ? ["--model", options.model] : [],
|
|
46995
|
+
// `--verbose` is not optional alongside stream-json: without it the harness
|
|
46996
|
+
// emits only the final result and the live view is a blank terminal for the
|
|
46997
|
+
// length of the run.
|
|
46998
|
+
...streaming ? ["--output-format", "stream-json", "--verbose"] : []
|
|
46798
46999
|
];
|
|
46799
47000
|
try {
|
|
46800
47001
|
return await new Promise((resolve2) => {
|
|
@@ -46810,14 +47011,34 @@ async function invokeHarness(ctx, identity, hit, worktree) {
|
|
|
46810
47011
|
let stdoutBytes = 0;
|
|
46811
47012
|
let tail = "";
|
|
46812
47013
|
let head2 = "";
|
|
47014
|
+
let pending = "";
|
|
47015
|
+
const addHead = (text) => {
|
|
47016
|
+
if (head2.length < 2048) head2 += text.slice(0, 2048 - head2.length);
|
|
47017
|
+
};
|
|
46813
47018
|
child.stdout?.on("data", (c) => {
|
|
46814
47019
|
stdoutBytes += c.length;
|
|
46815
|
-
|
|
46816
|
-
|
|
46817
|
-
|
|
47020
|
+
const chunk = c.toString("utf8");
|
|
47021
|
+
if (!streaming) {
|
|
47022
|
+
addHead(chunk);
|
|
47023
|
+
tail = (tail + chunk).slice(-4096);
|
|
47024
|
+
process.stdout.write(c);
|
|
47025
|
+
return;
|
|
47026
|
+
}
|
|
47027
|
+
pending += chunk;
|
|
47028
|
+
const lines = pending.split("\n");
|
|
47029
|
+
pending = lines.pop() ?? "";
|
|
47030
|
+
for (const line of lines) {
|
|
47031
|
+
if (line.trim() === "") continue;
|
|
47032
|
+
tail = line;
|
|
47033
|
+
const rendered = renderEvent(line);
|
|
47034
|
+
if (rendered.text) addHead(rendered.text + "\n");
|
|
47035
|
+
if (rendered.show) process.stdout.write(rendered.show + "\n");
|
|
47036
|
+
}
|
|
46818
47037
|
});
|
|
46819
47038
|
child.stderr?.on("data", (c) => process.stderr.write(c));
|
|
47039
|
+
let killed = false;
|
|
46820
47040
|
const timer = setTimeout(() => {
|
|
47041
|
+
killed = true;
|
|
46821
47042
|
log(` harness exceeded ${options.harnessTimeoutMs / 1e3}s \u2014 killing it`);
|
|
46822
47043
|
child.kill("SIGTERM");
|
|
46823
47044
|
setTimeout(() => child.kill("SIGKILL"), 1e4).unref();
|
|
@@ -46830,17 +47051,28 @@ async function invokeHarness(ctx, identity, hit, worktree) {
|
|
|
46830
47051
|
stdoutBytes,
|
|
46831
47052
|
refusal: refusalIn(head2),
|
|
46832
47053
|
durationMs: Date.now() - startedAtMs,
|
|
46833
|
-
usage: null
|
|
47054
|
+
usage: null,
|
|
47055
|
+
killed,
|
|
47056
|
+
stdoutHead: headFor(head2)
|
|
46834
47057
|
});
|
|
46835
47058
|
});
|
|
46836
47059
|
child.on("close", (code) => {
|
|
46837
47060
|
clearTimeout(timer);
|
|
47061
|
+
if (streaming && pending.trim() !== "") {
|
|
47062
|
+
tail = pending;
|
|
47063
|
+
const rendered = renderEvent(pending);
|
|
47064
|
+
if (rendered.text) addHead(rendered.text + "\n");
|
|
47065
|
+
if (rendered.show) process.stdout.write(rendered.show + "\n");
|
|
47066
|
+
pending = "";
|
|
47067
|
+
}
|
|
46838
47068
|
resolve2({
|
|
46839
47069
|
code: code ?? 1,
|
|
46840
47070
|
stdoutBytes,
|
|
46841
47071
|
refusal: refusalIn(head2),
|
|
46842
47072
|
durationMs: Date.now() - startedAtMs,
|
|
46843
|
-
usage: usageIn(tail)
|
|
47073
|
+
usage: usageIn(tail),
|
|
47074
|
+
killed,
|
|
47075
|
+
stdoutHead: headFor(head2)
|
|
46844
47076
|
});
|
|
46845
47077
|
});
|
|
46846
47078
|
});
|
|
@@ -46882,8 +47114,8 @@ async function clearAttempts(ctx, hit) {
|
|
|
46882
47114
|
const { error: error51 } = await ctx.client.from("issue_attempts").delete().eq("issue_id", hit.issue.id).eq("agent_id", ctx.userId).eq("reason", hit.reason);
|
|
46883
47115
|
if (error51) log(` could not clear attempts on ${hit.ref}: ${error51.message}`);
|
|
46884
47116
|
}
|
|
46885
|
-
async function repliedSince(ctx, hit,
|
|
46886
|
-
const { count, error: error51 } = await ctx.client.from("comments").select("id", { count: "exact", head: true }).eq("issue_id", hit.issue.id).eq("author_id", ctx.userId).gt("created_at",
|
|
47117
|
+
async function repliedSince(ctx, hit, since2) {
|
|
47118
|
+
const { count, error: error51 } = await ctx.client.from("comments").select("id", { count: "exact", head: true }).eq("issue_id", hit.issue.id).eq("author_id", ctx.userId).gt("created_at", since2);
|
|
46887
47119
|
if (error51) {
|
|
46888
47120
|
log(` could not check for a reply on ${hit.ref}: ${error51.message}`);
|
|
46889
47121
|
return false;
|
|
@@ -46918,11 +47150,29 @@ async function beat(ctx, fields) {
|
|
|
46918
47150
|
last_seen_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
46919
47151
|
holding: fields.holding,
|
|
46920
47152
|
dead_ticks: fields.deadTicks,
|
|
46921
|
-
poll_interval_ms: fields.intervalMs
|
|
47153
|
+
poll_interval_ms: fields.intervalMs,
|
|
47154
|
+
// CRA-102. Without this the board cannot tell an agent someone paused
|
|
47155
|
+
// from one that is wading through a backlog, nor one stopped on purpose
|
|
47156
|
+
// from one whose laptop shut its lid — all of them simply went quiet.
|
|
47157
|
+
status: fields.status ?? (paused ? "paused" : "running")
|
|
46922
47158
|
},
|
|
46923
47159
|
{ onConflict: "user_id,workspace_id" }
|
|
46924
47160
|
);
|
|
46925
47161
|
if (error51) log(` could not record a heartbeat: ${error51.message}`);
|
|
47162
|
+
lastHolding = fields.holding;
|
|
47163
|
+
}
|
|
47164
|
+
function announce(status) {
|
|
47165
|
+
void (async () => {
|
|
47166
|
+
try {
|
|
47167
|
+
await beat(await getContext(), {
|
|
47168
|
+
holding: lastHolding,
|
|
47169
|
+
deadTicks: 0,
|
|
47170
|
+
intervalMs: options.intervalMs,
|
|
47171
|
+
status
|
|
47172
|
+
});
|
|
47173
|
+
} catch {
|
|
47174
|
+
}
|
|
47175
|
+
})();
|
|
46926
47176
|
}
|
|
46927
47177
|
async function recordRun(ctx, hit, run, outcome) {
|
|
46928
47178
|
const { error: error51 } = await ctx.client.from("harness_runs").insert({
|
|
@@ -46933,6 +47183,7 @@ async function recordRun(ctx, hit, run, outcome) {
|
|
|
46933
47183
|
duration_ms: run.durationMs,
|
|
46934
47184
|
exit_code: run.code,
|
|
46935
47185
|
stdout_bytes: run.stdoutBytes,
|
|
47186
|
+
stdout_head: run.stdoutHead,
|
|
46936
47187
|
outcome,
|
|
46937
47188
|
model: run.usage?.model ?? null,
|
|
46938
47189
|
input_tokens: run.usage?.inputTokens ?? null,
|
|
@@ -47029,9 +47280,28 @@ async function tick(ctx, identity, deadTicks, allowance) {
|
|
|
47029
47280
|
}
|
|
47030
47281
|
let run;
|
|
47031
47282
|
let committed = 0;
|
|
47283
|
+
let salvaged = null;
|
|
47032
47284
|
try {
|
|
47033
47285
|
run = await invokeHarness(ctx, identity, hit, worktree);
|
|
47034
47286
|
if (worktree) committed = await worktree.commits();
|
|
47287
|
+
if (worktree && run.killed) {
|
|
47288
|
+
try {
|
|
47289
|
+
salvaged = await worktree.saveWip("killed at the harness timeout");
|
|
47290
|
+
if (salvaged) {
|
|
47291
|
+
log(
|
|
47292
|
+
` ${hit.ref} killed with an uncommitted tree \u2014 saved as ${salvaged.slice(0, 8)} on ${worktree.branch}; the next attempt resumes from it`
|
|
47293
|
+
);
|
|
47294
|
+
}
|
|
47295
|
+
} catch (error51) {
|
|
47296
|
+
log(
|
|
47297
|
+
` ${hit.ref} could not save the killed run's tree: ${error51 instanceof Error ? error51.message.split("\n")[0] : String(error51)}`
|
|
47298
|
+
);
|
|
47299
|
+
}
|
|
47300
|
+
} else if (worktree && committed === 0 && await worktree.dirty()) {
|
|
47301
|
+
log(
|
|
47302
|
+
` ${hit.ref} exited ${run.code} leaving uncommitted changes \u2014 they go with the worktree${options.keepFailed ? " (kept: --keep-failed)" : "; use --keep-failed to inspect them"}`
|
|
47303
|
+
);
|
|
47304
|
+
}
|
|
47035
47305
|
} finally {
|
|
47036
47306
|
await release(ctx, hit);
|
|
47037
47307
|
}
|
|
@@ -47071,7 +47341,8 @@ async function tick(ctx, identity, deadTicks, allowance) {
|
|
|
47071
47341
|
}
|
|
47072
47342
|
} else {
|
|
47073
47343
|
const what = hit.reason === "mention" ? "no reply written" : "unchanged";
|
|
47074
|
-
const
|
|
47344
|
+
const note = salvaged ? `${what} (exit ${code}); cut short, work saved as ${salvaged.slice(0, 8)}` : `${what} (exit ${code})`;
|
|
47345
|
+
const n2 = await recordAttempt(ctx, hit, note, attemptsFor(hit));
|
|
47075
47346
|
log(
|
|
47076
47347
|
` ${hit.ref} ${what} after the run (exit ${code}) \u2014 attempt ${n2}/${options.maxAttempts}` + (n2 >= options.maxAttempts ? "; setting it aside, a human should look" : "")
|
|
47077
47348
|
);
|
|
@@ -47100,6 +47371,35 @@ async function tick(ctx, identity, deadTicks, allowance) {
|
|
|
47100
47371
|
});
|
|
47101
47372
|
return { invoked, failedToRun, suppressed: false, waiting: actionable.length };
|
|
47102
47373
|
}
|
|
47374
|
+
function installControls() {
|
|
47375
|
+
const requestStop = (why) => {
|
|
47376
|
+
if (stopping && Date.now() - stopRequestedAt < 500) return;
|
|
47377
|
+
if (stopping) {
|
|
47378
|
+
log(` ${why} again \u2014 leaving now; a harness still running will be orphaned`);
|
|
47379
|
+
process.exit(130);
|
|
47380
|
+
}
|
|
47381
|
+
stopping = true;
|
|
47382
|
+
stopRequestedAt = Date.now();
|
|
47383
|
+
log(` ${why} \u2014 finishing the current run, then stopping. Again to leave now.`);
|
|
47384
|
+
};
|
|
47385
|
+
process.on("SIGTERM", () => requestStop("stop requested"));
|
|
47386
|
+
process.on("SIGINT", () => requestStop("interrupted"));
|
|
47387
|
+
process.on("SIGUSR1", () => {
|
|
47388
|
+
if (paused) return;
|
|
47389
|
+
paused = true;
|
|
47390
|
+
log(` paused \u2014 still polling and still reporting, but starting no new runs`);
|
|
47391
|
+
announce("paused");
|
|
47392
|
+
});
|
|
47393
|
+
process.on("SIGUSR2", () => {
|
|
47394
|
+
if (!paused) return;
|
|
47395
|
+
paused = false;
|
|
47396
|
+
log(` resumed \u2014 taking work again`);
|
|
47397
|
+
announce("running");
|
|
47398
|
+
});
|
|
47399
|
+
}
|
|
47400
|
+
function identityName() {
|
|
47401
|
+
return safeName(process.env.CRUO_IDENTITY?.trim() || "default");
|
|
47402
|
+
}
|
|
47103
47403
|
async function main() {
|
|
47104
47404
|
log(
|
|
47105
47405
|
`board: ${config2.url}${config2.url === CRUO_CLOUD.apiUrl ? " (Cruo cloud)" : " (configured)"}`
|
|
@@ -47113,6 +47413,18 @@ async function main() {
|
|
|
47113
47413
|
}
|
|
47114
47414
|
const ctx = await contextOrExplain();
|
|
47115
47415
|
const identity = await identify(ctx);
|
|
47416
|
+
installControls();
|
|
47417
|
+
const runName = identityName();
|
|
47418
|
+
if (!options.once && !options.dryRun) {
|
|
47419
|
+
await writeRecord({
|
|
47420
|
+
agent: runName,
|
|
47421
|
+
pid: process.pid,
|
|
47422
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
47423
|
+
procStartedAt: await processStartedAt(process.pid),
|
|
47424
|
+
log: join3(logDir(), `${runName}.log`),
|
|
47425
|
+
argv: process.argv.slice(2)
|
|
47426
|
+
});
|
|
47427
|
+
}
|
|
47116
47428
|
try {
|
|
47117
47429
|
await preflight();
|
|
47118
47430
|
} catch (error51) {
|
|
@@ -47166,7 +47478,7 @@ async function main() {
|
|
|
47166
47478
|
await getContext(),
|
|
47167
47479
|
identity,
|
|
47168
47480
|
pacing.deadTicks,
|
|
47169
|
-
allowanceAt(pacing, Date.now())
|
|
47481
|
+
paused ? "held" : allowanceAt(pacing, Date.now())
|
|
47170
47482
|
);
|
|
47171
47483
|
if (result.suppressed) {
|
|
47172
47484
|
const now = Date.now();
|
|
@@ -47193,10 +47505,12 @@ async function main() {
|
|
|
47193
47505
|
log(`poll error: ${e instanceof Error ? e.message : String(e)}`);
|
|
47194
47506
|
}
|
|
47195
47507
|
if (options.once) return;
|
|
47508
|
+
if (stopping) return;
|
|
47196
47509
|
await new Promise((r) => setTimeout(r, options.intervalMs));
|
|
47510
|
+
if (stopping) return;
|
|
47197
47511
|
}
|
|
47198
47512
|
}
|
|
47199
|
-
var argv, flag, opt, num2, ms, readOptions, options, log, worktreeConfig, lastRefusal, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV, warnedAboutSpendTable;
|
|
47513
|
+
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;
|
|
47200
47514
|
var init_supervisor = __esm({
|
|
47201
47515
|
"src/supervisor.ts"() {
|
|
47202
47516
|
"use strict";
|
|
@@ -47206,6 +47520,7 @@ var init_supervisor = __esm({
|
|
|
47206
47520
|
init_auth();
|
|
47207
47521
|
init_options();
|
|
47208
47522
|
init_harness_signal();
|
|
47523
|
+
init_process();
|
|
47209
47524
|
init_pacing();
|
|
47210
47525
|
init_worktree();
|
|
47211
47526
|
argv = process.argv.slice(2);
|
|
@@ -47250,14 +47565,40 @@ var init_supervisor = __esm({
|
|
|
47250
47565
|
* supervisor down with it. Observed in the first live run: a harness that
|
|
47251
47566
|
* produced no output and no writes for ten minutes. A killed run counts as
|
|
47252
47567
|
* unproductive like any other, so the retry/set-aside path handles it.
|
|
47568
|
+
*
|
|
47569
|
+
* THIRTY MINUTES, and the number is a decision rather than a guess — the two
|
|
47570
|
+
* mistakes this sits between are nowhere near the same size (`CRA-105`).
|
|
47571
|
+
* Killing a wedged run late costs wall clock the supervisor was not using
|
|
47572
|
+
* anyway. Killing a WORKING run early costs everything it did: the checkout
|
|
47573
|
+
* is recreated from scratch on the next attempt, so an uncommitted tree is
|
|
47574
|
+
* discarded rather than resumed (`CRA-104`). Ten minutes was tuned on the
|
|
47575
|
+
* cards the loop was first exercised on, and `CRA-83` — four packages and a
|
|
47576
|
+
* test file — was killed at it twice, having written real work both times
|
|
47577
|
+
* and committed neither. Err long: the cheap failure is the slow one.
|
|
47253
47578
|
*/
|
|
47254
|
-
harnessTimeoutMs: ms("harness-timeout",
|
|
47579
|
+
harnessTimeoutMs: ms("harness-timeout", 1800),
|
|
47255
47580
|
/**
|
|
47256
47581
|
* Give up on an issue after this many unproductive runs. Persisted in
|
|
47257
47582
|
* `pm.issue_attempts` since CRA-7 — it was in-memory, which meant a restart
|
|
47258
47583
|
* forgot every set-aside and nothing outside the process could see one.
|
|
47259
47584
|
*/
|
|
47260
47585
|
maxAttempts: num2("max-attempts", { fallback: 3, integer: true }),
|
|
47586
|
+
/**
|
|
47587
|
+
* Ask the harness to report what each run spent — `CRA-89`.
|
|
47588
|
+
*
|
|
47589
|
+
* Off by default and that is a real choice, not caution. It appends
|
|
47590
|
+
* `--output-format stream-json --verbose`, which are Claude Code's flags, and
|
|
47591
|
+
* the supervisor's whole design is that `--harness` takes any command that
|
|
47592
|
+
* accepts a prompt and exits. Turning this on says "my harness is Claude Code
|
|
47593
|
+
* shaped"; leaving it off keeps the promise.
|
|
47594
|
+
*
|
|
47595
|
+
* What it costs: the harness's stdout stops being prose and becomes NDJSON,
|
|
47596
|
+
* so the live view is reconstructed by `renderEvent` rather than passed
|
|
47597
|
+
* through. What it buys is the only numbers there are — without it every
|
|
47598
|
+
* token column in `pm.harness_runs` is null, which is what 8,335 rows of it
|
|
47599
|
+
* were.
|
|
47600
|
+
*/
|
|
47601
|
+
usage: flag("usage"),
|
|
47261
47602
|
/**
|
|
47262
47603
|
* Ceiling on how long invocation is held back after a run of dead ticks.
|
|
47263
47604
|
* Ten minutes: long enough that a broken harness stops being spawned over and
|
|
@@ -47350,9 +47691,31 @@ cruo-supervisor: ${error51.message}
|
|
|
47350
47691
|
// stands in for. Cheap to list, and wrong to discover the hard way.
|
|
47351
47692
|
"CRUO_SESSION"
|
|
47352
47693
|
];
|
|
47694
|
+
HEAD_STORED = 200;
|
|
47695
|
+
lastHolding = 0;
|
|
47353
47696
|
warnedAboutSpendTable = false;
|
|
47354
|
-
|
|
47697
|
+
stopping = false;
|
|
47698
|
+
paused = false;
|
|
47699
|
+
stopRequestedAt = 0;
|
|
47700
|
+
main().then(async () => {
|
|
47701
|
+
if (!options.once && !options.dryRun) {
|
|
47702
|
+
try {
|
|
47703
|
+
const ctx = await getContext();
|
|
47704
|
+
await beat(ctx, {
|
|
47705
|
+
holding: 0,
|
|
47706
|
+
deadTicks: 0,
|
|
47707
|
+
intervalMs: options.intervalMs,
|
|
47708
|
+
status: "stopped"
|
|
47709
|
+
});
|
|
47710
|
+
} catch {
|
|
47711
|
+
}
|
|
47712
|
+
await removeRecord(identityName()).catch(() => {
|
|
47713
|
+
});
|
|
47714
|
+
}
|
|
47715
|
+
}).catch(async (e) => {
|
|
47355
47716
|
console.error(e instanceof Error ? e.message : e);
|
|
47717
|
+
await removeRecord(identityName()).catch(() => {
|
|
47718
|
+
});
|
|
47356
47719
|
process.exit(1);
|
|
47357
47720
|
});
|
|
47358
47721
|
}
|
|
@@ -47362,11 +47725,12 @@ cruo-supervisor: ${error51.message}
|
|
|
47362
47725
|
init_dist();
|
|
47363
47726
|
init_dist7();
|
|
47364
47727
|
init_options();
|
|
47365
|
-
|
|
47366
|
-
import {
|
|
47367
|
-
import {
|
|
47368
|
-
|
|
47369
|
-
var
|
|
47728
|
+
init_process();
|
|
47729
|
+
import { chmod, mkdir as mkdir3, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
|
|
47730
|
+
import { homedir as homedir3 } from "node:os";
|
|
47731
|
+
import { join as join4 } from "node:path";
|
|
47732
|
+
var CONFIG_DIR = join4(homedir3(), ".cruo");
|
|
47733
|
+
var CONFIG_PATH = join4(CONFIG_DIR, "config.json");
|
|
47370
47734
|
function nameFromEmail(email3) {
|
|
47371
47735
|
const local = email3.split("@")[0]?.toLowerCase().replace(/[^a-z0-9._-]/g, "");
|
|
47372
47736
|
return local && local.length > 0 ? local : null;
|
|
@@ -47377,9 +47741,22 @@ var USAGE = `cruo \u2014 run a Cruo agent
|
|
|
47377
47741
|
cruo agents list what is remembered (never the tokens)
|
|
47378
47742
|
cruo usage [--days 7] what the agents have been spending
|
|
47379
47743
|
cruo logout [name] forget one, or --all
|
|
47380
|
-
cruo [options] run the agent's supervisor
|
|
47744
|
+
cruo [options] run the agent's supervisor, in this terminal
|
|
47381
47745
|
cruo --as <name> \u2026 run a particular one
|
|
47382
47746
|
|
|
47747
|
+
Running one in the background
|
|
47748
|
+
cruo start [options] same, detached \u2014 survives closing the terminal
|
|
47749
|
+
cruo ps what is running on this machine
|
|
47750
|
+
cruo stop [name] ask it to finish the run it is on, then exit
|
|
47751
|
+
cruo pause [name] keep watching the board, start nothing new
|
|
47752
|
+
cruo resume [name] take work again
|
|
47753
|
+
cruo logs [name] [-f] what a detached agent has been saying
|
|
47754
|
+
\u2026 --all stop/pause/resume every one of them
|
|
47755
|
+
|
|
47756
|
+
\`stop\` is a request, not a kill: a harness that is mid-run is allowed to
|
|
47757
|
+
finish, so the work it has done is not thrown away. Say it twice to give up
|
|
47758
|
+
waiting. macOS and Linux only \u2014 these are signals and pid files.
|
|
47759
|
+
|
|
47383
47760
|
Getting a token
|
|
47384
47761
|
Cruo \u2192 Settings \u2192 Members \u2192 Add an agent. It is shown once.
|
|
47385
47762
|
|
|
@@ -47391,6 +47768,7 @@ Common options
|
|
|
47391
47768
|
--allow <tools> what the harness may use; default is the board only
|
|
47392
47769
|
--push publish the branch after a run that commits
|
|
47393
47770
|
--interval <seconds> how often to poll (default 20)
|
|
47771
|
+
--usage record what each run costs (needs a Claude Code harness)
|
|
47394
47772
|
|
|
47395
47773
|
Full list and what each one is for: https://cruo.space/docs#agents
|
|
47396
47774
|
|
|
@@ -47404,7 +47782,7 @@ var EMPTY = { version: 2, default: null, agents: {} };
|
|
|
47404
47782
|
async function readConfig2() {
|
|
47405
47783
|
let parsed;
|
|
47406
47784
|
try {
|
|
47407
|
-
parsed = JSON.parse(await
|
|
47785
|
+
parsed = JSON.parse(await readFile3(CONFIG_PATH, "utf8"));
|
|
47408
47786
|
} catch {
|
|
47409
47787
|
return { ...EMPTY };
|
|
47410
47788
|
}
|
|
@@ -47423,8 +47801,8 @@ async function readConfig2() {
|
|
|
47423
47801
|
return { ...EMPTY };
|
|
47424
47802
|
}
|
|
47425
47803
|
async function writeConfig(config3) {
|
|
47426
|
-
await
|
|
47427
|
-
await
|
|
47804
|
+
await mkdir3(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
47805
|
+
await writeFile3(CONFIG_PATH, JSON.stringify(config3, null, 2) + "\n", { mode: 384 });
|
|
47428
47806
|
await chmod(CONFIG_PATH, 384);
|
|
47429
47807
|
}
|
|
47430
47808
|
async function identify2(token) {
|
|
@@ -47484,7 +47862,7 @@ async function logout(which, all) {
|
|
|
47484
47862
|
const config3 = await readConfig2();
|
|
47485
47863
|
const names = Object.keys(config3.agents);
|
|
47486
47864
|
if (all) {
|
|
47487
|
-
await
|
|
47865
|
+
await rm4(CONFIG_PATH, { force: true });
|
|
47488
47866
|
console.log(
|
|
47489
47867
|
names.length === 0 ? `
|
|
47490
47868
|
Nothing was stored.
|
|
@@ -47496,7 +47874,7 @@ Forgot ${names.length} token(s): ${names.join(", ")}. ${CONFIG_PATH} is gone.
|
|
|
47496
47874
|
}
|
|
47497
47875
|
if (!which) {
|
|
47498
47876
|
if (names.length <= 1) {
|
|
47499
|
-
await
|
|
47877
|
+
await rm4(CONFIG_PATH, { force: true });
|
|
47500
47878
|
console.log(
|
|
47501
47879
|
names.length === 0 ? `
|
|
47502
47880
|
Nothing was stored.
|
|
@@ -47552,6 +47930,156 @@ No tokens stored. \`cruo login <token>\` to add one.
|
|
|
47552
47930
|
* is what a bare \`cruo\` runs. Use \`--as <name>\` for another.
|
|
47553
47931
|
`);
|
|
47554
47932
|
}
|
|
47933
|
+
async function targetName(explicit, asName) {
|
|
47934
|
+
if (explicit) return safeName(explicit);
|
|
47935
|
+
if (asName) return safeName(asName);
|
|
47936
|
+
const config3 = await readConfig2();
|
|
47937
|
+
return safeName(config3.default ?? "default");
|
|
47938
|
+
}
|
|
47939
|
+
async function targets(explicit, asName, all) {
|
|
47940
|
+
if (all) {
|
|
47941
|
+
const rows = await listRecords();
|
|
47942
|
+
return rows.filter((r) => r.live.state === "running").map((r) => r.live.record);
|
|
47943
|
+
}
|
|
47944
|
+
const name = await targetName(explicit, asName);
|
|
47945
|
+
const live = await liveness(name, true);
|
|
47946
|
+
if (live.state === "running") return [live.record];
|
|
47947
|
+
if (live.state === "gone") {
|
|
47948
|
+
console.error(`
|
|
47949
|
+
${name} is not running (it left a record behind; cleared).
|
|
47950
|
+
`);
|
|
47951
|
+
} else if (live.state === "recycled") {
|
|
47952
|
+
console.error(`
|
|
47953
|
+
${name} is not running \u2014 its pid now belongs to something else. Cleared.
|
|
47954
|
+
`);
|
|
47955
|
+
} else {
|
|
47956
|
+
console.error(`
|
|
47957
|
+
${name} is not running. Start it with \`cruo start --as ${name}\`.
|
|
47958
|
+
`);
|
|
47959
|
+
}
|
|
47960
|
+
return [];
|
|
47961
|
+
}
|
|
47962
|
+
async function startDetached(argv2, asName) {
|
|
47963
|
+
const { spawn: spawn2 } = await import("node:child_process");
|
|
47964
|
+
const { open, mkdir: mkdir4 } = await import("node:fs/promises");
|
|
47965
|
+
const name = await targetName(null, asName);
|
|
47966
|
+
const already = await liveness(name, true);
|
|
47967
|
+
if (already.state === "running") {
|
|
47968
|
+
console.error(
|
|
47969
|
+
`
|
|
47970
|
+
${name} is already running (pid ${already.record.pid}, up ${since(already.record.startedAt)}).
|
|
47971
|
+
Stop it first, or use --as <name> to run a different agent.
|
|
47972
|
+
`
|
|
47973
|
+
);
|
|
47974
|
+
process.exit(1);
|
|
47975
|
+
}
|
|
47976
|
+
await mkdir4(logDir(), { recursive: true });
|
|
47977
|
+
const logPath = join4(logDir(), `${name}.log`);
|
|
47978
|
+
const handle = await open(logPath, "a");
|
|
47979
|
+
const forwarded = argv2.slice(1).filter((a) => a !== "--all");
|
|
47980
|
+
const child = spawn2(process.execPath, [process.argv[1], ...forwarded], {
|
|
47981
|
+
detached: true,
|
|
47982
|
+
stdio: ["ignore", handle.fd, handle.fd],
|
|
47983
|
+
env: process.env
|
|
47984
|
+
});
|
|
47985
|
+
child.unref();
|
|
47986
|
+
await handle.close();
|
|
47987
|
+
console.log(
|
|
47988
|
+
`
|
|
47989
|
+
${name} started (pid ${child.pid}).
|
|
47990
|
+
|
|
47991
|
+
logs cruo logs ${name} -f
|
|
47992
|
+
check cruo ps
|
|
47993
|
+
stop cruo stop ${name}
|
|
47994
|
+
|
|
47995
|
+
It survives this terminal closing. It does not survive the machine
|
|
47996
|
+
sleeping or shutting down \u2014 for that, see the launchd notes in
|
|
47997
|
+
https://cruo.space/docs#agents.
|
|
47998
|
+
`
|
|
47999
|
+
);
|
|
48000
|
+
}
|
|
48001
|
+
async function listProcesses() {
|
|
48002
|
+
const rows = await listRecords();
|
|
48003
|
+
const running = rows.filter((r) => r.live.state === "running");
|
|
48004
|
+
if (rows.length === 0) {
|
|
48005
|
+
console.log(`
|
|
48006
|
+
Nothing running. Start one with \`cruo start\`.
|
|
48007
|
+
`);
|
|
48008
|
+
return;
|
|
48009
|
+
}
|
|
48010
|
+
console.log("");
|
|
48011
|
+
console.log(" agent pid up started with");
|
|
48012
|
+
for (const { agent, live } of rows) {
|
|
48013
|
+
if (live.state === "running") {
|
|
48014
|
+
const flags = live.record.argv.filter((a) => a.startsWith("--")).join(" ") || "(no flags)";
|
|
48015
|
+
console.log(
|
|
48016
|
+
` ${agent.padEnd(14)} ${String(live.record.pid).padEnd(8)} ${since(live.record.startedAt).padEnd(8)} ${flags}`
|
|
48017
|
+
);
|
|
48018
|
+
} else if (live.state === "gone") {
|
|
48019
|
+
console.log(` ${agent.padEnd(14)} \u2014 \u2014 died or was killed (record cleared)`);
|
|
48020
|
+
await removeRecord(agent);
|
|
48021
|
+
} else if (live.state === "recycled") {
|
|
48022
|
+
console.log(` ${agent.padEnd(14)} \u2014 \u2014 gone; its pid is now someone else's (cleared)`);
|
|
48023
|
+
await removeRecord(agent);
|
|
48024
|
+
}
|
|
48025
|
+
}
|
|
48026
|
+
console.log(
|
|
48027
|
+
`
|
|
48028
|
+
${running.length} running. Whether each is actually picking up work is on the board, not here \u2014
|
|
48029
|
+
Cruo \u2192 Settings \u2192 Members shows what it is holding and when it last looked.
|
|
48030
|
+
`
|
|
48031
|
+
);
|
|
48032
|
+
}
|
|
48033
|
+
async function control(which, explicit, asName, all) {
|
|
48034
|
+
const records = await targets(explicit, asName, all);
|
|
48035
|
+
if (records.length === 0) {
|
|
48036
|
+
if (all) console.log(`
|
|
48037
|
+
Nothing running.
|
|
48038
|
+
`);
|
|
48039
|
+
process.exitCode = all ? 0 : 1;
|
|
48040
|
+
return;
|
|
48041
|
+
}
|
|
48042
|
+
for (const record2 of records) {
|
|
48043
|
+
const sent = signal(record2.pid, which);
|
|
48044
|
+
if (!sent) {
|
|
48045
|
+
console.error(` ${record2.agent}: could not signal pid ${record2.pid} \u2014 it may have just exited`);
|
|
48046
|
+
await removeRecord(record2.agent);
|
|
48047
|
+
continue;
|
|
48048
|
+
}
|
|
48049
|
+
if (which === "stop") {
|
|
48050
|
+
console.log(
|
|
48051
|
+
` ${record2.agent}: asked to stop. It will finish the run it is on first \u2014
|
|
48052
|
+
a harness mid-run keeps its work, which is the point of asking rather than killing.
|
|
48053
|
+
Watch it leave with: cruo logs ${record2.agent} -f`
|
|
48054
|
+
);
|
|
48055
|
+
} else if (which === "pause") {
|
|
48056
|
+
console.log(` ${record2.agent}: paused. Still watching the board and still reporting; starts nothing new.`);
|
|
48057
|
+
} else {
|
|
48058
|
+
console.log(` ${record2.agent}: resumed.`);
|
|
48059
|
+
}
|
|
48060
|
+
}
|
|
48061
|
+
console.log("");
|
|
48062
|
+
}
|
|
48063
|
+
async function showLogs(explicit, asName, follow) {
|
|
48064
|
+
const { spawn: spawn2 } = await import("node:child_process");
|
|
48065
|
+
const name = await targetName(explicit, asName);
|
|
48066
|
+
const record2 = await readRecord(name);
|
|
48067
|
+
const path = record2?.log ?? join4(logDir(), `${name}.log`);
|
|
48068
|
+
const { access } = await import("node:fs/promises");
|
|
48069
|
+
try {
|
|
48070
|
+
await access(path);
|
|
48071
|
+
} catch {
|
|
48072
|
+
console.error(`
|
|
48073
|
+
No log for ${name} at ${path}.
|
|
48074
|
+
A detached run writes one; a foreground run prints to its terminal.
|
|
48075
|
+
`);
|
|
48076
|
+
process.exit(1);
|
|
48077
|
+
}
|
|
48078
|
+
const child = spawn2("tail", follow ? ["-f", "-n", "40", path] : ["-n", "200", path], {
|
|
48079
|
+
stdio: "inherit"
|
|
48080
|
+
});
|
|
48081
|
+
await new Promise((resolve2) => child.on("close", resolve2));
|
|
48082
|
+
}
|
|
47555
48083
|
async function main2() {
|
|
47556
48084
|
const argv2 = process.argv.slice(2);
|
|
47557
48085
|
const [first] = argv2;
|
|
@@ -47574,12 +48102,25 @@ Usage: cruo login <token> [--as <name>]
|
|
|
47574
48102
|
return logout(named, argv2.includes("--all"));
|
|
47575
48103
|
}
|
|
47576
48104
|
if (first === "agents" || first === "whoami") return listAgents();
|
|
48105
|
+
{
|
|
48106
|
+
const named = argv2[1] && !argv2[1].startsWith("--") ? argv2[1] : null;
|
|
48107
|
+
const all = argv2.includes("--all");
|
|
48108
|
+
const asName2 = flagValue("--as");
|
|
48109
|
+
if (first === "ps") return listProcesses();
|
|
48110
|
+
if (first === "stop") return control("stop", named, asName2, all);
|
|
48111
|
+
if (first === "pause") return control("pause", named, asName2, all);
|
|
48112
|
+
if (first === "resume") return control("resume", named, asName2, all);
|
|
48113
|
+
if (first === "logs") {
|
|
48114
|
+
return showLogs(named, asName2, argv2.includes("-f") || argv2.includes("--follow"));
|
|
48115
|
+
}
|
|
48116
|
+
}
|
|
47577
48117
|
if (first === "usage") {
|
|
47578
48118
|
}
|
|
47579
48119
|
if (first === "help" || argv2.includes("--help") || argv2.includes("-h")) {
|
|
47580
48120
|
console.log(USAGE);
|
|
47581
48121
|
return;
|
|
47582
48122
|
}
|
|
48123
|
+
if (first === "start") return startDetached(argv2, flagValue("--as"));
|
|
47583
48124
|
const flagIndex = argv2.indexOf("--token");
|
|
47584
48125
|
const fromFlag = flagIndex >= 0 ? argv2[flagIndex + 1] : void 0;
|
|
47585
48126
|
if (fromFlag) {
|
package/package.json
CHANGED