cruo-agent 0.1.1 → 0.1.3

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.3
package/dist/cli.js CHANGED
@@ -15185,7 +15185,7 @@ var init_zod = __esm({
15185
15185
  });
15186
15186
 
15187
15187
  // ../../packages/core/dist/schemas.js
15188
- var stateCategory, issuePriority, memberRole, projectKey, createProjectInput, createIssueInput, updateIssueInput, moveIssueInput, workflowStateInput, listIssuesFilter;
15188
+ var stateCategory, issuePriority, memberRole, projectKey, createProjectInput, estimateHours, createIssueInput, updateIssueInput, moveIssueInput, workflowStateInput, listIssuesFilter;
15189
15189
  var init_schemas3 = __esm({
15190
15190
  "../../packages/core/dist/schemas.js"() {
15191
15191
  "use strict";
@@ -15213,6 +15213,7 @@ var init_schemas3 = __esm({
15213
15213
  description: external_exports.string().trim().max(2e3).optional(),
15214
15214
  leadId: external_exports.string().uuid().optional()
15215
15215
  });
15216
+ estimateHours = external_exports.number().positive().max(9999).multipleOf(0.01).nullish();
15216
15217
  createIssueInput = external_exports.object({
15217
15218
  projectId: external_exports.string().uuid(),
15218
15219
  title: external_exports.string().trim().min(1).max(300),
@@ -15223,6 +15224,7 @@ var init_schemas3 = __esm({
15223
15224
  parentId: external_exports.string().uuid().nullish(),
15224
15225
  dueDate: external_exports.string().date().nullish(),
15225
15226
  // YYYY-MM-DD
15227
+ estimateHours,
15226
15228
  labelIds: external_exports.array(external_exports.string().uuid()).optional()
15227
15229
  });
15228
15230
  updateIssueInput = external_exports.object({
@@ -15230,7 +15232,8 @@ var init_schemas3 = __esm({
15230
15232
  description: external_exports.string().trim().max(2e4).nullish(),
15231
15233
  priority: issuePriority.optional(),
15232
15234
  assigneeId: external_exports.string().uuid().nullish(),
15233
- dueDate: external_exports.string().date().nullish()
15235
+ dueDate: external_exports.string().date().nullish(),
15236
+ estimateHours
15234
15237
  });
15235
15238
  moveIssueInput = external_exports.object({
15236
15239
  issueId: external_exports.string().uuid(),
@@ -15288,6 +15291,13 @@ var init_identity = __esm({
15288
15291
  }
15289
15292
  });
15290
15293
 
15294
+ // ../../packages/core/dist/effort.js
15295
+ var init_effort = __esm({
15296
+ "../../packages/core/dist/effort.js"() {
15297
+ "use strict";
15298
+ }
15299
+ });
15300
+
15291
15301
  // ../../packages/core/dist/plans.js
15292
15302
  var init_plans = __esm({
15293
15303
  "../../packages/core/dist/plans.js"() {
@@ -15339,6 +15349,7 @@ var init_dist = __esm({
15339
15349
  init_schemas3();
15340
15350
  init_rank();
15341
15351
  init_identity();
15352
+ init_effort();
15342
15353
  init_plans();
15343
15354
  init_mcp_connect();
15344
15355
  init_tree();
@@ -46294,17 +46305,25 @@ async function pollMentions(ctx, identity) {
46294
46305
  );
46295
46306
  }
46296
46307
  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}`);
46308
+ const ttlMs = options.harnessTimeoutMs + 3e4;
46309
+ const { data, error: error51 } = await ctx.client.schema("public").rpc("claim_issue", {
46310
+ issue: hit.issue.id,
46311
+ ttl_ms: ttlMs
46312
+ });
46313
+ if (error51) {
46314
+ log(` could not claim ${hit.ref}: ${error51.message} \u2014 leaving it`);
46315
+ return false;
46316
+ }
46317
+ return data === true;
46318
+ }
46319
+ async function claimedElsewhere(ctx, hits) {
46320
+ if (hits.length === 0) return /* @__PURE__ */ new Set();
46321
+ 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);
46322
+ if (error51) {
46323
+ log(` could not read claims: ${error51.message} \u2014 relying on the claim itself`);
46324
+ return /* @__PURE__ */ new Set();
46325
+ }
46326
+ return new Set((data ?? []).map((r) => r.issue_id));
46308
46327
  }
46309
46328
  async function release(ctx, hit) {
46310
46329
  const { error: error51 } = await ctx.client.schema("pm").from("issue_claims").delete().eq("issue_id", hit.issue.id).eq("claimed_by", ctx.userId);
@@ -46442,8 +46461,18 @@ function systemPrompt(ctx, identity, worktree) {
46442
46461
  `survives. A run that ends with uncommitted changes has produced nothing,`,
46443
46462
  `however good the changes were.`,
46444
46463
  ``,
46445
- `Do not push, and do not merge into any other branch. Publishing is done`,
46446
- `for you after you exit, by the process that started you.`
46464
+ `Do not push, and do not merge into any other branch. That is not a`,
46465
+ `matter of trust \u2014 you have no push tool, and nothing in this loop`,
46466
+ `merges anything (\`CRA-88\`). Releasing is a person's job.`,
46467
+ ...options.push ? [
46468
+ `Your branch is published for you after you exit, by the process`,
46469
+ `that started you. It is pushed, not merged: somebody still has`,
46470
+ `to review and land it.`
46471
+ ] : [
46472
+ `Your branch stays on this machine. Nothing publishes it, so say`,
46473
+ `on the card which branch holds the work \u2014 that name is the only`,
46474
+ `way anyone finds it.`
46475
+ ]
46447
46476
  ] : [],
46448
46477
  ``,
46449
46478
  `You act on the board through the Cruo MCP tools. Start with get_issue to read the`,
@@ -46634,11 +46663,17 @@ async function tick(ctx, identity, deadTicks) {
46634
46663
  seen.add(h.ref);
46635
46664
  return true;
46636
46665
  });
46637
- const attemptsByHit = await loadAttempts(ctx, hits);
46666
+ const heldByOthers = await claimedElsewhere(ctx, hits);
46667
+ const free = hits.filter((h) => !heldByOthers.has(h.issue.id));
46668
+ if (heldByOthers.size > 0) {
46669
+ log(`${heldByOthers.size} issue(s) already being worked by someone else`);
46670
+ }
46671
+ const attemptsByHit = await loadAttempts(ctx, free);
46638
46672
  const attemptsFor = (h) => attemptsByHit.get(`${h.reason}:${h.issue.id}`) ?? 0;
46639
- const actionable = hits.filter((h) => attemptsFor(h) < options.maxAttempts);
46673
+ const actionable = free.filter((h) => attemptsFor(h) < options.maxAttempts);
46640
46674
  if (actionable.length === 0) {
46641
- if (hits.length > 0) log(`${hits.length} issue(s) waiting, all set aside`);
46675
+ if (free.length > 0) log(`${free.length} issue(s) waiting, all set aside`);
46676
+ else if (heldByOthers.size > 0) log(`nothing free \u2014 everything waiting is claimed`);
46642
46677
  await beat(ctx, { holding: 0, deadTicks, intervalMs: options.intervalMs });
46643
46678
  return { invoked: 0, failedToRun: 0 };
46644
46679
  }
@@ -46667,6 +46702,10 @@ async function tick(ctx, identity, deadTicks) {
46667
46702
  invoked += 1;
46668
46703
  const before = { status: hit.issue.status_id, assignee: hit.issue.assignee_id };
46669
46704
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
46705
+ if (!await claim(ctx, hit)) {
46706
+ log(` ${hit.ref} is being worked by another agent \u2014 skipping`);
46707
+ continue;
46708
+ }
46670
46709
  let worktree = null;
46671
46710
  if (worktreeConfig) {
46672
46711
  try {
@@ -46674,12 +46713,12 @@ async function tick(ctx, identity, deadTicks) {
46674
46713
  log(` ${hit.ref} worktree ${worktree.path} on ${worktree.branch}`);
46675
46714
  } catch (error51) {
46676
46715
  failedToRun += 1;
46716
+ await release(ctx, hit);
46677
46717
  log(` ${hit.ref} could not prepare a worktree \u2014 not counted against this issue`);
46678
46718
  log(` ${error51 instanceof Error ? error51.message.split("\n")[0] : String(error51)}`);
46679
46719
  continue;
46680
46720
  }
46681
46721
  }
46682
- await claim(ctx, hit);
46683
46722
  let run;
46684
46723
  let committed = 0;
46685
46724
  try {
@@ -46793,6 +46832,7 @@ async function main() {
46793
46832
  const swept = await sweep(worktreeConfig, options.harnessTimeoutMs + options.prepareTimeout);
46794
46833
  if (swept.length) log(` swept ${swept.length} worktree(s) left by an earlier run: ${swept.join(", ")}`);
46795
46834
  if (options.push) log(` publishing to ${options.pushRemote} after any run that commits`);
46835
+ else log(` --push is off: branches stay on this machine and reach no remote`);
46796
46836
  if (options.allow === "mcp__cruo") {
46797
46837
  log(`! --worktree with the default --allow gives the harness a checkout it cannot read.`);
46798
46838
  log(` Widen it, e.g. --allow "mcp__cruo,Read,Glob,Grep,Edit,Write,Bash(git:*),Bash(pnpm:*)"`);
@@ -47006,7 +47046,7 @@ Common options
47006
47046
  --push publish the branch after a run that commits
47007
47047
  --interval <seconds> how often to poll (default 20)
47008
47048
 
47009
- Full list and what each one is for: https://cruo.space/agents
47049
+ Full list and what each one is for: https://cruo.space/docs#agents
47010
47050
 
47011
47051
  The token comes from CRUO_TOKEN, then --token, then whatever \`login\` stored.
47012
47052
  `;
package/dist/index.js CHANGED
@@ -34897,6 +34897,7 @@ var createProjectInput = external_exports.object({
34897
34897
  description: external_exports.string().trim().max(2e3).optional(),
34898
34898
  leadId: external_exports.string().uuid().optional()
34899
34899
  });
34900
+ var estimateHours = external_exports.number().positive().max(9999).multipleOf(0.01).nullish();
34900
34901
  var createIssueInput = external_exports.object({
34901
34902
  projectId: external_exports.string().uuid(),
34902
34903
  title: external_exports.string().trim().min(1).max(300),
@@ -34907,6 +34908,7 @@ var createIssueInput = external_exports.object({
34907
34908
  parentId: external_exports.string().uuid().nullish(),
34908
34909
  dueDate: external_exports.string().date().nullish(),
34909
34910
  // YYYY-MM-DD
34911
+ estimateHours,
34910
34912
  labelIds: external_exports.array(external_exports.string().uuid()).optional()
34911
34913
  });
34912
34914
  var updateIssueInput = external_exports.object({
@@ -34914,7 +34916,8 @@ var updateIssueInput = external_exports.object({
34914
34916
  description: external_exports.string().trim().max(2e4).nullish(),
34915
34917
  priority: issuePriority.optional(),
34916
34918
  assigneeId: external_exports.string().uuid().nullish(),
34917
- dueDate: external_exports.string().date().nullish()
34919
+ dueDate: external_exports.string().date().nullish(),
34920
+ estimateHours
34918
34921
  });
34919
34922
  var moveIssueInput = external_exports.object({
34920
34923
  issueId: external_exports.string().uuid(),
@@ -53389,6 +53392,9 @@ function issueDetail(project, issue2, opts = {}) {
53389
53392
  ];
53390
53393
  if (issue2.start_date || issue2.due_date)
53391
53394
  lines.push(`Schedule: ${issue2.start_date ?? "\u2014"} \u2192 ${issue2.due_date ?? "\u2014"}`);
53395
+ lines.push(
53396
+ issue2.estimate_hours == null ? "Estimate: none \u2014 nobody has sized this yet" : `Estimate: ${issue2.estimate_hours}h`
53397
+ );
53392
53398
  if (opts.labels && opts.labels.length)
53393
53399
  lines.push(`Labels: ${opts.labels.map((l) => l.name).join(", ")}`);
53394
53400
  if (opts.parent)
@@ -53420,6 +53426,7 @@ var FIELD_LABEL = {
53420
53426
  status_id: "status",
53421
53427
  parent_id: "parent",
53422
53428
  due_date: "due date",
53429
+ estimate_hours: "estimate",
53423
53430
  start_date: "start date",
53424
53431
  board_rank: "board position",
53425
53432
  gantt_pos: "gantt position"
@@ -53778,6 +53785,7 @@ function registerTools(server, getContext2, options = {}) {
53778
53785
  priority: issuePriority.optional(),
53779
53786
  assignee: external_exports.string().optional().describe("Member email, name, or 'me'"),
53780
53787
  due_date: external_exports.string().optional().describe("YYYY-MM-DD"),
53788
+ estimate_hours: external_exports.number().positive().max(9999).optional().describe("Estimated effort in hours. Omit if you do not know \u2014 an absent estimate reads as unestimated, which is not the same as free."),
53781
53789
  labels: external_exports.array(external_exports.string()).optional().describe("Label names to apply"),
53782
53790
  parent: external_exports.string().optional().describe("Make this a sub-issue of an issue in the same project, e.g. CRU-14")
53783
53791
  }
@@ -53808,6 +53816,7 @@ function registerTools(server, getContext2, options = {}) {
53808
53816
  assignee_id: assignee?.id ?? null,
53809
53817
  parent_id: parentId,
53810
53818
  due_date: args.due_date?.trim() || null,
53819
+ estimate_hours: args.estimate_hours ?? null,
53811
53820
  board_rank: rank,
53812
53821
  gantt_pos: ganttPos,
53813
53822
  created_by: ctx.userId
@@ -53836,7 +53845,7 @@ function registerTools(server, getContext2, options = {}) {
53836
53845
  "update_issue",
53837
53846
  {
53838
53847
  title: "Update issue",
53839
- description: "Update an issue's title, description, priority, assignee, due date, or parent. Use assignee 'none', due_date '', or parent 'none' to clear.",
53848
+ description: "Update an issue's title, description, priority, assignee, due date, estimate, or parent. Use assignee 'none', due_date '', estimate_hours 0, or parent 'none' to clear.",
53840
53849
  inputSchema: {
53841
53850
  issue: external_exports.string().describe("Issue reference, e.g. CRU-14"),
53842
53851
  title: external_exports.string().min(1).max(300).optional(),
@@ -53844,6 +53853,7 @@ function registerTools(server, getContext2, options = {}) {
53844
53853
  priority: issuePriority.optional(),
53845
53854
  assignee: external_exports.string().optional().describe("Member email/name/'me', or 'none' to clear"),
53846
53855
  due_date: external_exports.string().optional().describe("YYYY-MM-DD, or '' to clear"),
53856
+ estimate_hours: external_exports.number().min(0).max(9999).optional().describe("Estimated effort in hours; 0 clears it back to unestimated"),
53847
53857
  parent: external_exports.string().optional().describe(
53848
53858
  "Issue reference of the parent (same project), e.g. CRU-14, or 'none' to make it top-level"
53849
53859
  )
@@ -53856,6 +53866,8 @@ function registerTools(server, getContext2, options = {}) {
53856
53866
  if (args.description !== void 0) patch.description = String(args.description).trim() || null;
53857
53867
  if (args.priority !== void 0) patch.priority = args.priority;
53858
53868
  if (args.due_date !== void 0) patch.due_date = String(args.due_date).trim() || null;
53869
+ if (args.estimate_hours !== void 0)
53870
+ patch.estimate_hours = args.estimate_hours > 0 ? args.estimate_hours : null;
53859
53871
  if (args.assignee !== void 0) {
53860
53872
  patch.assignee_id = await resolveAssigneeArg(ctx, args.assignee, project);
53861
53873
  }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "cruo-agent",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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
  }