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 +23 -1
- package/dist/VERSION +1 -1
- package/dist/cli.js +60 -20
- package/dist/index.js +14 -2
- 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.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
|
|
46298
|
-
const { error: error51 } = await ctx.client.schema("
|
|
46299
|
-
|
|
46300
|
-
|
|
46301
|
-
|
|
46302
|
-
|
|
46303
|
-
|
|
46304
|
-
|
|
46305
|
-
|
|
46306
|
-
|
|
46307
|
-
|
|
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.
|
|
46446
|
-
`
|
|
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
|
|
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 =
|
|
46673
|
+
const actionable = free.filter((h) => attemptsFor(h) < options.maxAttempts);
|
|
46640
46674
|
if (actionable.length === 0) {
|
|
46641
|
-
if (
|
|
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.
|
|
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
|
}
|