cruo-agent 0.1.1 → 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 +34 -16
- package/package.json +3 -3
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
|
@@ -46294,17 +46294,25 @@ async function pollMentions(ctx, identity) {
|
|
|
46294
46294
|
);
|
|
46295
46295
|
}
|
|
46296
46296
|
async function claim(ctx, hit) {
|
|
46297
|
-
const
|
|
46298
|
-
const { error: error51 } = await ctx.client.schema("
|
|
46299
|
-
|
|
46300
|
-
|
|
46301
|
-
|
|
46302
|
-
|
|
46303
|
-
|
|
46304
|
-
|
|
46305
|
-
|
|
46306
|
-
|
|
46307
|
-
|
|
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));
|
|
46308
46316
|
}
|
|
46309
46317
|
async function release(ctx, hit) {
|
|
46310
46318
|
const { error: error51 } = await ctx.client.schema("pm").from("issue_claims").delete().eq("issue_id", hit.issue.id).eq("claimed_by", ctx.userId);
|
|
@@ -46634,11 +46642,17 @@ async function tick(ctx, identity, deadTicks) {
|
|
|
46634
46642
|
seen.add(h.ref);
|
|
46635
46643
|
return true;
|
|
46636
46644
|
});
|
|
46637
|
-
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);
|
|
46638
46651
|
const attemptsFor = (h) => attemptsByHit.get(`${h.reason}:${h.issue.id}`) ?? 0;
|
|
46639
|
-
const actionable =
|
|
46652
|
+
const actionable = free.filter((h) => attemptsFor(h) < options.maxAttempts);
|
|
46640
46653
|
if (actionable.length === 0) {
|
|
46641
|
-
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`);
|
|
46642
46656
|
await beat(ctx, { holding: 0, deadTicks, intervalMs: options.intervalMs });
|
|
46643
46657
|
return { invoked: 0, failedToRun: 0 };
|
|
46644
46658
|
}
|
|
@@ -46667,6 +46681,10 @@ async function tick(ctx, identity, deadTicks) {
|
|
|
46667
46681
|
invoked += 1;
|
|
46668
46682
|
const before = { status: hit.issue.status_id, assignee: hit.issue.assignee_id };
|
|
46669
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
|
+
}
|
|
46670
46688
|
let worktree = null;
|
|
46671
46689
|
if (worktreeConfig) {
|
|
46672
46690
|
try {
|
|
@@ -46674,12 +46692,12 @@ async function tick(ctx, identity, deadTicks) {
|
|
|
46674
46692
|
log(` ${hit.ref} worktree ${worktree.path} on ${worktree.branch}`);
|
|
46675
46693
|
} catch (error51) {
|
|
46676
46694
|
failedToRun += 1;
|
|
46695
|
+
await release(ctx, hit);
|
|
46677
46696
|
log(` ${hit.ref} could not prepare a worktree \u2014 not counted against this issue`);
|
|
46678
46697
|
log(` ${error51 instanceof Error ? error51.message.split("\n")[0] : String(error51)}`);
|
|
46679
46698
|
continue;
|
|
46680
46699
|
}
|
|
46681
46700
|
}
|
|
46682
|
-
await claim(ctx, hit);
|
|
46683
46701
|
let run;
|
|
46684
46702
|
let committed = 0;
|
|
46685
46703
|
try {
|
|
@@ -47006,7 +47024,7 @@ Common options
|
|
|
47006
47024
|
--push publish the branch after a run that commits
|
|
47007
47025
|
--interval <seconds> how often to poll (default 20)
|
|
47008
47026
|
|
|
47009
|
-
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
|
|
47010
47028
|
|
|
47011
47029
|
The token comes from CRUO_TOKEN, then --token, then whatever \`login\` stored.
|
|
47012
47030
|
`;
|
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",
|
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
"@cruo/mcp": "workspace:*"
|
|
37
37
|
},
|
|
38
38
|
"bugs": {
|
|
39
|
-
"url": "https://cruo.space/agents"
|
|
39
|
+
"url": "https://cruo.space/docs#agents"
|
|
40
40
|
}
|
|
41
41
|
}
|