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 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
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 expiresAt = new Date(Date.now() + options.harnessTimeoutMs + 3e4);
46298
- const { error: error51 } = await ctx.client.schema("pm").from("issue_claims").upsert(
46299
- {
46300
- issue_id: hit.issue.id,
46301
- claimed_by: ctx.userId,
46302
- claimed_at: (/* @__PURE__ */ new Date()).toISOString(),
46303
- expires_at: expiresAt.toISOString()
46304
- },
46305
- { onConflict: "issue_id" }
46306
- );
46307
- if (error51) log(` could not claim ${hit.ref}: ${error51.message}`);
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 attemptsByHit = await loadAttempts(ctx, hits);
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 = hits.filter((h) => attemptsFor(h) < options.maxAttempts);
46652
+ const actionable = free.filter((h) => attemptsFor(h) < options.maxAttempts);
46640
46653
  if (actionable.length === 0) {
46641
- if (hits.length > 0) log(`${hits.length} issue(s) waiting, all set aside`);
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.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
  }