cruo-agent 0.1.4 → 0.1.5
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 +188 -11
- 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.5
|
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();
|
|
@@ -46237,15 +46245,77 @@ function usageIn(text) {
|
|
|
46237
46245
|
const measured = Object.values(out).some((v) => v !== void 0);
|
|
46238
46246
|
return measured ? out : null;
|
|
46239
46247
|
}
|
|
46248
|
+
function renderEvent(line) {
|
|
46249
|
+
let event;
|
|
46250
|
+
try {
|
|
46251
|
+
event = JSON.parse(line);
|
|
46252
|
+
} catch {
|
|
46253
|
+
return { text: line, show: line };
|
|
46254
|
+
}
|
|
46255
|
+
if (typeof event !== "object" || event === null) return { text: line, show: line };
|
|
46256
|
+
const e = event;
|
|
46257
|
+
if (e.type === "assistant" || e.type === "user") {
|
|
46258
|
+
const message = e.message;
|
|
46259
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
46260
|
+
const said = [];
|
|
46261
|
+
const shown = [];
|
|
46262
|
+
for (const part of content) {
|
|
46263
|
+
if (typeof part !== "object" || part === null) continue;
|
|
46264
|
+
const p = part;
|
|
46265
|
+
if (p.type === "text" && typeof p.text === "string") {
|
|
46266
|
+
said.push(p.text);
|
|
46267
|
+
shown.push(p.text);
|
|
46268
|
+
} else if (p.type === "tool_use" && typeof p.name === "string") {
|
|
46269
|
+
shown.push(` \xB7 ${p.name}`);
|
|
46270
|
+
} else if (p.type === "tool_result") {
|
|
46271
|
+
const failed = p.is_error === true;
|
|
46272
|
+
if (failed) shown.push(" \xB7 tool error");
|
|
46273
|
+
}
|
|
46274
|
+
}
|
|
46275
|
+
return {
|
|
46276
|
+
text: said.length > 0 ? said.join("\n") : null,
|
|
46277
|
+
show: shown.length > 0 ? shown.join("\n") : null
|
|
46278
|
+
};
|
|
46279
|
+
}
|
|
46280
|
+
if (e.type === "result") {
|
|
46281
|
+
const summary = typeof e.result === "string" ? e.result : null;
|
|
46282
|
+
const usage = usageIn(line);
|
|
46283
|
+
const spend = usage ? ` \u2014 ${usage.inputTokens ?? "?"} in / ${usage.outputTokens ?? "?"} out` + (usage.costUsd !== void 0 ? `, $${usage.costUsd.toFixed(4)}` : "") : "";
|
|
46284
|
+
return { text: summary, show: ` [${String(e.subtype ?? "result")}${spend}]` };
|
|
46285
|
+
}
|
|
46286
|
+
if (e.type === "system") return { text: null, show: null };
|
|
46287
|
+
return { text: null, show: null };
|
|
46288
|
+
}
|
|
46240
46289
|
var REFUSALS, num;
|
|
46241
46290
|
var init_harness_signal = __esm({
|
|
46242
46291
|
"src/harness-signal.ts"() {
|
|
46243
46292
|
"use strict";
|
|
46244
46293
|
REFUSALS = [
|
|
46245
|
-
|
|
46246
|
-
|
|
46294
|
+
// "hit your" AND "reached your". The original had only the first, and the
|
|
46295
|
+
// harness ships the second — `you have reached your weekly usage limit` is a
|
|
46296
|
+
// literal string in the 2.x binary. That one word cost 8,239 runs (CRA-89):
|
|
46297
|
+
// the refusal went unrecognised, so `neverRan` was false, so the backoff
|
|
46298
|
+
// never engaged and every card was charged for an outage.
|
|
46299
|
+
[
|
|
46300
|
+
/(hit|reached) your (monthly |weekly |daily )?(spend|usage) limit/i,
|
|
46301
|
+
"the account's usage limit is reached"
|
|
46302
|
+
],
|
|
46303
|
+
// The passive voice of the same thing, which is what the harness prints when
|
|
46304
|
+
// it is reporting rather than addressing you.
|
|
46305
|
+
[/(usage|spend|usage credit) limit reached/i, "the account's usage limit is reached"],
|
|
46306
|
+
// `is` optional: the binary carries BOTH "credit balance is too low" and
|
|
46307
|
+
// "credit balance too low", and the pattern only knew the longer one.
|
|
46308
|
+
[
|
|
46309
|
+
/insufficient credit|out of credit|credit balance (is )?too low/i,
|
|
46310
|
+
"the account is out of credit"
|
|
46311
|
+
],
|
|
46247
46312
|
[/upgrade to (a paid plan|claude pro)/i, "the plan does not cover this"],
|
|
46248
46313
|
[/invalid api key|authentication[_ ]error|please run \/login/i, "the harness is not signed in"],
|
|
46314
|
+
// Still requires the follow-up clause. A bare "rate limited" is left
|
|
46315
|
+
// UNMATCHED on purpose — it is a phrase a model writes about a codebase
|
|
46316
|
+
// ("the endpoint is rate limited"), and the note above is explicit that a
|
|
46317
|
+
// false positive is the worse error: it credits an issue an attempt it
|
|
46318
|
+
// should have been charged.
|
|
46249
46319
|
[/rate limit(ed| exceeded|s? reached)\b.*\btry again/i, "the account is rate limited"]
|
|
46250
46320
|
];
|
|
46251
46321
|
num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
@@ -46357,10 +46427,28 @@ async function createWorktree(ref, config3) {
|
|
|
46357
46427
|
const out = await git(config3.repo, ["rev-list", "--count", `${startCommit}..${branch}`]);
|
|
46358
46428
|
return Number(out) || 0;
|
|
46359
46429
|
};
|
|
46430
|
+
const isDirty = async () => (await git(path, ["status", "--porcelain"])).length > 0;
|
|
46360
46431
|
return {
|
|
46361
46432
|
path,
|
|
46362
46433
|
branch,
|
|
46363
46434
|
commits: countCommits,
|
|
46435
|
+
dirty: isDirty,
|
|
46436
|
+
async saveWip(reason) {
|
|
46437
|
+
if (!await isDirty()) return null;
|
|
46438
|
+
await git(path, ["add", "-A"]);
|
|
46439
|
+
const staged = await git(path, ["diff", "--cached", "--name-only"]);
|
|
46440
|
+
if (staged.length === 0) return null;
|
|
46441
|
+
const message = `wip(${ref}): saved by the supervisor when the run was cut short
|
|
46442
|
+
|
|
46443
|
+
The harness was ${reason} with this in the tree. NOT a handoff: the
|
|
46444
|
+
agent did not choose to commit it and nothing here has been reviewed.
|
|
46445
|
+
The next attempt on this card starts from here rather than from scratch.
|
|
46446
|
+
|
|
46447
|
+
Cruo-Wip: ${reason}
|
|
46448
|
+
`;
|
|
46449
|
+
await git(path, ["commit", "--no-verify", "-m", message]);
|
|
46450
|
+
return await git(path, ["rev-parse", "HEAD"]);
|
|
46451
|
+
},
|
|
46364
46452
|
async push(remote) {
|
|
46365
46453
|
assertPublishable(branch);
|
|
46366
46454
|
if (await countCommits() === 0 && !await hasRef(config3.repo, `${remote}/${branch}`)) {
|
|
@@ -46778,9 +46866,14 @@ function userPrompt(hit) {
|
|
|
46778
46866
|
`which your function owns. Pick it up and do your part.`
|
|
46779
46867
|
].join(" ");
|
|
46780
46868
|
}
|
|
46869
|
+
function headFor(head2) {
|
|
46870
|
+
const flat = head2.replace(/\s+/g, " ").trim();
|
|
46871
|
+
return flat === "" ? null : flat.slice(0, HEAD_STORED);
|
|
46872
|
+
}
|
|
46781
46873
|
async function invokeHarness(ctx, identity, hit, worktree) {
|
|
46782
46874
|
const cwd = worktree?.path ?? options.cwd;
|
|
46783
46875
|
const { path: mcpConfig, cleanup } = await writeMcpConfig();
|
|
46876
|
+
const streaming = options.usage === true;
|
|
46784
46877
|
const args = [
|
|
46785
46878
|
"-p",
|
|
46786
46879
|
userPrompt(hit),
|
|
@@ -46794,7 +46887,11 @@ async function invokeHarness(ctx, identity, hit, worktree) {
|
|
|
46794
46887
|
options.allow,
|
|
46795
46888
|
"--system-prompt",
|
|
46796
46889
|
systemPrompt(ctx, identity, worktree),
|
|
46797
|
-
...options.model ? ["--model", options.model] : []
|
|
46890
|
+
...options.model ? ["--model", options.model] : [],
|
|
46891
|
+
// `--verbose` is not optional alongside stream-json: without it the harness
|
|
46892
|
+
// emits only the final result and the live view is a blank terminal for the
|
|
46893
|
+
// length of the run.
|
|
46894
|
+
...streaming ? ["--output-format", "stream-json", "--verbose"] : []
|
|
46798
46895
|
];
|
|
46799
46896
|
try {
|
|
46800
46897
|
return await new Promise((resolve2) => {
|
|
@@ -46810,14 +46907,34 @@ async function invokeHarness(ctx, identity, hit, worktree) {
|
|
|
46810
46907
|
let stdoutBytes = 0;
|
|
46811
46908
|
let tail = "";
|
|
46812
46909
|
let head2 = "";
|
|
46910
|
+
let pending = "";
|
|
46911
|
+
const addHead = (text) => {
|
|
46912
|
+
if (head2.length < 2048) head2 += text.slice(0, 2048 - head2.length);
|
|
46913
|
+
};
|
|
46813
46914
|
child.stdout?.on("data", (c) => {
|
|
46814
46915
|
stdoutBytes += c.length;
|
|
46815
|
-
|
|
46816
|
-
|
|
46817
|
-
|
|
46916
|
+
const chunk = c.toString("utf8");
|
|
46917
|
+
if (!streaming) {
|
|
46918
|
+
addHead(chunk);
|
|
46919
|
+
tail = (tail + chunk).slice(-4096);
|
|
46920
|
+
process.stdout.write(c);
|
|
46921
|
+
return;
|
|
46922
|
+
}
|
|
46923
|
+
pending += chunk;
|
|
46924
|
+
const lines = pending.split("\n");
|
|
46925
|
+
pending = lines.pop() ?? "";
|
|
46926
|
+
for (const line of lines) {
|
|
46927
|
+
if (line.trim() === "") continue;
|
|
46928
|
+
tail = line;
|
|
46929
|
+
const rendered = renderEvent(line);
|
|
46930
|
+
if (rendered.text) addHead(rendered.text + "\n");
|
|
46931
|
+
if (rendered.show) process.stdout.write(rendered.show + "\n");
|
|
46932
|
+
}
|
|
46818
46933
|
});
|
|
46819
46934
|
child.stderr?.on("data", (c) => process.stderr.write(c));
|
|
46935
|
+
let killed = false;
|
|
46820
46936
|
const timer = setTimeout(() => {
|
|
46937
|
+
killed = true;
|
|
46821
46938
|
log(` harness exceeded ${options.harnessTimeoutMs / 1e3}s \u2014 killing it`);
|
|
46822
46939
|
child.kill("SIGTERM");
|
|
46823
46940
|
setTimeout(() => child.kill("SIGKILL"), 1e4).unref();
|
|
@@ -46830,17 +46947,28 @@ async function invokeHarness(ctx, identity, hit, worktree) {
|
|
|
46830
46947
|
stdoutBytes,
|
|
46831
46948
|
refusal: refusalIn(head2),
|
|
46832
46949
|
durationMs: Date.now() - startedAtMs,
|
|
46833
|
-
usage: null
|
|
46950
|
+
usage: null,
|
|
46951
|
+
killed,
|
|
46952
|
+
stdoutHead: headFor(head2)
|
|
46834
46953
|
});
|
|
46835
46954
|
});
|
|
46836
46955
|
child.on("close", (code) => {
|
|
46837
46956
|
clearTimeout(timer);
|
|
46957
|
+
if (streaming && pending.trim() !== "") {
|
|
46958
|
+
tail = pending;
|
|
46959
|
+
const rendered = renderEvent(pending);
|
|
46960
|
+
if (rendered.text) addHead(rendered.text + "\n");
|
|
46961
|
+
if (rendered.show) process.stdout.write(rendered.show + "\n");
|
|
46962
|
+
pending = "";
|
|
46963
|
+
}
|
|
46838
46964
|
resolve2({
|
|
46839
46965
|
code: code ?? 1,
|
|
46840
46966
|
stdoutBytes,
|
|
46841
46967
|
refusal: refusalIn(head2),
|
|
46842
46968
|
durationMs: Date.now() - startedAtMs,
|
|
46843
|
-
usage: usageIn(tail)
|
|
46969
|
+
usage: usageIn(tail),
|
|
46970
|
+
killed,
|
|
46971
|
+
stdoutHead: headFor(head2)
|
|
46844
46972
|
});
|
|
46845
46973
|
});
|
|
46846
46974
|
});
|
|
@@ -46933,6 +47061,7 @@ async function recordRun(ctx, hit, run, outcome) {
|
|
|
46933
47061
|
duration_ms: run.durationMs,
|
|
46934
47062
|
exit_code: run.code,
|
|
46935
47063
|
stdout_bytes: run.stdoutBytes,
|
|
47064
|
+
stdout_head: run.stdoutHead,
|
|
46936
47065
|
outcome,
|
|
46937
47066
|
model: run.usage?.model ?? null,
|
|
46938
47067
|
input_tokens: run.usage?.inputTokens ?? null,
|
|
@@ -47029,9 +47158,28 @@ async function tick(ctx, identity, deadTicks, allowance) {
|
|
|
47029
47158
|
}
|
|
47030
47159
|
let run;
|
|
47031
47160
|
let committed = 0;
|
|
47161
|
+
let salvaged = null;
|
|
47032
47162
|
try {
|
|
47033
47163
|
run = await invokeHarness(ctx, identity, hit, worktree);
|
|
47034
47164
|
if (worktree) committed = await worktree.commits();
|
|
47165
|
+
if (worktree && run.killed) {
|
|
47166
|
+
try {
|
|
47167
|
+
salvaged = await worktree.saveWip("killed at the harness timeout");
|
|
47168
|
+
if (salvaged) {
|
|
47169
|
+
log(
|
|
47170
|
+
` ${hit.ref} killed with an uncommitted tree \u2014 saved as ${salvaged.slice(0, 8)} on ${worktree.branch}; the next attempt resumes from it`
|
|
47171
|
+
);
|
|
47172
|
+
}
|
|
47173
|
+
} catch (error51) {
|
|
47174
|
+
log(
|
|
47175
|
+
` ${hit.ref} could not save the killed run's tree: ${error51 instanceof Error ? error51.message.split("\n")[0] : String(error51)}`
|
|
47176
|
+
);
|
|
47177
|
+
}
|
|
47178
|
+
} else if (worktree && committed === 0 && await worktree.dirty()) {
|
|
47179
|
+
log(
|
|
47180
|
+
` ${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"}`
|
|
47181
|
+
);
|
|
47182
|
+
}
|
|
47035
47183
|
} finally {
|
|
47036
47184
|
await release(ctx, hit);
|
|
47037
47185
|
}
|
|
@@ -47071,7 +47219,8 @@ async function tick(ctx, identity, deadTicks, allowance) {
|
|
|
47071
47219
|
}
|
|
47072
47220
|
} else {
|
|
47073
47221
|
const what = hit.reason === "mention" ? "no reply written" : "unchanged";
|
|
47074
|
-
const
|
|
47222
|
+
const note = salvaged ? `${what} (exit ${code}); cut short, work saved as ${salvaged.slice(0, 8)}` : `${what} (exit ${code})`;
|
|
47223
|
+
const n2 = await recordAttempt(ctx, hit, note, attemptsFor(hit));
|
|
47075
47224
|
log(
|
|
47076
47225
|
` ${hit.ref} ${what} after the run (exit ${code}) \u2014 attempt ${n2}/${options.maxAttempts}` + (n2 >= options.maxAttempts ? "; setting it aside, a human should look" : "")
|
|
47077
47226
|
);
|
|
@@ -47196,7 +47345,7 @@ async function main() {
|
|
|
47196
47345
|
await new Promise((r) => setTimeout(r, options.intervalMs));
|
|
47197
47346
|
}
|
|
47198
47347
|
}
|
|
47199
|
-
var argv, flag, opt, num2, ms, readOptions, options, log, worktreeConfig, lastRefusal, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV, warnedAboutSpendTable;
|
|
47348
|
+
var argv, flag, opt, num2, ms, readOptions, options, log, worktreeConfig, lastRefusal, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV, HEAD_STORED, warnedAboutSpendTable;
|
|
47200
47349
|
var init_supervisor = __esm({
|
|
47201
47350
|
"src/supervisor.ts"() {
|
|
47202
47351
|
"use strict";
|
|
@@ -47250,14 +47399,40 @@ var init_supervisor = __esm({
|
|
|
47250
47399
|
* supervisor down with it. Observed in the first live run: a harness that
|
|
47251
47400
|
* produced no output and no writes for ten minutes. A killed run counts as
|
|
47252
47401
|
* unproductive like any other, so the retry/set-aside path handles it.
|
|
47402
|
+
*
|
|
47403
|
+
* THIRTY MINUTES, and the number is a decision rather than a guess — the two
|
|
47404
|
+
* mistakes this sits between are nowhere near the same size (`CRA-105`).
|
|
47405
|
+
* Killing a wedged run late costs wall clock the supervisor was not using
|
|
47406
|
+
* anyway. Killing a WORKING run early costs everything it did: the checkout
|
|
47407
|
+
* is recreated from scratch on the next attempt, so an uncommitted tree is
|
|
47408
|
+
* discarded rather than resumed (`CRA-104`). Ten minutes was tuned on the
|
|
47409
|
+
* cards the loop was first exercised on, and `CRA-83` — four packages and a
|
|
47410
|
+
* test file — was killed at it twice, having written real work both times
|
|
47411
|
+
* and committed neither. Err long: the cheap failure is the slow one.
|
|
47253
47412
|
*/
|
|
47254
|
-
harnessTimeoutMs: ms("harness-timeout",
|
|
47413
|
+
harnessTimeoutMs: ms("harness-timeout", 1800),
|
|
47255
47414
|
/**
|
|
47256
47415
|
* Give up on an issue after this many unproductive runs. Persisted in
|
|
47257
47416
|
* `pm.issue_attempts` since CRA-7 — it was in-memory, which meant a restart
|
|
47258
47417
|
* forgot every set-aside and nothing outside the process could see one.
|
|
47259
47418
|
*/
|
|
47260
47419
|
maxAttempts: num2("max-attempts", { fallback: 3, integer: true }),
|
|
47420
|
+
/**
|
|
47421
|
+
* Ask the harness to report what each run spent — `CRA-89`.
|
|
47422
|
+
*
|
|
47423
|
+
* Off by default and that is a real choice, not caution. It appends
|
|
47424
|
+
* `--output-format stream-json --verbose`, which are Claude Code's flags, and
|
|
47425
|
+
* the supervisor's whole design is that `--harness` takes any command that
|
|
47426
|
+
* accepts a prompt and exits. Turning this on says "my harness is Claude Code
|
|
47427
|
+
* shaped"; leaving it off keeps the promise.
|
|
47428
|
+
*
|
|
47429
|
+
* What it costs: the harness's stdout stops being prose and becomes NDJSON,
|
|
47430
|
+
* so the live view is reconstructed by `renderEvent` rather than passed
|
|
47431
|
+
* through. What it buys is the only numbers there are — without it every
|
|
47432
|
+
* token column in `pm.harness_runs` is null, which is what 8,335 rows of it
|
|
47433
|
+
* were.
|
|
47434
|
+
*/
|
|
47435
|
+
usage: flag("usage"),
|
|
47261
47436
|
/**
|
|
47262
47437
|
* Ceiling on how long invocation is held back after a run of dead ticks.
|
|
47263
47438
|
* Ten minutes: long enough that a broken harness stops being spawned over and
|
|
@@ -47350,6 +47525,7 @@ cruo-supervisor: ${error51.message}
|
|
|
47350
47525
|
// stands in for. Cheap to list, and wrong to discover the hard way.
|
|
47351
47526
|
"CRUO_SESSION"
|
|
47352
47527
|
];
|
|
47528
|
+
HEAD_STORED = 200;
|
|
47353
47529
|
warnedAboutSpendTable = false;
|
|
47354
47530
|
main().catch((e) => {
|
|
47355
47531
|
console.error(e instanceof Error ? e.message : e);
|
|
@@ -47391,6 +47567,7 @@ Common options
|
|
|
47391
47567
|
--allow <tools> what the harness may use; default is the board only
|
|
47392
47568
|
--push publish the branch after a run that commits
|
|
47393
47569
|
--interval <seconds> how often to poll (default 20)
|
|
47570
|
+
--usage record what each run costs (needs a Claude Code harness)
|
|
47394
47571
|
|
|
47395
47572
|
Full list and what each one is for: https://cruo.space/docs#agents
|
|
47396
47573
|
|
package/package.json
CHANGED