cruo-agent 0.1.0 → 0.1.2
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 +23 -1
- package/dist/VERSION +1 -1
- package/dist/cli.js +163 -81
- package/dist/index.js +3 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -60,6 +60,28 @@ Run it from the repository the agent should work on. If that repository needs
|
|
|
60
60
|
installing before its tests will run, add `--prepare 'npm ci'` or whatever your
|
|
61
61
|
equivalent is — a fresh checkout has your source and none of your dependencies.
|
|
62
62
|
|
|
63
|
+
Checkouts go under `~/.cruo-work/<agent>`, one directory per agent, so **running
|
|
64
|
+
several agents at once needs no extra flags**. `--worktree-root` overrides it if
|
|
65
|
+
you want them somewhere else; pointing two agents at one root is safe too, since
|
|
66
|
+
the sweep that clears abandoned checkouts at startup only removes ones too old
|
|
67
|
+
for any run to still be inside.
|
|
68
|
+
|
|
69
|
+
## When it stops doing anything
|
|
70
|
+
|
|
71
|
+
A supervisor sitting idle costs nothing — it is a database query on a timer. If
|
|
72
|
+
the *harness* refuses to work, though, the supervisor says so and slows down:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
! the harness is refusing to work: the account's spend limit is reached
|
|
76
|
+
No card is charged for this, and polling backs off until it changes.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
That distinction matters more than it looks. A refusal is a fact about your
|
|
80
|
+
account, not about the card, so those runs are **not** counted against the
|
|
81
|
+
card's `--max-attempts` — otherwise one billing problem would quietly set aside
|
|
82
|
+
every issue the agent was holding, and you would come back to a board that had
|
|
83
|
+
given up on work nobody abandoned.
|
|
84
|
+
|
|
63
85
|
## Common options
|
|
64
86
|
|
|
65
87
|
| flag | |
|
|
@@ -74,7 +96,7 @@ equivalent is — a fresh checkout has your source and none of your dependencies
|
|
|
74
96
|
| `--harness-timeout <seconds>` | kill a run that wedges (default 600) |
|
|
75
97
|
| `--max-attempts <n>` | give up on a card after n unproductive runs (default 3) |
|
|
76
98
|
|
|
77
|
-
The full list, and what each is for: <https://cruo.space/agents>
|
|
99
|
+
The full list, and what each is for: <https://cruo.space/docs#agents>
|
|
78
100
|
|
|
79
101
|
## Running it somewhere that is not your laptop
|
|
80
102
|
|
package/dist/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.2
|
package/dist/cli.js
CHANGED
|
@@ -45,6 +45,64 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
45
45
|
));
|
|
46
46
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
47
47
|
|
|
48
|
+
// src/options.ts
|
|
49
|
+
function stripTokenFlag(argv2) {
|
|
50
|
+
const at = argv2.indexOf("--token");
|
|
51
|
+
if (at < 0) return [...argv2];
|
|
52
|
+
return [...argv2.slice(0, at), ...argv2.slice(at + 2)];
|
|
53
|
+
}
|
|
54
|
+
function numericIn(argv2, name, spec = {}) {
|
|
55
|
+
const i = argv2.indexOf(`--${name}`);
|
|
56
|
+
if (i < 0) return spec.fallback;
|
|
57
|
+
const raw = argv2[i + 1];
|
|
58
|
+
if (raw === void 0 || raw.startsWith("--")) {
|
|
59
|
+
throw new OptionError(
|
|
60
|
+
`--${name} expects a ${spec.integer ? "whole " : ""}number${spec.unit ? ` of ${spec.unit}` : ""}, but no value followed it.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const value = raw.trim() === "" ? NaN : Number(raw);
|
|
64
|
+
const what = `--${name} ${JSON.stringify(raw)}`;
|
|
65
|
+
if (!Number.isFinite(value)) {
|
|
66
|
+
throw new OptionError(
|
|
67
|
+
`${what} is not a number.` + // Reached only by something like `-abc`. A plain `-5` is a finite
|
|
68
|
+
// negative and falls to the range check below, which is the accurate
|
|
69
|
+
// refusal for it — this hint is for the case where the leading dash is
|
|
70
|
+
// the reason it did not parse at all.
|
|
71
|
+
(raw.startsWith("-") ? " A leading `-` reads as a flag, not a sign." : "")
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (value <= 0) {
|
|
75
|
+
throw new OptionError(`${what} must be greater than zero${spec.unit ? ` ${spec.unit}` : ""}.`);
|
|
76
|
+
}
|
|
77
|
+
if (spec.integer && !Number.isInteger(value)) {
|
|
78
|
+
throw new OptionError(`${what} must be a whole number.`);
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
function requiredNumericIn(argv2, name, fallback, spec = {}) {
|
|
83
|
+
return numericIn(argv2, name, { ...spec, fallback });
|
|
84
|
+
}
|
|
85
|
+
function secondsIn(argv2, name, fallbackSeconds) {
|
|
86
|
+
return requiredNumericIn(argv2, name, fallbackSeconds, { unit: "seconds" }) * 1e3;
|
|
87
|
+
}
|
|
88
|
+
var OptionError, flagIn, optIn;
|
|
89
|
+
var init_options = __esm({
|
|
90
|
+
"src/options.ts"() {
|
|
91
|
+
"use strict";
|
|
92
|
+
OptionError = class extends Error {
|
|
93
|
+
constructor(message) {
|
|
94
|
+
super(message);
|
|
95
|
+
this.name = "OptionError";
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
flagIn = (argv2, name) => argv2.includes(`--${name}`);
|
|
99
|
+
optIn = (argv2, name, fallback) => {
|
|
100
|
+
const i = argv2.indexOf(`--${name}`);
|
|
101
|
+
return i >= 0 && argv2[i + 1] && !argv2[i + 1].startsWith("--") ? argv2[i + 1] : fallback;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
48
106
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
|
|
49
107
|
// @__NO_SIDE_EFFECTS__
|
|
50
108
|
function $constructor(name, initializer3, params) {
|
|
@@ -15238,10 +15296,13 @@ var init_plans = __esm({
|
|
|
15238
15296
|
});
|
|
15239
15297
|
|
|
15240
15298
|
// ../../packages/core/dist/mcp-connect.js
|
|
15241
|
-
var CRUO_CLOUD;
|
|
15299
|
+
var DOCS_URL, AGENT_DOCS_URL, MCP_DOCS_URL, CRUO_CLOUD;
|
|
15242
15300
|
var init_mcp_connect = __esm({
|
|
15243
15301
|
"../../packages/core/dist/mcp-connect.js"() {
|
|
15244
15302
|
"use strict";
|
|
15303
|
+
DOCS_URL = "https://cruo.space/docs";
|
|
15304
|
+
AGENT_DOCS_URL = `${DOCS_URL}#agents`;
|
|
15305
|
+
MCP_DOCS_URL = `${DOCS_URL}#mcp`;
|
|
15245
15306
|
CRUO_CLOUD = {
|
|
15246
15307
|
apiUrl: "https://szsutuujxbldkykcvdrm.supabase.co",
|
|
15247
15308
|
publishableKey: "sb_publishable_hpnQFQ8IV0JXTch51XAstA_-S8H1eaD",
|
|
@@ -45940,62 +46001,34 @@ var init_auth = __esm({
|
|
|
45940
46001
|
}
|
|
45941
46002
|
});
|
|
45942
46003
|
|
|
45943
|
-
// src/
|
|
45944
|
-
function
|
|
45945
|
-
|
|
45946
|
-
|
|
45947
|
-
|
|
45948
|
-
if (raw === void 0 || raw.startsWith("--")) {
|
|
45949
|
-
throw new OptionError(
|
|
45950
|
-
`--${name} expects a ${spec.integer ? "whole " : ""}number${spec.unit ? ` of ${spec.unit}` : ""}, but no value followed it.`
|
|
45951
|
-
);
|
|
46004
|
+
// src/harness-signal.ts
|
|
46005
|
+
function refusalIn(stdout) {
|
|
46006
|
+
if (!stdout.trim()) return null;
|
|
46007
|
+
for (const [pattern, reason] of REFUSALS) {
|
|
46008
|
+
if (pattern.test(stdout)) return reason;
|
|
45952
46009
|
}
|
|
45953
|
-
|
|
45954
|
-
const what = `--${name} ${JSON.stringify(raw)}`;
|
|
45955
|
-
if (!Number.isFinite(value)) {
|
|
45956
|
-
throw new OptionError(
|
|
45957
|
-
`${what} is not a number.` + // Reached only by something like `-abc`. A plain `-5` is a finite
|
|
45958
|
-
// negative and falls to the range check below, which is the accurate
|
|
45959
|
-
// refusal for it — this hint is for the case where the leading dash is
|
|
45960
|
-
// the reason it did not parse at all.
|
|
45961
|
-
(raw.startsWith("-") ? " A leading `-` reads as a flag, not a sign." : "")
|
|
45962
|
-
);
|
|
45963
|
-
}
|
|
45964
|
-
if (value <= 0) {
|
|
45965
|
-
throw new OptionError(`${what} must be greater than zero${spec.unit ? ` ${spec.unit}` : ""}.`);
|
|
45966
|
-
}
|
|
45967
|
-
if (spec.integer && !Number.isInteger(value)) {
|
|
45968
|
-
throw new OptionError(`${what} must be a whole number.`);
|
|
45969
|
-
}
|
|
45970
|
-
return value;
|
|
45971
|
-
}
|
|
45972
|
-
function requiredNumericIn(argv2, name, fallback, spec = {}) {
|
|
45973
|
-
return numericIn(argv2, name, { ...spec, fallback });
|
|
46010
|
+
return null;
|
|
45974
46011
|
}
|
|
45975
|
-
function
|
|
45976
|
-
return
|
|
46012
|
+
function neverRan(run) {
|
|
46013
|
+
return run.refusal !== null || run.stdoutBytes === 0;
|
|
45977
46014
|
}
|
|
45978
|
-
var
|
|
45979
|
-
var
|
|
45980
|
-
"src/
|
|
46015
|
+
var REFUSALS;
|
|
46016
|
+
var init_harness_signal = __esm({
|
|
46017
|
+
"src/harness-signal.ts"() {
|
|
45981
46018
|
"use strict";
|
|
45982
|
-
|
|
45983
|
-
|
|
45984
|
-
|
|
45985
|
-
|
|
45986
|
-
|
|
45987
|
-
|
|
45988
|
-
|
|
45989
|
-
optIn = (argv2, name, fallback) => {
|
|
45990
|
-
const i = argv2.indexOf(`--${name}`);
|
|
45991
|
-
return i >= 0 && argv2[i + 1] && !argv2[i + 1].startsWith("--") ? argv2[i + 1] : fallback;
|
|
45992
|
-
};
|
|
46019
|
+
REFUSALS = [
|
|
46020
|
+
[/hit your (monthly |weekly |daily )?(spend|usage) limit/i, "the account's spend limit is reached"],
|
|
46021
|
+
[/insufficient credit|out of credit|credit balance is too low/i, "the account is out of credit"],
|
|
46022
|
+
[/upgrade to (a paid plan|claude pro)/i, "the plan does not cover this"],
|
|
46023
|
+
[/invalid api key|authentication[_ ]error|please run \/login/i, "the harness is not signed in"],
|
|
46024
|
+
[/rate limit(ed| exceeded|s? reached)\b.*\btry again/i, "the account is rate limited"]
|
|
46025
|
+
];
|
|
45993
46026
|
}
|
|
45994
46027
|
});
|
|
45995
46028
|
|
|
45996
46029
|
// src/worktree.ts
|
|
45997
46030
|
import { execFile } from "node:child_process";
|
|
45998
|
-
import { mkdir, readdir, rm } from "node:fs/promises";
|
|
46031
|
+
import { mkdir, readdir, rm, stat } from "node:fs/promises";
|
|
45999
46032
|
import { join } from "node:path";
|
|
46000
46033
|
import { promisify } from "node:util";
|
|
46001
46034
|
async function git(repo, args) {
|
|
@@ -46078,7 +46111,7 @@ async function createWorktree(ref, config3) {
|
|
|
46078
46111
|
}
|
|
46079
46112
|
};
|
|
46080
46113
|
}
|
|
46081
|
-
async function sweep(config3) {
|
|
46114
|
+
async function sweep(config3, maxRunMs = 0) {
|
|
46082
46115
|
await git(config3.repo, ["worktree", "prune"]);
|
|
46083
46116
|
let entries;
|
|
46084
46117
|
try {
|
|
@@ -46086,9 +46119,17 @@ async function sweep(config3) {
|
|
|
46086
46119
|
} catch {
|
|
46087
46120
|
return [];
|
|
46088
46121
|
}
|
|
46122
|
+
const cutoff = Date.now() - Math.max(maxRunMs + 5 * 6e4, 60 * 6e4);
|
|
46089
46123
|
const removed = [];
|
|
46090
46124
|
for (const name of entries) {
|
|
46091
46125
|
const path = join(config3.root, name);
|
|
46126
|
+
let touched;
|
|
46127
|
+
try {
|
|
46128
|
+
touched = (await stat(path)).mtimeMs;
|
|
46129
|
+
} catch {
|
|
46130
|
+
continue;
|
|
46131
|
+
}
|
|
46132
|
+
if (touched >= cutoff) continue;
|
|
46092
46133
|
await git(config3.repo, ["worktree", "remove", "--force", path]).catch(async () => {
|
|
46093
46134
|
await rm(path, { recursive: true, force: true });
|
|
46094
46135
|
});
|
|
@@ -46109,7 +46150,7 @@ var init_worktree = __esm({
|
|
|
46109
46150
|
var supervisor_exports = {};
|
|
46110
46151
|
import { spawn } from "node:child_process";
|
|
46111
46152
|
import { createRequire } from "node:module";
|
|
46112
|
-
import { mkdtemp, readdir as readdir2, readFile, rm as rm2, stat, writeFile } from "node:fs/promises";
|
|
46153
|
+
import { mkdtemp, readdir as readdir2, readFile, rm as rm2, stat as stat2, writeFile } from "node:fs/promises";
|
|
46113
46154
|
import { homedir, tmpdir } from "node:os";
|
|
46114
46155
|
import { join as join2 } from "node:path";
|
|
46115
46156
|
import { fileURLToPath } from "node:url";
|
|
@@ -46132,6 +46173,12 @@ async function identify(ctx) {
|
|
|
46132
46173
|
capabilities: member.capabilities
|
|
46133
46174
|
};
|
|
46134
46175
|
}
|
|
46176
|
+
function resolveWorktreeRoot(identity) {
|
|
46177
|
+
if (options.worktreeRoot) return options.worktreeRoot;
|
|
46178
|
+
const slug = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
46179
|
+
const name = slug(identity.name) || slug(identity.email.split("@")[0] ?? "") || "agent";
|
|
46180
|
+
return join2(homedir(), ".cruo-work", name);
|
|
46181
|
+
}
|
|
46135
46182
|
async function pollAssigned(ctx) {
|
|
46136
46183
|
const { data: issues, error: error51 } = await ctx.client.from("issues").select("*").eq("assignee_id", ctx.userId).order("created_at", { ascending: true });
|
|
46137
46184
|
if (error51) throw new Error(`Assignment poll failed: ${error51.message}`);
|
|
@@ -46247,17 +46294,25 @@ async function pollMentions(ctx, identity) {
|
|
|
46247
46294
|
);
|
|
46248
46295
|
}
|
|
46249
46296
|
async function claim(ctx, hit) {
|
|
46250
|
-
const
|
|
46251
|
-
const { error: error51 } = await ctx.client.schema("
|
|
46252
|
-
|
|
46253
|
-
|
|
46254
|
-
|
|
46255
|
-
|
|
46256
|
-
|
|
46257
|
-
|
|
46258
|
-
|
|
46259
|
-
|
|
46260
|
-
|
|
46297
|
+
const ttlMs = options.harnessTimeoutMs + 3e4;
|
|
46298
|
+
const { data, error: error51 } = await ctx.client.schema("public").rpc("claim_issue", {
|
|
46299
|
+
issue: hit.issue.id,
|
|
46300
|
+
ttl_ms: ttlMs
|
|
46301
|
+
});
|
|
46302
|
+
if (error51) {
|
|
46303
|
+
log(` could not claim ${hit.ref}: ${error51.message} \u2014 leaving it`);
|
|
46304
|
+
return false;
|
|
46305
|
+
}
|
|
46306
|
+
return data === true;
|
|
46307
|
+
}
|
|
46308
|
+
async function claimedElsewhere(ctx, hits) {
|
|
46309
|
+
if (hits.length === 0) return /* @__PURE__ */ new Set();
|
|
46310
|
+
const { data, error: error51 } = await ctx.client.schema("pm").from("issue_claims").select("issue_id, claimed_by").in("issue_id", hits.map((h) => h.issue.id)).gt("expires_at", (/* @__PURE__ */ new Date()).toISOString()).neq("claimed_by", ctx.userId);
|
|
46311
|
+
if (error51) {
|
|
46312
|
+
log(` could not read claims: ${error51.message} \u2014 relying on the claim itself`);
|
|
46313
|
+
return /* @__PURE__ */ new Set();
|
|
46314
|
+
}
|
|
46315
|
+
return new Set((data ?? []).map((r) => r.issue_id));
|
|
46261
46316
|
}
|
|
46262
46317
|
async function release(ctx, hit) {
|
|
46263
46318
|
const { error: error51 } = await ctx.client.schema("pm").from("issue_claims").delete().eq("issue_id", hit.issue.id).eq("claimed_by", ctx.userId);
|
|
@@ -46317,7 +46372,7 @@ async function sweepStaleConfigs() {
|
|
|
46317
46372
|
if (!name.startsWith(CONFIG_DIR_PREFIX)) continue;
|
|
46318
46373
|
const path = join2(dir, name);
|
|
46319
46374
|
try {
|
|
46320
|
-
const info = await
|
|
46375
|
+
const info = await stat2(path);
|
|
46321
46376
|
if (info.mtimeMs >= cutoff) continue;
|
|
46322
46377
|
await rm2(path, { recursive: true, force: true });
|
|
46323
46378
|
removed += 1;
|
|
@@ -46447,9 +46502,6 @@ function userPrompt(hit) {
|
|
|
46447
46502
|
`which your function owns. Pick it up and do your part.`
|
|
46448
46503
|
].join(" ");
|
|
46449
46504
|
}
|
|
46450
|
-
function neverRan(run) {
|
|
46451
|
-
return run.code !== 0 && run.stdoutBytes === 0;
|
|
46452
|
-
}
|
|
46453
46505
|
async function invokeHarness(ctx, identity, hit, worktree) {
|
|
46454
46506
|
const cwd = worktree?.path ?? options.cwd;
|
|
46455
46507
|
const { path: mcpConfig, cleanup } = await writeMcpConfig();
|
|
@@ -46479,8 +46531,10 @@ async function invokeHarness(ctx, identity, hit, worktree) {
|
|
|
46479
46531
|
env: harnessEnv()
|
|
46480
46532
|
});
|
|
46481
46533
|
let stdoutBytes = 0;
|
|
46534
|
+
let head2 = "";
|
|
46482
46535
|
child.stdout?.on("data", (c) => {
|
|
46483
46536
|
stdoutBytes += c.length;
|
|
46537
|
+
if (head2.length < 2048) head2 += c.toString("utf8").slice(0, 2048 - head2.length);
|
|
46484
46538
|
process.stdout.write(c);
|
|
46485
46539
|
});
|
|
46486
46540
|
child.stderr?.on("data", (c) => process.stderr.write(c));
|
|
@@ -46492,11 +46546,11 @@ async function invokeHarness(ctx, identity, hit, worktree) {
|
|
|
46492
46546
|
child.on("error", (e) => {
|
|
46493
46547
|
clearTimeout(timer);
|
|
46494
46548
|
log(` harness failed to start: ${e.message}`);
|
|
46495
|
-
resolve2({ code: 127, stdoutBytes });
|
|
46549
|
+
resolve2({ code: 127, stdoutBytes, refusal: refusalIn(head2) });
|
|
46496
46550
|
});
|
|
46497
46551
|
child.on("close", (code) => {
|
|
46498
46552
|
clearTimeout(timer);
|
|
46499
|
-
resolve2({ code: code ?? 1, stdoutBytes });
|
|
46553
|
+
resolve2({ code: code ?? 1, stdoutBytes, refusal: refusalIn(head2) });
|
|
46500
46554
|
});
|
|
46501
46555
|
});
|
|
46502
46556
|
} finally {
|
|
@@ -46588,11 +46642,17 @@ async function tick(ctx, identity, deadTicks) {
|
|
|
46588
46642
|
seen.add(h.ref);
|
|
46589
46643
|
return true;
|
|
46590
46644
|
});
|
|
46591
|
-
const
|
|
46645
|
+
const heldByOthers = await claimedElsewhere(ctx, hits);
|
|
46646
|
+
const free = hits.filter((h) => !heldByOthers.has(h.issue.id));
|
|
46647
|
+
if (heldByOthers.size > 0) {
|
|
46648
|
+
log(`${heldByOthers.size} issue(s) already being worked by someone else`);
|
|
46649
|
+
}
|
|
46650
|
+
const attemptsByHit = await loadAttempts(ctx, free);
|
|
46592
46651
|
const attemptsFor = (h) => attemptsByHit.get(`${h.reason}:${h.issue.id}`) ?? 0;
|
|
46593
|
-
const actionable =
|
|
46652
|
+
const actionable = free.filter((h) => attemptsFor(h) < options.maxAttempts);
|
|
46594
46653
|
if (actionable.length === 0) {
|
|
46595
|
-
if (
|
|
46654
|
+
if (free.length > 0) log(`${free.length} issue(s) waiting, all set aside`);
|
|
46655
|
+
else if (heldByOthers.size > 0) log(`nothing free \u2014 everything waiting is claimed`);
|
|
46596
46656
|
await beat(ctx, { holding: 0, deadTicks, intervalMs: options.intervalMs });
|
|
46597
46657
|
return { invoked: 0, failedToRun: 0 };
|
|
46598
46658
|
}
|
|
@@ -46621,6 +46681,10 @@ async function tick(ctx, identity, deadTicks) {
|
|
|
46621
46681
|
invoked += 1;
|
|
46622
46682
|
const before = { status: hit.issue.status_id, assignee: hit.issue.assignee_id };
|
|
46623
46683
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
46684
|
+
if (!await claim(ctx, hit)) {
|
|
46685
|
+
log(` ${hit.ref} is being worked by another agent \u2014 skipping`);
|
|
46686
|
+
continue;
|
|
46687
|
+
}
|
|
46624
46688
|
let worktree = null;
|
|
46625
46689
|
if (worktreeConfig) {
|
|
46626
46690
|
try {
|
|
@@ -46628,12 +46692,12 @@ async function tick(ctx, identity, deadTicks) {
|
|
|
46628
46692
|
log(` ${hit.ref} worktree ${worktree.path} on ${worktree.branch}`);
|
|
46629
46693
|
} catch (error51) {
|
|
46630
46694
|
failedToRun += 1;
|
|
46695
|
+
await release(ctx, hit);
|
|
46631
46696
|
log(` ${hit.ref} could not prepare a worktree \u2014 not counted against this issue`);
|
|
46632
46697
|
log(` ${error51 instanceof Error ? error51.message.split("\n")[0] : String(error51)}`);
|
|
46633
46698
|
continue;
|
|
46634
46699
|
}
|
|
46635
46700
|
}
|
|
46636
|
-
await claim(ctx, hit);
|
|
46637
46701
|
let run;
|
|
46638
46702
|
let committed = 0;
|
|
46639
46703
|
try {
|
|
@@ -46645,9 +46709,17 @@ async function tick(ctx, identity, deadTicks) {
|
|
|
46645
46709
|
const code = run.code;
|
|
46646
46710
|
if (neverRan(run)) {
|
|
46647
46711
|
failedToRun += 1;
|
|
46648
|
-
|
|
46649
|
-
|
|
46650
|
-
|
|
46712
|
+
if (run.refusal) {
|
|
46713
|
+
if (run.refusal !== lastRefusal) {
|
|
46714
|
+
log(`! the harness is refusing to work: ${run.refusal}`);
|
|
46715
|
+
log(` No card is charged for this, and polling backs off until it changes.`);
|
|
46716
|
+
lastRefusal = run.refusal;
|
|
46717
|
+
}
|
|
46718
|
+
} else {
|
|
46719
|
+
log(
|
|
46720
|
+
` ${hit.ref} the harness exited ${code} without reaching a model \u2014 not counted against this issue`
|
|
46721
|
+
);
|
|
46722
|
+
}
|
|
46651
46723
|
continue;
|
|
46652
46724
|
}
|
|
46653
46725
|
const { data: after } = await ctx.client.from("issues").select("status_id, assignee_id").eq("id", hit.issue.id).maybeSingle();
|
|
@@ -46684,6 +46756,7 @@ async function tick(ctx, identity, deadTicks) {
|
|
|
46684
46756
|
}
|
|
46685
46757
|
}
|
|
46686
46758
|
}
|
|
46759
|
+
if (failedToRun < invoked) lastRefusal = null;
|
|
46687
46760
|
const allDead = invoked > 0 && failedToRun === invoked;
|
|
46688
46761
|
if (allDead) {
|
|
46689
46762
|
log(`! nothing reached a model this tick (${invoked}/${invoked} failed to run).`);
|
|
@@ -46722,20 +46795,20 @@ async function main() {
|
|
|
46722
46795
|
const base = await resolveBase(options.repo, options.base);
|
|
46723
46796
|
worktreeConfig = {
|
|
46724
46797
|
repo: options.repo,
|
|
46725
|
-
root:
|
|
46798
|
+
root: resolveWorktreeRoot(identity),
|
|
46726
46799
|
base,
|
|
46727
46800
|
prepare: options.prepare,
|
|
46728
46801
|
prepareTimeoutMs: options.prepareTimeout,
|
|
46729
46802
|
keepFailed: options.keepFailed
|
|
46730
46803
|
};
|
|
46731
|
-
log(`worktrees in ${
|
|
46804
|
+
log(`worktrees in ${worktreeConfig.root}, branching from ${base} in ${options.repo}`);
|
|
46732
46805
|
if (base === "HEAD") {
|
|
46733
46806
|
log(`! no origin/main or main \u2014 branching from HEAD, so tasks inherit your working state`);
|
|
46734
46807
|
}
|
|
46735
46808
|
if (!options.prepare) {
|
|
46736
46809
|
log(` no --prepare: a fresh worktree has no dependencies and no built packages`);
|
|
46737
46810
|
}
|
|
46738
|
-
const swept = await sweep(worktreeConfig);
|
|
46811
|
+
const swept = await sweep(worktreeConfig, options.harnessTimeoutMs + options.prepareTimeout);
|
|
46739
46812
|
if (swept.length) log(` swept ${swept.length} worktree(s) left by an earlier run: ${swept.join(", ")}`);
|
|
46740
46813
|
if (options.push) log(` publishing to ${options.pushRemote} after any run that commits`);
|
|
46741
46814
|
if (options.allow === "mcp__cruo") {
|
|
@@ -46770,7 +46843,7 @@ async function main() {
|
|
|
46770
46843
|
await new Promise((r) => setTimeout(r, wait));
|
|
46771
46844
|
}
|
|
46772
46845
|
}
|
|
46773
|
-
var argv, flag, opt, num, ms, readOptions, options, log, worktreeConfig, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV;
|
|
46846
|
+
var argv, flag, opt, num, ms, readOptions, options, log, worktreeConfig, lastRefusal, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV;
|
|
46774
46847
|
var init_supervisor = __esm({
|
|
46775
46848
|
"src/supervisor.ts"() {
|
|
46776
46849
|
"use strict";
|
|
@@ -46779,6 +46852,7 @@ var init_supervisor = __esm({
|
|
|
46779
46852
|
init_env2();
|
|
46780
46853
|
init_auth();
|
|
46781
46854
|
init_options();
|
|
46855
|
+
init_harness_signal();
|
|
46782
46856
|
init_worktree();
|
|
46783
46857
|
argv = process.argv.slice(2);
|
|
46784
46858
|
flag = (name) => flagIn(argv, name);
|
|
@@ -46845,7 +46919,13 @@ var init_supervisor = __esm({
|
|
|
46845
46919
|
* legitimate setup, and so is a PM agent with no filesystem at all.
|
|
46846
46920
|
*/
|
|
46847
46921
|
worktree: flag("worktree"),
|
|
46848
|
-
|
|
46922
|
+
/**
|
|
46923
|
+
* Where worktrees are created. Defaults to `~/.cruo-work/<agent>` — per
|
|
46924
|
+
* AGENT, not one shared directory — which needs this agent's name and so is
|
|
46925
|
+
* resolved in `main` rather than here. Two supervisors sharing a root is a
|
|
46926
|
+
* setup someone has to ask for; see `resolveWorktreeRoot`.
|
|
46927
|
+
*/
|
|
46928
|
+
worktreeRoot: opt("worktree-root"),
|
|
46849
46929
|
/** The repo to branch from. Defaults to wherever the supervisor was started. */
|
|
46850
46930
|
repo: opt("repo", process.cwd()),
|
|
46851
46931
|
/** Ref new task branches are cut from. Resolved at startup; see `resolveBase`. */
|
|
@@ -46890,6 +46970,7 @@ cruo-supervisor: ${error51.message}
|
|
|
46890
46970
|
})();
|
|
46891
46971
|
log = (...parts) => console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })}]`, ...parts);
|
|
46892
46972
|
worktreeConfig = null;
|
|
46973
|
+
lastRefusal = null;
|
|
46893
46974
|
PRIORITY_RANK = {
|
|
46894
46975
|
urgent: 0,
|
|
46895
46976
|
high: 1,
|
|
@@ -46919,6 +47000,7 @@ cruo-supervisor: ${error51.message}
|
|
|
46919
47000
|
});
|
|
46920
47001
|
|
|
46921
47002
|
// src/cli.ts
|
|
47003
|
+
init_options();
|
|
46922
47004
|
import { chmod, mkdir as mkdir2, readFile as readFile2, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
|
|
46923
47005
|
import { homedir as homedir2 } from "node:os";
|
|
46924
47006
|
import { join as join3 } from "node:path";
|
|
@@ -46942,7 +47024,7 @@ Common options
|
|
|
46942
47024
|
--push publish the branch after a run that commits
|
|
46943
47025
|
--interval <seconds> how often to poll (default 20)
|
|
46944
47026
|
|
|
46945
|
-
Full list and what each one is for: https://cruo.space/agents
|
|
47027
|
+
Full list and what each one is for: https://cruo.space/docs#agents
|
|
46946
47028
|
|
|
46947
47029
|
The token comes from CRUO_TOKEN, then --token, then whatever \`login\` stored.
|
|
46948
47030
|
`;
|
|
@@ -47014,7 +47096,7 @@ Get one from Cruo \u2192 Settings \u2192 Members \u2192 Add an agent.
|
|
|
47014
47096
|
process.exit(2);
|
|
47015
47097
|
}
|
|
47016
47098
|
process.env.CRUO_TOKEN = token;
|
|
47017
|
-
const passthrough = argv2
|
|
47099
|
+
const passthrough = stripTokenFlag(argv2);
|
|
47018
47100
|
process.argv = [process.argv[0], process.argv[1], ...passthrough];
|
|
47019
47101
|
await Promise.resolve().then(() => (init_supervisor(), supervisor_exports));
|
|
47020
47102
|
}
|
package/dist/index.js
CHANGED
|
@@ -34986,6 +34986,9 @@ var signupSchema = external_exports.object({
|
|
|
34986
34986
|
});
|
|
34987
34987
|
|
|
34988
34988
|
// ../../packages/core/dist/mcp-connect.js
|
|
34989
|
+
var DOCS_URL = "https://cruo.space/docs";
|
|
34990
|
+
var AGENT_DOCS_URL = `${DOCS_URL}#agents`;
|
|
34991
|
+
var MCP_DOCS_URL = `${DOCS_URL}#mcp`;
|
|
34989
34992
|
var CRUO_CLOUD = {
|
|
34990
34993
|
apiUrl: "https://szsutuujxbldkykcvdrm.supabase.co",
|
|
34991
34994
|
publishableKey: "sb_publishable_hpnQFQ8IV0JXTch51XAstA_-S8H1eaD",
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cruo-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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
|
-
"homepage": "https://cruo.space/agents",
|
|
6
|
+
"homepage": "https://cruo.space/docs#agents",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"cruo",
|
|
9
9
|
"agent",
|
|
@@ -29,12 +29,13 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"build": "node scripts/build.mjs"
|
|
32
|
+
"build": "node scripts/build.mjs",
|
|
33
|
+
"prepublishOnly": "node scripts/build.mjs"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"@cruo/mcp": "workspace:*"
|
|
36
37
|
},
|
|
37
38
|
"bugs": {
|
|
38
|
-
"url": "https://cruo.space/agents"
|
|
39
|
+
"url": "https://cruo.space/docs#agents"
|
|
39
40
|
}
|
|
40
41
|
}
|